关于springmvc父子容器概念的思考

首先先看下经典的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">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>
</web-app>

我们知道tomcat在启动的时候会加载这个web.xml配置文件,加载玩之后会按照listener,filter,servlet的顺序进行实例化和初始化的操作(这个顺序需要看tomcat源码),那么对于上述这个配置文件解析之后会先加载org.springframework.web.context.ContextLoaderListener,这个类实现了

javax.servlet.ServletContextListener这个接口,那么一定会调用对应的javax.servlet.ServletContextListener#contextInitialized这个方法,而这个方法在org.springframework.web.context.ContextLoaderListener中的实现如下:

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

initWebApplicationContext会调用到org.springframework.web.context.ContextLoader#initWebApplicationContext

在这个方法中会调用org.springframework.web.context.ContextLoader#createWebApplicationContext来创建spring容器对象,并在后续进行refresh:

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) {//创建容器对象
         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);
            }
           //刷新容器,会调用到spring容器启动的时候经典的refresh方法
            configureAndRefreshWebApplicationContext(cwac, 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;
   }
}

接下来就是加载springmvc子容器了,也就是org.springframework.web.servlet.DispatcherServlet的初始化,它继承了org.springframework.web.servlet.FrameworkServlet,servlet加载的顺序此处不作赘述,org.springframework.web.servlet.FrameworkServlet在初始化的时候会调用org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext这个方法来初始化springmvc子容器:

protected WebApplicationContext initWebApplicationContext() {
   WebApplicationContext rootContext =
         WebApplicationContextUtils.getWebApplicationContext(getServletContext());
   WebApplicationContext wac = null;

   if (this.webApplicationContext != null) {
      // A context instance was injected at construction time -> use it
      wac = this.webApplicationContext;
      if (wac instanceof ConfigurableWebApplicationContext) {
         ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
         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 -> set
               // the root application context (if any; may be null) as the parent
               cwac.setParent(rootContext);
            }
            configureAndRefreshWebApplicationContext(cwac);
         }
      }
   }
   if (wac == null) {
      // No context instance was injected at construction time -> see if one
      // has been registered in the servlet context. If one exists, it is assumed
      // that the parent context (if any) has already been set and that the
      // user has performed any initialization such as setting the context id
      wac = findWebApplicationContext();
   }
   if (wac == null) {
      // No context instance is defined for this servlet -> create a local one
      wac = createWebApplicationContext(rootContext);
   }

   if (!this.refreshEventReceived) {
      // Either the context is not a ConfigurableApplicationContext with refresh
      // support or the context injected at construction time had already been
      // refreshed -> trigger initial onRefresh manually here.
      synchronized (this.onRefreshMonitor) {
         onRefresh(wac);
      }
   }

   if (this.publishContext) {
      // Publish the context as a servlet context attribute.
      String attrName = getServletContextAttributeName();
      getServletContext().setAttribute(attrName, wac);
   }

   return wac;
}

WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());

其中标红的这一行代码就是获取前面已经创建好的spring服容器,但是不明白为什么this.webApplicationContext这个属性去判断是否需要创建子容器,这个属性在FrameworkServlet里面只有两个方法会进行set值:

public void setApplicationContext(ApplicationContext applicationContext) {
   if (this.webApplicationContext == null && applicationContext instanceof WebApplicationContext) {
      this.webApplicationContext = (WebApplicationContext) applicationContext;
      this.webApplicationContextInjected = true;
   }
}
public FrameworkServlet(WebApplicationContext webApplicationContext) {
   this.webApplicationContext = webApplicationContext;
}

但是很遗憾,在springmvc项目中打断点发现都没有调用,在springboot中则会调用setApplicationContext这个方法

这个属性如果为空的话,就会调用org.springframework.web.servlet.FrameworkServlet#createWebApplicationContext(org.springframework.web.context.WebApplicationContext)去创建一个子容器,并将子容器里面的parant属性设置为rootContext

因为有了父子容器的关系,所以我们在springmvc项目中通常要讲controller注入到springmvc子容器,不能注入到spring父容器中,否则会出现404,而service,dao层则只需要注入到spring容器

但是现在大家普遍用的比较多的是springboot,我们在配置的时候也没要求controller只能注入到子容器中,所以我猜测在springboot中应该不再有父子容器的概念,最终在跟踪源码的时候发现确实如此,springboot中子容器直接复用父容器,不会创建子容器,也就是在前面提到的this.webApplicationContext这个属性不为空,因为org.springframework.web.servlet.FrameworkServlet实现了org.springframework.context.ApplicationContextAware这个接口,这个接口我想大家都很熟悉,而这个接口的实现,在spring容器启动过程中,通过BeanpostProcessor来调用org.springframework.context.ApplicationContextAware#setApplicationContext这个方法的填充applicationcontext,FrameworkServlet实现如下:

public void setApplicationContext(ApplicationContext applicationContext) {
   if (this.webApplicationContext == null && applicationContext instanceof WebApplicationContext) {
      this.webApplicationContext = (WebApplicationContext) applicationContext;
      this.webApplicationContextInjected = true;
   }
}

总结:

springmvc启动过程:

tomcat启动,加载web.xml-》加载org.springframework.web.context.ContextLoaderListener创建spring父容器-》加载org.springframework.web.servlet.DispatcherServlet创建springmvc子容器

springboot启动过程:

spring容器启动-》加载org.springframework.web.servlet.DispatcherServlet,通过org.springframework.context.ApplicationContextAware#setApplicationContext直接复用已经创建好的spring容器

最后,没想明白的是在org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext这个方法中为什么不直接复用这个方法第一行取到的那个rootContext(已经创建好的spring容器),而是要单独通过

private WebApplicationContext webApplicationContext

这么一个属性来判断

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值