3.[springMvc]spring mvc 父子容器

本文深入剖析 Spring MVC 中的容器机制,包括父容器和子容器的创建过程及原理,探讨容器间的继承关系及其实际应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

spring MVC的实现方式

我们在早期的项目开发过程中,在web框架方面要不使用原生的servlet进行编程,要么使用struts,要么使用spring mvc web框架,但是在目前的行业中,可能spring mvc使用的频率是最高的,我记得我在2012年的时候在一家软件公司,公司中就使用的是spring的mvc,但是当初还没有微服务这个概念,都是前后端在一起的,前后端没有分离,使用的是sprinv mvc的视图解析器,前端使用的framemarker模板引擎技术实现的数据展示。到现如今,struts2基本上已经退出历史舞台了,特别是现在的微服务中使用的都是spring的一整套解决方案,但是我们在早期的spring mvc中,可能大家搭建环境的时候影响都非常深刻,在web.xml中会配置两个xml文件,比如像下面这种:

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath:spring.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>classpath:spring-servlet.xml</param-value>
  </init-param>

  <load-on-startup>1</load-on-startup>
</servlet>

在上面的xml文件中有一个监听器,一个servlet,在使用这种方式来配置我们的项目的时候,其实这两个配置就是spring mvc启动的时候需要根据这两个来创建容器,其中ContextLoaderListener是spring的默认容器,也就是在ContextLoaderListener中会创建一个父容器,作为我们项目中来使用,而DispatcherServlet是mvc中非常重要的一个servlet,这个servlet在启动过程中也会创建一个子容器,在controller中使用,这两个容器的创建过程都是tomcat本身来完成的,前面我们说过tomcat在启动的过程中会注册监听器,当容器向内引爆的启动到了context容器过后,那么这个时候启动了context容器过后,就会调用注册的监听器,具体的tomcat代码在StartdarContext中的startInternal启动conext容器的方法里面
在这里插入图片描述
这里就会去调用注册的监听器中的contextInitialized方法
在这里插入图片描述
来创建父容器,父容器在也是spring的一个IOC容器,只是这里面存放的是非controller的bean对象,但是如果把controller的扫描路径也放进去,也能扫描出来controller来存放到里面,这里就来说下父子容器。

spring 父子容器

通过从上面的web.xml中我们可以知道,tomcat在启动过程中会调用一个监听器和一个servlet,那么这两个类都是spring提供的实现,所以这里虽然是tomcat作为web容器启动sevlet容器,但是其实是spring来完成的servle容器的初始化和启动,根据上面两个我们可以简单看下spring的配置文件:
spring.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:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">


    <context:component-scan  base-package="com.dev1.web.service"/>
    <mvc:annotation-driven />



</beans>

spring-servlet.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:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">


    <context:component-scan base-package="com.dev1.web.controller"/>
    <mvc:annotation-driven />



</beans>

spring作为父容器,而spring-servlet作为子容器启动,父容器是在监听器ContextLoaderListener中完成的启动,而子容器也就是web容器是在DispatcherServlet中完成的启动;
那么如果说我只有一个容器,也就是我只配置一个配置文件有没有问题,也没有问题或者说两个配置文件都配置一样的路径,也没有问题,spring也会启动成功,但是注意的是,在service中也就是父容器中是不能注入父容器controller对象的,而在conroller中是可以注入父容器中的sevice bean对象的,这个是一个继承关系,类似于java中类的继承关系,父类中是不可以访问子类的方法的,而子类是可以访问父类的方法的,这个在前面的spring专题的笔记中在源码中都已经体现了,sprng在getBean的时候,如果当前容器没有获取到,会去调用getParent获取父容器来找bean,如果都没有找到才会报错,所以要明白的是父子容器的一个关系,但是如果说你把controller的扫描路径也配置到父容器中,那么也可以扫描出来的,但是这样就很乱,子容器和父容器中有相同的对象在一起,这样不仅浪费了资源,而且启动过程性能还要受一定的影响,这样做没有意义,要不你把父子容器的bean对象进行分类存储,要不就不要父子容器,直接在一个容器里面搞多好,在controller注入的时候还不需要找两次,当前容器找不到还要去父容器去找。在早期的spring mvc项目中,其实是分了两个 容器的,那么为什么要怎么做,在早期的项目中,市面上还有其他的web框架,sprig只是作为一个后端服务的ioc框架,而比如像struts一样,也有自己的容器框架,那么两个配置文件,把各自的bean都放在对应的xml文件中,那么这样比如说替换框架什么的就很方面,如果你都在一个配置文件中,首先你还要区分出那些bean是controller的,那些是service的,这样做就乱,分开过后子容器管理子容器的bean,父容器管理父容器的bean,并且父容器不能去访问子容器的bean,但是到目前为止,现在都是零配置的的项目了,已经没有分父子容器了,像spring boot已经没有分父子容器了,都在一个容器中,因为现在spring有了一整套生态了,所有的东西都是自己的,controller也是自己的mvc,所以先应该 很多项目都没有父子容器了。

父容器创建过程

我们前面已经说了父容器的创建是在ContextLoadLIstener中完成的,所以我这里就开始分析ContextLoadListener的父容器创建过程的源码,虽然说父容器的创建是在ContextLoadLIstener,但是其实创建的过程都是大同小异,无非就是获取到容器类,然后得到容器类的实例对象,然后加载web.xml中配置的spring配置文件路径,然后将xml中bean标签扫描成一个一个的BeanDefinition,然后refresh,启动容器,最后将创建成功的容器对象实例放入到servlet的上下文中,那么只要servlet的生命周期内,这个容器对象都是可用的,这里值得一说的是我们在服务层经常会实现一个接口ApplicationContextAware接口,然后可以得到spring当前容器的上下文对象,然后有时候会通过这个对象获取到sevlet上下文信息,其实这个是在容器的创建过程中将servlet的上下文信息添加到了spring的上下文中,这样两个上下文就可以相通了,这个在源码中会有体现,ContextLoadLIstener这个监听器还隐藏了一个非常有用的扩展点,我们通过源码可以知道ContextLoadLIstener是创建父容器的过程,但是这里隐藏了一个扩展点就是我们可以再次创建一个顶级的容器,然后作为一个最顶级的父级容器,然后ContextLoadLIstener创建的容器作为第二级容器,这个待会儿通过源码和实例来分析;
ContextLoadLIstener是一个监听器,在tomcat扫描的时候会注册这个监听器,然后容器启动完成过后会来回调这个监听器的contextInitialized方法,所以源码如下:

/**
 * Initialize the root web application context.
 * 这个就是spring提供给容器回调的一个监听器中回调的方法
 * 在这个监听器的方法中,spring启动了一个父容器,这个父容器就是
 * 启动加载了我们服务层的一些bean,这里加载得到的父容器,spring会将
 * 它放在servlet的上下文中
 * 1.找到父容器class对象;
 * 2.得到父容器XmlWebApplicationContext的实例对象;
 * 3.将servlet上下文信息放入到容器中;
 * 4.设置容器的spring配置文件加载路径;
 * 5.调用容器的refresh方法启动容器(在容器的obtainFreshBeanFactory加载xml文件的bean标签);
 * 6.将创建的容器对象设置到servlet上下文中;
 * 7.将创建的容器放入一个map<当前线程的类加载器,容器>.
 */
@Override
public void contextInitialized(ServletContextEvent event) {
   initWebApplicationContext(event.getServletContext());
}

initWebApplicationContext

initWebApplicationContext是父容器的创建过程,我们这里详细来看下这个方法

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
   /**
    * 这里先获取一个属性WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,这个名字就是
    * 父容器的名字,从servlet的上下文中获取,如果说这里获取到的容器不为空,则直接报错
    * 因为这个方法就是来创建父容器的,如果这里为空的话,那么肯定直接报错的,因为已经有父容器存在了
    * 什么情况下会出现这里获取到不为空呢?我觉得除非了你配置了多个监听器,而这些监听器都在创建父容器,
    * 所以说一个web框架的spring项目中,不可能存在多个父容器
    */
   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.
      //context是当前的容器,创建父容器的时候这里一般都是为空的
      //如果不为空,则上面就会报错
      if (this.context == null) {
         //得到容器上下文的实例对象
         this.context = createWebApplicationContext(servletContext);
      }
      //从createWebApplicationContext方法可知,我们这里得到的容器上下文类肯定是ConfigurableWebApplicationContext类型
      //否则都要报错
      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
            /**
             * 下面的这个方法是什么意思呢?loadParentContext是一个空实现,返回的是一个null
             * 其实这里就是spring在设计的时候考虑到一个问题就是,我这里创建的是父容器,在DispatcherServletWebRequest
             * 中创建的是controoler的子容器,那么spring这里的下面几行代码是什么意思呢?
             * 其实我们读源码的目的就是要知道它有哪些扩展点,其实就是说如果说当前创建出来的
             *容器的父容器如果是空的,那么可以提供一个给子类去实现,比如你又添加了一个子容器的实现,
             * 那么这里通过你的实现类继承了ContextLoader去重写了loadParentContext,得到一个父容器
             * 也就是说这里的父容器的父容器,可以设置到当前父容器的父容器中去
             * 所以我的理解就是下面的if代码块中的两行代码的意思就是为了提供给子类去扩展的,
             * 去扩展自己的实现的个性化需求的
             */
            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);
         }
      }
      //将当前创建的容器设置到servlet上下文中,也就是创建一个新的容器,作为父容器放置到servlet上下文中
      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;
   }
}

createWebApplicationContext

在这里插入代码片
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
   //这里得到一个容器上下文类的calss对象,判断得到的类是否是ConfigurableWebApplicationContext
   //如果不是ConfigurableWebApplicationContext则报错,从determineContextClass我们知道
   //得到的类是XmlWebApplicationContext类,是继承了ConfigurableWebApplicationContext的
   //所以这里是不满足下面的if条件的
   Class<?> contextClass = determineContextClass(sc);
   if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
      throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
            "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
   }
   //得到XmlWebApplicationContext的实例对象
   return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

configureAndRefreshWebApplicationContext

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
   if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
      // The application context id is still set to its original default value
      // -> assign a more useful id based on available information
      //这里是判断是否有配置一个容器的ID,如果没有就spring就生成一个
      String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
      if (idParam != null) {
         wac.setId(idParam);
      }
      else {
         // Generate default id...
         wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
               ObjectUtils.getDisplayString(sc.getContextPath()));
      }
   }

   /**
    * 这里将servlet的上下文添加到容器中, 这样的话,在容器中可以访问servlet中,也就是容器上下文相通 了
    */

   wac.setServletContext(sc);
   //这里是获取web.xml中的ContextLoadListener中配置的contextConfigLocation路径,也就是我们常看见的
   /**
    *classpath:spring-servlet.xml 文件,这里把获取到的配置文件的路径设置到容器中去
    * 因为设置进去过后,在refresh中会去加载这个配置文件
    */
   String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
   if (configLocationParam != null) {
      //将配置文件设置到上下文中
      wac.setConfigLocation(configLocationParam);
   }

   // The wac environment's #initPropertySources will be called in any case when the context
   // is refreshed; do it eagerly here to ensure servlet property sources are in place for
   // use in any post-processing or initialization that occurs below prior to #refresh
   //这里是获取环境变量信息,也就是spring中Environment对象,将系统的、项目的环境信息放入到这个对象中,然后在把servlet中的初始化对象放入对象中
   ConfigurableEnvironment env = wac.getEnvironment();
   if (env instanceof ConfigurableWebEnvironment) {
      ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
   }

   customizeContext(sc, wac);
   //刷新容器,也就是spring中最核心的容器启动过程,这个
   //比较复杂,之前已经全面分析过一次了,这里主要看下我们在web.xml中配置的文件是如何加载成一个一个的BeanDefinition的
   /**
    * 在refresh方法中的obtainFreshBeanFactory完成的对spring.xml中的classpath:spring.xml文件的解析,并且将所有的bean标签解析成一个
    * 一个的BeanDefinition
    * 下面的refresh方法调用完成过后,当前容器就启动完成了
    */

   wac.refresh();
}

上面贴的代码就是spring在创建父容器的核心代码,代码的注释在贴的代码上已经写的非常详细了,其他我就不说了,我这里说下创建父容器的两个核心点,一个是刚刚说的创建父容器的一个扩展点,一个是创建的容器和一般比如spring boot的创建的容器的区别;

首先我们要明白的是这种方式是在xml的配置文件中配置了一个spring.xml的文件路径,然后加载的时候会去加载这个目录,具体的加载这个这个xml然后将bean标签解析成一个一个的BeanDefinition是在spring的最最核心的一个类AbstractApplicationContext中的refresh方法中的obtainFreshBeanFactory得到一个工厂的方法里面,如果是web 的方式,那么会调用AbstractRefreshableApplicationContext中的refreshBeanFactory去实现的bean标签解析成一个一个的BeanDefinition这里就不多说了。

再者就是在创建父容器过程中发现的一个扩展点,我们看源码的目的之一就是要发现源码中提供的扩展点,方便以后我们自己来扩展,在上面贴的代码中的initWebApplicationContext方法中代码片段如下:
在这里插入图片描述
这里会演示下如何利用这个扩展点

示例

我这里新建了一个工厂dev-201121,因为今天的日期是11月21号,难得去想名字了,我这里使用的是tomcat的内嵌版本,通过mvn来启动的,关于tomcat的容器概念前面已经说了,我这里只提如何利用tomcat的内嵌版本来启动,先来看pom:

<plugin>
    <groupId>org.apache.tomcat.maven</groupId>
    <artifactId>tomcat7-maven-plugin</artifactId>
    <version>2.2</version>
    <configuration>
        <path>/</path>
        <port>8080</port>
    </configuration>
</plugin>

然后在启动的窗口里面新增一个mvn启动
在这里插入图片描述
就可以了;现在我们启动一个spring mvc的简单项目,配置两个容器,spring的配置文件如下:

//父容器的配置,里面当然可以配置bean的标签,只是这里我没有配置
<?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:p="http://www.springframework.org/schema/p"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">


//父容器只扫描我的service层的bean
    <context:component-scan base-package="com.dev201121.service"/>
    <mvc:annotation-driven />



</beans>


//子容器的配置
<?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:p="http://www.springframework.org/schema/p"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">


//子容器只扫描我web的bean,这个一定要分开,当然不分开也不报错,但是。。。这个看你吧
    <context:component-scan base-package="com.dev201121.web"/>
    <mvc:annotation-driven />



</beans>

我只想说如果你不分容器来的话,就是整个项目中只有一个spring容器的话,那么你就 可以虽然怎么配置扫描路径,如果是分了容器的话,最后每个容器都不要有重复的bean,因为那样很乱,而且spring的单例池的概念是什么呢?就是进入单例池的对象在spring的整个生命周期中只存在一个对象,如果一个bean在每个bean的容器中都扫描进去了,那么在这个系统中就会存在几个实例对象,这既是浪费了空间,也毫无意义;我们都知道spring的getBean会在当前容器中找,如果找不到就去父容器找,如果你觉得这样性能有影响,但是我只想说这只是在启动阶段的=依赖注入阶段完成的时候,如果你实在忍受不了, 那么就不要启动那么多容器,就不要父容器 ,全面塞进一个容器就好了。既然用了父子容器就要按照父子容器的类型来存放不同类型的bean。

Controller:

@RestController
public class HelloController implements ApplicationContextAware  {

    private ApplicationContext applicationContext;


    @RequestMapping("/hello.do")
    public String hello(){
        System.out.println("hello....");
        return "ni hao ";
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
我们在15行打个断点,看下父子容器中的bean
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201221104532104.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NjamF2YQ==,size_16,color_FFFFFF,t_70)
可以看到当前的容器下面有个parent就是XmlWebApplicationContext,就是父容器,就是上面分析的源码中的父容器;

上面提到了spring在创建的过程中提供了一个扩展点,就是可以设置一个顶级的父容器,我们分析下如何来利用这扩展,其实非常简单,spring的ContextLoad中提供了一个方法可以让子类去重写,那么我只需要建立一个新的监听器,然后重写这个父类中的得到一个顶级的父类容器就可以了,所以我这里新建一个监听器:

```java
public class MyLinstener extends ContextLoaderListener {


    @Override
    protected ApplicationContext loadParentContext(ServletContext servletContext) {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
        ac.register(AppConfig.class);com.dev201121.service
        ac.refresh();
        System.out.println("自定义的容器启动完毕,作为顶级父级容器来使用");

        return ac;

    }
}

在这个顶级的父容器中,我使用了注解的方式来扫描我指定的bean,上面的是通过web.xml配置的文件去扫描,而我这里使用的是注解方式,通过Appconfig来配置的:

@ComponentScan("com.dev201121.definitions")
public class AppConfig {


    @Bean
    public User1 user1(){
        return new User1();
    }

}

就是这个意思:
顶级容器:扫描的路径是:com.dev201121.definitions
第二级容器:扫描的路径:com.dev201121.service
controller容器扫描的路径:com.dev201121.web

所以这里我是把容器的每个层级需要存放的bean分类的,这样做的以后。我们来修改下web.xml的配置文件

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <listener>
    <listener-class>com.dev201121.listeners.MyLinstener</listener-class>
    <listener-class>com.dev201121.listeners.TestLinisten</listener-class>
  </listener>

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring.xml</param-value>
  </context-param>
  
  <servlet>
    <servlet-name>dispartServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:spring-servlet.xml</param-value>
    </init-param>
  </servlet>

  <servlet-mapping>
    <servlet-name>dispartServlet</servlet-name>
    <url-pattern>*.do</url-pattern>
  </servlet-mapping>
</web-app>

把ContextLoadListener监听器换成我的MyLinstener就可以了,我们启动来看下
在这里插入图片描述
启动自定义的容器中肯定有我们创建的两个User类bean对象的。

子容器创建过程

知道了父容器的创建过程,那么子容器的创建过程也就非常简单了,和父容器的创建过程大同小异,我们上面说了其实子容器就是管理controller的,也就是web框架中的bean对象,也就是servlet上下文中的对象,而springmvc最重要的一个servlet就是DispatcherServlet,而这个servlet就是创建子容器的过程,我们大概看下这个servlet的类的继承关系:
在这里插入图片描述
其中右上边是servlet的,左上边到下面是spring实现的类,其中最重要的三个类是HttpServletBean->FrameworkServlet->DispatcherServlet,我们知道一个servlet调用的最核心有两个方法,一个是init方法,一个是service方法;service方法是只有在有请求过来的时候才会去调用的方法,而init方法如果不是懒加载的情况下启动servlet的过程中回去调用的,只要在servlet中设置了 load-on-startup 这个值只要是一个正数,也就是一个大于0的数字,那么servlet启动完成过后都会来调用,所以我们要看DispatcherServlet是如何创建的子容器,肯定是要来DispatcherServlet中来看init方法的,前面介绍过GerericServlet的接口,里面有两个init方法,一个是带参数的,一个是不带参数的,带参数的是GenericServlet实现的,就是拿到servlet配置的一些参数,而不带参数的init方法是提供给子类去实现的
在这里插入图片描述
所以在DispatcherServlet中,我们只需要去找init方法即可,而DispatcherServlet中的init方法是在父类HttpServletBean中实现的

HttpServletBean.init

public final void init() throws ServletException {

      // Set bean properties from init parameters.
      /**
       * 下面这行代码就是得到这个servlet的初始化参数,就是init下面的初始化参数然后封装成一个PropertyValues
       */
      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.
//    这里就是创建子容器的过程,在子类中的FrameworkServlet中实现的
      initServletBean();
   }

这里的init的方法其实要完成的事情就是创建出子容器,然后将ContextLoadListener创建的父容器绑定到子容器中,然后去初始化spring mvc的核心组件,所以这里的initServletBean方法就是去创建容器和初始化spring mvc核心组件的过程,它是在FrameworkServlet中完成的

FrameworkServlet.initServletBean

/**
 * Overridden method of {@link HttpServletBean}, invoked after any bean properties
 * have been set. Creates this servlet's WebApplicationContext.
 *
 * 这个方法是创建子容器的,被看这个方法写了那么多,只有一行代码有用
 * initWebApplicationContext,一看就知道是创建容器的,下面的
 * initFrameworkServlet方法是一个空的实现,简单来说就是提供给子类去实现的
 * 也就是说在当前容器加载完成过后,如果又需要加载点项目个性化的东西就可以重写这个父类中的方法
 */
@Override
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");
   }
}

initWebApplicationContext

protected WebApplicationContext initWebApplicationContext() {
   /**
    * 从servlet上下文中获取一个 WebApplicationContext.class.getName() + ".ROOT"为名字的容器,这个容器从名字就可以看出来是一个父容器
    * 就是在监听器ContextLoadListener的回调方法中创建的一个父容器
    */
   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) {
         //调用了子类DispatcherServlet去初始化mvc的组件
         onRefresh(wac);
      }
   }

   if (this.publishContext) {
      // Publish the context as a servlet context attribute.
      String attrName = getServletContextAttributeName();
      //最后将创建出来的容器对象放入到servlet上下文中
      /**
       * servlet上下文对象是在tomcat服务器中的context服务器中的
       * 在contxt容器的生命周期内永远存活的,所以这样就将spring容器的生命周期交给了
       * servlet上下文文容器中,而spring上下文也可以访问servlet上下文,servlet上下文也可以访问spring
       * 上下文
       */
      getServletContext().setAttribute(attrName, wac);
   }

   return wac;
}

createWebApplicationContext

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
   //得到当前(子容器)容器的class对象,这class对象肯定是ConfigurableWebApplicationContext的子类,这个class
   //也是XmlWebApplicationContext
   Class<?> contextClass = getContextClass();
   if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
      throw new ApplicationContextException(
            "Fatal initialization error in servlet with name '" + getServletName() +
            "': custom WebApplicationContext class [" + contextClass.getName() +
            "] is not of type ConfigurableWebApplicationContext");
   }
   //得到一个可支配的web上下文容器对象
   ConfigurableWebApplicationContext wac =
         (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

   //设置环境变量对象
   wac.setEnvironment(getEnvironment());
   //将获取的父容器设置当前的容器的父容器,这样关系就绑定好了
   wac.setParent(parent);
   //将当前容器的spring配置文件路径获取到然后设置到当前容器的对象中,在容器refresh中回去AbstractRefreshableApplicationContext中加载xml中bean
   String configLocation = getContextConfigLocation();
   if (configLocation != null) {
      wac.setConfigLocation(configLocation);
   }
   //和父容器创建的过程一样,就是配置和刷新web上下文工厂
   configureAndRefreshWebApplicationContext(wac);

   return wac;
}

configureAndRefreshWebApplicationContext
1.设置容器的ID;
2.添加servlet上下文到spring的上下文;
3.设置servlet的配置到spring上下文中;
4.添加命名空间;
5.添加一个监听器
6.提供了一个扩展
7.初始化监听器
8.启动容器

onRefresh

在创建子容器完成后,还要去初始化mvc的组件,就在onRefresh方法中去初始化的
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值