spring中的父子容器

Spring中的父子容器

背景

在很長(zhǎng)的一段時(shí)間里面兔乞,關(guān)于Spring父子容器這個(gè)問(wèn)題我一直沒(méi)太關(guān)注招拙,但是上次同事碰見(jiàn)一個(gè)奇怪的bug于是我決定重新了解一下Spring中的父子容器夹抗。

項(xiàng)目是一個(gè)老的SSM項(xiàng)目糯彬,同事在使用AOP對(duì)Controller層的方法進(jìn)行攔截做token驗(yàn)證条霜。這個(gè)功能在實(shí)際的開(kāi)發(fā)項(xiàng)目中很常見(jiàn)對(duì)吧幸缕,估計(jì)大家都能輕易解決极舔。但是問(wèn)題就處在了AOP上面凤覆,根據(jù)AOP失效的八股文全部排查了一遍,問(wèn)題還是沒(méi)有解決拆魏。但是神奇的問(wèn)題出現(xiàn)了盯桦,我嘗試把切點(diǎn)放在Service中的方法AOP生效了。然后我看了下配置文件渤刃,發(fā)現(xiàn)了問(wèn)題所在拥峦。

  • root-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:component-scan base-package="com.buydeem.container">
        <context:exclude-filter type="regex" expression="com.buydeem.container.controller.*"/>
    </context:component-scan>

    <aop:aspectj-autoproxy/>

</beans>
  • mvc-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:component-scan base-package="com.buydeem.container">
        <context:exclude-filter type="regex" expression="com.buydeem.container.controller.*"/>
    </context:component-scan>

</beans>
  • TokenAspect
@Component
@Aspect
@Slf4j
public class TokenAspect {

    @Pointcut("execution (public * com.buydeem.container.controller..*.*(..))")
    //@Pointcut("execution (public * com.buydeem.container.service..*.*(..))")
    public void point(){

    }

    @Before("point()")
    public void before(){
      log.info("before");
    }

}

其實(shí)問(wèn)題所在就是父子容器造成的,現(xiàn)在我們使用的SpringBoot中基本上不會(huì)出現(xiàn)問(wèn)題卖子,默認(rèn)情況下SpringBoot中只會(huì)有一個(gè)容器略号,而在傳統(tǒng)的SSM架構(gòu)中我們很可能會(huì)有兩個(gè)容器。在傳統(tǒng)的SSM架構(gòu)中洋闽,我們會(huì)創(chuàng)建兩個(gè)配置文件玄柠,一個(gè)用來(lái)創(chuàng)建Controller層的容器通常是子容器,而Service和Dao層的容器通常就是父容器诫舅。

父子容器相關(guān)接口

在IOC容器時(shí)羽利,Spring中通常會(huì)提到兩個(gè)頂級(jí)接口BeanFactory和ApplicationContext,這兩個(gè)都是IOC容器接口刊懈,相比BeanFactory而言这弧,ApplicationContext提供了更強(qiáng)大的功能。

HierarchicalBeanFactory

該接口作為BeanFactory的子接口俏讹,它的定義如下:

public interface HierarchicalBeanFactory extends BeanFactory {

    BeanFactory getParentBeanFactory();

    boolean containsLocalBean(String name);

}

從它名稱可以看出当宴,它是一個(gè)有層級(jí)的BeanFactory,它提供的兩個(gè)方法其中一個(gè)就是用來(lái)獲取父容器的泽疆。

ConfigurableBeanFactory

上面說(shuō)了HierarchicalBeanFactory提供了獲取父容器的方法户矢,那么父容器是怎么設(shè)置的呢?而設(shè)置父容器的方法則被定義在ConfigurableBeanFactory接口中殉疼。從名字可以看出它是一個(gè)可配置的BeanFactory梯浪,設(shè)置父容器的方法定義如下:

void setParentBeanFactory(BeanFactory parentBeanFactory) throws IllegalStateException;

ApplicationContext

上面講了BeanFactory中獲取和設(shè)置父容器相關(guān)接口和方法,而ApplicationContext中同樣提供了一個(gè)方法用來(lái)獲取父容器瓢娜。

ApplicationContext getParent();

ConfigurableApplicationContext

與BeanFactory中設(shè)置父容器一樣挂洛,ConfigurableApplicationContext提供了一個(gè)用來(lái)設(shè)置父容器的方法。

void setParent(@Nullable ApplicationContext parent);

特性

通過(guò)上面介紹我們明白了什么是父子容器眠砾,那么它有哪些特性呢虏劲?使用時(shí)需要注意什么呢?

示例代碼如下:

  • 父容器配置
@Component
public class ParentService {
}


@Configuration
public class ParentContainerConfig {

    @Bean
    public ParentService parentService(){
        return new ParentService();
    }
}
  • 子容器配置
@Component
public class ChildService {
}

@Configuration
public class ChildContainerConfig {

    @Bean
    public ChildService childService(){
        return new ChildService();
    }
}

子容器能獲取到父容器中的Bean

@Slf4j
public class App {
    public static void main(String[] args) {
        //父容器
        ApplicationContext parentContainer = new AnnotationConfigApplicationContext(ParentContainerConfig.class);
        //子容器
        ConfigurableApplicationContext childContainer = new AnnotationConfigApplicationContext(ChildContainerConfig.class);
        childContainer.setParent(parentContainer);
        //從子容器中獲取父容器中的Bean
        ParentService parentService = childContainer.getBean(ParentService.class);
        log.info("{}",parentService);
        //getBeansOfType無(wú)法獲取到父容器中的Bean
        Map<String, ParentService> map = childContainer.getBeansOfType(ParentService.class);
        map.forEach((k,v) -> log.info("{} => {}",k,v));
    }
}

ParentService是父容器中的Bean,但是我們?cè)谧尤萜髦袇s能獲取到柒巫,這說(shuō)明在子容器中是可以獲取到父容器中的Bean的励堡,但是并不是所有方法都能,所以在使用時(shí)我們需要注意堡掏。這也解釋了一個(gè)問(wèn)題应结,那就是在SSM架構(gòu)中為什么我們能在Controller中獲取到Service,如果不是這個(gè)特性那我們的肯定是不行的泉唁。

父容器不能獲取子容器中的Bean

子容器能獲取到父容器中的Bean鹅龄,但是父容器卻不能獲取到子容器中的Bean。

@Slf4j
public class App {
    public static void main(String[] args) {
        //父容器
        ApplicationContext parentContainer = new AnnotationConfigApplicationContext(ParentContainerConfig.class);
        //子容器
        ConfigurableApplicationContext childContainer = new AnnotationConfigApplicationContext(ChildContainerConfig.class);
        childContainer.setParent(parentContainer);

        try {
            ChildService childService = parentContainer.getBean(ChildService.class);
            log.info("{}",childService);
        }catch (NoSuchBeanDefinitionException e){
            log.error(e.getMessage());
        }
    }
}

上面的代碼運(yùn)行時(shí)會(huì)拋出異常亭畜,因?yàn)楦溉萜魇菬o(wú)法獲取到子容器中的Bean的扮休。

SSM中的父子容器

回到我們最初的問(wèn)題,在SSM中存在這兩個(gè)容器贱案,這也是導(dǎo)致我們前面AOP失敗的原因肛炮。那么SSM中的父子容器是如何被創(chuàng)建和設(shè)置的呢止吐?

web.xml

首先要解答這個(gè)問(wèn)題我們需要先來(lái)看一下web.xml中的配置宝踪。

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/root-context.xml</param-value>
  </context-param>

  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/mvc-context.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

</web-app>

通常這個(gè)配置如上所示,我們需要關(guān)注的就兩分別為

ContextLoaderListenerDispatcherServlet碍扔。

父容器創(chuàng)建

其中ContextLoaderListener就是Servlet中的監(jiān)聽(tīng)器瘩燥,當(dāng)Servlet容器啟動(dòng)時(shí)就會(huì)調(diào)用contextInitialized()方法進(jìn)行初始化,該方法的實(shí)現(xiàn)如下:

    @Override
    public void contextInitialized(ServletContextEvent event) {
        initWebApplicationContext(event.getServletContext());
    }

initWebApplicationConte()的實(shí)現(xiàn)則在ContextLoader這個(gè)類中不同,該方法的實(shí)現(xiàn)如下:

    public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
            throw new IllegalStateException(
                    "Cannot initialize context because there is already a root application context present - " +
                            "check whether you have multiple ContextLoader* definitions in your web.xml!");
        }

        servletContext.log("Initializing Spring root WebApplicationContext");
        Log logger = LogFactory.getLog(ContextLoader.class);
        if (logger.isInfoEnabled()) {
            logger.info("Root WebApplicationContext: initialization started");
        }
        long startTime = System.currentTimeMillis();

        try {
            // Store context in local instance variable, to guarantee that
            // it is available on ServletContext shutdown.
            if (this.context == null) {
                //創(chuàng)建WebApplicationContext容器
                this.context = createWebApplicationContext(servletContext);
            }
            if (this.context instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
                if (!cwac.isActive()) {
                    // The context has not yet been refreshed -> provide services such as
                    // setting the parent context, setting the application context id, etc
                    if (cwac.getParent() == null) {
                        // The context instance was injected without an explicit parent ->
                        // determine parent for root web application context, if any.
                        ApplicationContext parent = loadParentContext(servletContext);
                        cwac.setParent(parent);
                    }
                    //配置并刷新WebApplicationContext
                    configureAndRefreshWebApplicationContext(cwac, servletContext);
                }
            }
            //將WebApplicationContext的引用保存到servletContext中(后面會(huì)用到)
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
            if (ccl == ContextLoader.class.getClassLoader()) {
                currentContext = this.context;
            }
            else if (ccl != null) {
                currentContextPerThread.put(ccl, this.context);
            }

            if (logger.isInfoEnabled()) {
                long elapsedTime = System.currentTimeMillis() - startTime;
                logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
            }

            return this.context;
        }
        catch (RuntimeException | Error ex) {
            logger.error("Context initialization failed", ex);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
            throw ex;
        }
    }

雖然方法較長(zhǎng)厉膀,但實(shí)際上我們需要關(guān)注的就三點(diǎn):

  • 創(chuàng)建容器

  • 配置并刷新容器

  • 將容器設(shè)置到servletContext中

子容器創(chuàng)建

子容器的創(chuàng)建我們需要關(guān)注的就是web.xml中DispatcherServlet配置了,DispatcherServlet說(shuō)白了就是一個(gè)Servlet二拐,當(dāng)Servlet容器在實(shí)例化Servlet后就會(huì)調(diào)用其init()方法就行初始化服鹅,而DispatcherServlet的繼承如下圖所示:

DispatcherServlet

init()方法的實(shí)現(xiàn)則是在HttpServletBean中,方法定義如下:

    public final void init() throws ServletException {

        // Set bean properties from init parameters.
        PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
        if (!pvs.isEmpty()) {
            try {
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
                initBeanWrapper(bw);
                bw.setPropertyValues(pvs, true);
            }
            catch (BeansException ex) {
                if (logger.isErrorEnabled()) {
                    logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
                }
                throw ex;
            }
        }

        // Let subclasses do whatever initialization they like.
        initServletBean();
    }

從實(shí)現(xiàn)上可以看出并沒(méi)有子容器相關(guān)代碼百新,但是它留了一個(gè)方法企软,用來(lái)讓子類擴(kuò)展實(shí)現(xiàn)自己的初始化。而該方法的實(shí)現(xiàn)則是在FrameworkServlet中實(shí)現(xiàn)的饭望,源碼如下:

    protected final void initServletBean() throws ServletException {
        getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
        if (logger.isInfoEnabled()) {
            logger.info("Initializing Servlet '" + getServletName() + "'");
        }
        long startTime = System.currentTimeMillis();

        try {
            this.webApplicationContext = initWebApplicationContext();
            initFrameworkServlet();
        }
        catch (ServletException | RuntimeException ex) {
            logger.error("Context initialization failed", ex);
            throw ex;
        }

        if (logger.isDebugEnabled()) {
            String value = this.enableLoggingRequestDetails ?
                    "shown which may lead to unsafe logging of potentially sensitive data" :
                    "masked to prevent unsafe logging of potentially sensitive data";
            logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
                    "': request parameters and headers will be " + value);
        }

        if (logger.isInfoEnabled()) {
            logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
        }
    }

而實(shí)際創(chuàng)建子容器的實(shí)現(xiàn)則是在initWebApplicationContext()方法中實(shí)現(xiàn)的仗哨,該方法會(huì)創(chuàng)建子容器,并將先前創(chuàng)建的父容器從servletContext中取出來(lái)設(shè)置為子容器的父容器铅辞。

驗(yàn)證

@Component
public class HelloService {

    @Autowired
    private ApplicationContext context;

    public String sayHello(){
        return "Hello World";
    }

    public ApplicationContext getContext(){
        return context;
    }
}

@RestController
@Slf4j
public class HelloWorldController {

    @Autowired
    private HelloService helloService;
    @Autowired
    private ApplicationContext context;


    @GetMapping("hello")
    public String helloWorld(){
        //獲取Service中的容器
        ApplicationContext parentContext = helloService.getContext();
        //service中的容器并不等于controller中的容器
        log.info("parentContext == context ? {}",parentContext == context);
        //controller中的容器的父容器就是service中的容器
        log.info("{}",parentContext == context.getParent());
        return helloService.sayHello();
    }
}

上面代碼中我們分別在HelloService和HelloWorldController中分別注入ApplicationContext厌漂,執(zhí)行程序最后的打印結(jié)果如下:

14:45:23.443 [http-nio-8080-exec-2] INFO  c.b.c.c.HelloWorldController - parentContext == context ? false
14:45:23.451 [http-nio-8080-exec-2] INFO  c.b.c.c.HelloWorldController - true

從上面的打印結(jié)果可以看出HelloService和HelloWorldController中的容器并不是同一個(gè)。

解決辦法

回到我們最初的問(wèn)題斟珊,我們現(xiàn)在知道了AOP失效的原因是因?yàn)楦缸尤萜鲗?dǎo)致的苇倡,因?yàn)槲覀冎辉诟溉萜髦虚_(kāi)啟了@AspectJ支持,在子容器中我們并沒(méi)有開(kāi)啟。

只使用一個(gè)容器

既然問(wèn)題是由父子容器導(dǎo)致的旨椒,那我們將controller層也交給父容器管理那是不是就可以了胜嗓。實(shí)際上是沒(méi)有問(wèn)題的,但是并不推薦這么做钩乍。

開(kāi)啟子容器@AspectJ支持

在子容器的配置文件中增加如下配置:

<aop:aspectj-autoproxy/>

總結(jié)

對(duì)于Spring中父子容器的內(nèi)容就講到這里了辞州,后續(xù)如果還有新的發(fā)現(xiàn)會(huì)繼續(xù)更新相關(guān)內(nèi)容。文中示例代碼地址:
https://github.com/I-Like-Pepsi/spring-example.git

本文由mdnice多平臺(tái)發(fā)布

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末寥粹,一起剝皮案震驚了整個(gè)濱河市变过,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌涝涤,老刑警劉巖媚狰,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異阔拳,居然都是意外死亡崭孤,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門糊肠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)辨宠,“玉大人,你說(shuō)我怎么就攤上這事货裹∴托危” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵弧圆,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我搔预,道長(zhǎng)霹期,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上张弛,老公的妹妹穿的比我還像新娘滩字。我一直安慰自己,他們只是感情好诀蓉,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布根资。 她就那樣靜靜地躺著裤纹,像睡著了一般奸汇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上戳吝,一...
    開(kāi)封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天肩刃,我揣著相機(jī)與錄音,去河邊找鬼呢燥。 笑死崭添,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的叛氨。 我是一名探鬼主播呼渣,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼寞埠!你這毒婦竟也來(lái)了屁置?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤仁连,失蹤者是張志新(化名)和其女友劉穎蓝角,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體饭冬,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡使鹅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了昌抠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片患朱。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖扰魂,靈堂內(nèi)的尸體忽然破棺而出麦乞,到底是詐尸還是另有隱情蕴茴,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布姐直,位于F島的核電站倦淀,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏声畏。R本人自食惡果不足惜撞叽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望插龄。 院中可真熱鬧愿棋,春花似錦、人聲如沸均牢。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)瞭空。三九已至,卻和暖如春昔字,著一層夾襖步出監(jiān)牢的瞬間垮庐,已是汗流浹背松邪。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留哨查,地道東北人逗抑。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像寒亥,于是被迫代替她去往敵國(guó)和親邮府。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容