spring中的父子容器

Spring中的父子容器

背景

在很長的一段時間里面,關于Spring父子容器這個問題我一直沒太關注,但是上次同事碰見一個奇怪的bug于是我決定重新了解一下Spring中的父子容器。

項目是一個老的SSM項目,同事在使用AOP對Controller層的方法進行攔截做token驗證。這個功能在實際的開發(fā)項目中很常見對吧,估計大家都能輕易解決。但是問題就處在了AOP上面,根據AOP失效的八股文全部排查了一遍,問題還是沒有解決。但是神奇的問題出現(xiàn)了,我嘗試把切點放在Service中的方法AOP生效了。然后我看了下配置文件,發(fā)現(xià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");
    }

}

其實問題所在就是父子容器造成的,現(xiàn)在我們使用的SpringBoot中基本上不會出現(xiàn)問題,默認情況下SpringBoot中只會有一個容器,而在傳統(tǒng)的SSM架構中我們很可能會有兩個容器。在傳統(tǒng)的SSM架構中,我們會創(chuàng)建兩個配置文件,一個用來創(chuàng)建Controller層的容器通常是子容器,而Service和Dao層的容器通常就是父容器。

父子容器相關接口

在IOC容器時,Spring中通常會提到兩個頂級接口BeanFactory和ApplicationContext,這兩個都是IOC容器接口,相比BeanFactory而言,ApplicationContext提供了更強大的功能。

HierarchicalBeanFactory

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

public interface HierarchicalBeanFactory extends BeanFactory {

    BeanFactory getParentBeanFactory();

    boolean containsLocalBean(String name);

}

從它名稱可以看出,它是一個有層級的BeanFactory,它提供的兩個方法其中一個就是用來獲取父容器的。

ConfigurableBeanFactory

上面說了HierarchicalBeanFactory提供了獲取父容器的方法,那么父容器是怎么設置的呢?而設置父容器的方法則被定義在ConfigurableBeanFactory接口中。從名字可以看出它是一個可配置的BeanFactory,設置父容器的方法定義如下:

void setParentBeanFactory(BeanFactory parentBeanFactory) throws IllegalStateException;

ApplicationContext

上面講了BeanFactory中獲取和設置父容器相關接口和方法,而ApplicationContext中同樣提供了一個方法用來獲取父容器。

ApplicationContext getParent();

ConfigurableApplicationContext

與BeanFactory中設置父容器一樣,ConfigurableApplicationContext提供了一個用來設置父容器的方法。

void setParent(@Nullable ApplicationContext parent);

特性

通過上面介紹我們明白了什么是父子容器,那么它有哪些特性呢?使用時需要注意什么呢?

示例代碼如下:

  • 父容器配置
@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無法獲取到父容器中的Bean
        Map<String, ParentService> map = childContainer.getBeansOfType(ParentService.class);
        map.forEach((k,v) -> log.info("{} => {}",k,v));
    }
}

ParentService是父容器中的Bean,但是我們在子容器中卻能獲取到,這說明在子容器中是可以獲取到父容器中的Bean的,但是并不是所有方法都能,所以在使用時我們需要注意。這也解釋了一個問題,那就是在SSM架構中為什么我們能在Controller中獲取到Service,如果不是這個特性那我們的肯定是不行的。

父容器不能獲取子容器中的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());
        }
    }
}

上面的代碼運行時會拋出異常,因為父容器是無法獲取到子容器中的Bean的。

SSM中的父子容器

回到我們最初的問題,在SSM中存在這兩個容器,這也是導致我們前面AOP失敗的原因。那么SSM中的父子容器是如何被創(chuàng)建和設置的呢?

web.xml

首先要解答這個問題我們需要先來看一下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>

通常這個配置如上所示,我們需要關注的就兩分別為

ContextLoaderListenerDispatcherServlet

父容器創(chuàng)建

其中ContextLoaderListener就是Servlet中的監(jiān)聽器,當Servlet容器啟動時就會調用contextInitialized()方法進行初始化,該方法的實現(xiàn)如下:

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

initWebApplicationConte()的實現(xiàn)則在ContextLoader這個類中,該方法的實現(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中(后面會用到)
            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;
        }
    }

雖然方法較長,但實際上我們需要關注的就三點:

  • 創(chuàng)建容器

  • 配置并刷新容器

  • 將容器設置到servletContext中

子容器創(chuàng)建

子容器的創(chuàng)建我們需要關注的就是web.xml中DispatcherServlet配置了,DispatcherServlet說白了就是一個Servlet,當Servlet容器在實例化Servlet后就會調用其init()方法就行初始化,而DispatcherServlet的繼承如下圖所示:

DispatcherServlet

init()方法的實現(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();
    }

從實現(xiàn)上可以看出并沒有子容器相關代碼,但是它留了一個方法,用來讓子類擴展實現(xiàn)自己的初始化。而該方法的實現(xiàn)則是在FrameworkServlet中實現(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");
        }
    }

而實際創(chuàng)建子容器的實現(xiàn)則是在initWebApplicationContext()方法中實現(xiàn)的,該方法會創(chuàng)建子容器,并將先前創(chuàng)建的父容器從servletContext中取出來設置為子容器的父容器。

驗證

@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í)行程序最后的打印結果如下:

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

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

解決辦法

回到我們最初的問題,我們現(xiàn)在知道了AOP失效的原因是因為父子容器導致的,因為我們只在父容器中開啟了@AspectJ支持,在子容器中我們并沒有開啟。

只使用一個容器

既然問題是由父子容器導致的,那我們將controller層也交給父容器管理那是不是就可以了。實際上是沒有問題的,但是并不推薦這么做。

開啟子容器@AspectJ支持

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

<aop:aspectj-autoproxy/>

總結

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容