从Servlet和Spring-web整合理解父子容器

1,046 阅读14分钟

从Servlet和Spring-web整合理解父子容器

这篇文章主要分为两个部分,

  1. spring和servlet的融合。怎么Servlet容器启动起来,我就可以用Spring了。
  2. 从ServletContext和RootWebContext的关系理解Spring中的父子容器。

现在Springboot作为Java开发的主力军,开箱即用的特性,并且大多数的人在pom文件中写的都是直接写spring-boot-starter-web,然后在主启动类上在写@SpringbootApplication,在写一个Controller,一个简单的springboot 的web程序就开始了。这样好是好,但是就是因为太好了,导致底层实现不太清楚。Spring-web是怎么和Servlet容器结合起来的呢?

这让我想到了一开始学习Spring-web的时候,web.xml配置文件。得从它开始。在说一句,我觉得框架的发展不是一步到位的,也是需要慢慢进化的,这点在Spring身上体现的淋漓尽致,一开始是xml配置,之后是注解,现在是Springboot。

一个经典的基于web.xml的配置方式。

 <!-- 这配置的是Root Application Context--> 
   <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/springConfig.xml</param-value>
    </context-param>
  

    <!-- 配置DispatchServlet -->
    <servlet>
        <servlet-name>springmvcservlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

        <init-param>
          <!-- 给Servlet配置applicationContext的xml配置文件路径 -->
             <param-name>contextConfigLocation</param-name>
             <param-value>/WEB-INF/springmvc.xml</param-value>
        </init-param>
        <!--其他参数-->
        <init-param>
             <param-name>appName</param-name>
             <param-value>authplatform</param-value>
        </init-param>
        <!-- 下面值小一点比较合适,会优先加载 -->
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!--- 给servlet配置需要映射的请求路径 -->
    <servlet-mapping>
        <servlet-name>springmvcservlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

  <!-- 配置请求过滤器,编码格式设为UTF-8,避免中文乱码 -->
    <filter>
        <filter-name>charsetfilter</filter-name>
        <filter-class>
            org.springframework.web.filter.CharacterEncodingFilter
        </filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>charsetfilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 该类作为spring的listener使用,它会在创建时自动查找web.xml配置的applicationContext.xml文件 -->
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>

那么基于这个例子,开始今天的分析

一. Servlet和Spring-web整合分析

首先要说明,在Servlet中存在父子容器的概念,抛开Spring不谈,在一个经典的Web程序里是可以有多个Servlet的,这点是毋庸置疑的,否则上面xml里 <servlet-mapping>就不会存在了,

image-20211107221919569.png 所以,应该是这个样子,每一个Servlet都可以处理一类的请求,这都是随便指定的。那么Spring-web干的事情就是,利用一个Servlet拦截指定的请求,在处理请求的时候,添加了一套Spring自己的东西(之后博客说),比如拦截器,异常处理,Multipart,Locale,Theme,等等。

对比上图,一个web程序对应的就是一个Context(图中蓝色),在这个Context里面可以有多个servlet(图中白色)。Context和Servlet在Spring-web里面各有一个ApplicationContext,Context对应的就是Root(父),Servlet对应的就是子ApplicationContext。

Spring-web里面也是可以添加多个dispatchServlet的。

那么现在回想一下在Servlet容器里面的几个特殊的Listener,还有servlet是怎么写的?

Servlet容器里几个特殊的Listener,和Servlet接口

几个特殊的Listener

  1. ervletContextAttributeListener :Context里面属性变动

  2. ServletRequestListener ; request 初始化和销毁的回调

  3. ServletRequestAttributeListener: request 属性变动

  4. HttpSessionAttributeListener:session 属性变动

  5. HttpSessionIdListener: session id变动

6, HttpSessionListener:session 创建和销毁

  1. ServletContextListener: Context初始化和销毁

Servlet创建和Servlet的生命周期

servlet怎么写?实现Servlet接口,一般来说都是直接继承于HttpServlet.这就说到Servlet的生命周期了。

servlet生命周期

  1. init 方法,在第一个请求来的时候初始化这个Servlet。之后来的请求就不会调用这个方法了
  2. service 方法,处理真正的请求的地方。
  3. destroy方法,servlet关闭的时候,也就是servlet容器关闭的时候

分析完这些,下面就可以看两者是怎么结合的。 从上面知道,context有一个rootApplicationContext,Servlet也有一个子的ApplicationContext。加上web.xml配置文件上的配置。两者的结合肯定是在servlet的Context初始化的时候加载根配置的xml文件,创建ApplicationContext,在HttpServlet#init(Servlet初始化)创建Servlet的ApplicationContext。

一切的开始(源码分析)

从web.xml可以看到。必须要配置一个org.springframework.web.context.ContextLoaderListener,用于ServletContext初始化的时候的监听。同时,还得在 <context-param>里面指定一个<param-name>>为contextConfigLocation的值,value是该ServletContext对应的ApplicationContext的配置文件的路径。

此外,还得在配置<servlet>的时候在<param-name>里面配置key为contextConfigLocation的值,value是该Servlet对应ApplicationContext的配置文件的路径。

下面就从源码来分析分析

ContextLoaderListener

image-20211107232659135.png

ContextLoaderListener,在ServletContext,初始化和销毁时候的监听。创建和销毁rootApplicationContext。将真正的实现委托给ContextLoader来实现。

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

	public ContextLoaderListener() {
	}

	public ContextLoaderListener(WebApplicationContext context) {
		super(context);
	}
	/**
   将真正的实现委托给ContextLoader#initWebApplicationContext
	 */
	@Override
	public void contextInitialized(ServletContextEvent event) {
		initWebApplicationContext(event.getServletContext());
	}
	/**
	 将真正的实现委托给ContextLoader#closeWebApplicationContext
	 */
	@Override
	public void contextDestroyed(ServletContextEvent event) {
		closeWebApplicationContext(event.getServletContext());
		ContextCleanupListener.cleanupAttributes(event.getServletContext());
	}
}
ContextLoader#initWebApplicationContext(初始化root web application)
// servletContext这是javax.servlet里面的
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
   // WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 是一个key,value存放的初始化好的root web application
   if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
     // 说明在web.xml中只能配置一个ContextLoader的listener
      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 {
      
      if (this.context == null) {
         // 创建root web application
         this.context = createWebApplicationContext(servletContext);
      }
     // 这就说明如果要启动配置root web application,就必须是ConfigurableWebApplicationContext的子类
      if (this.context instanceof ConfigurableWebApplicationContext) {
         ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
          
        // 如果这个还没有初始化过
         if (!cwac.isActive()) {
            //如果没有设置父context
            if (cwac.getParent() == null) {
                // 加载父context,这个方法默认的实现是空方法,也就是说,可以重写这个方法,为root web application 设置父容器
               ApplicationContext parent = loadParentContext(servletContext);
               cwac.setParent(parent);
            }
           // 指定配置文件地址,并且调用context的refresh方法,启动容器。
            configureAndRefreshWebApplicationContext(cwac, servletContext);
         }
      }
      // 设置context到servletContext中,
     // 之后就可以通过属性名获得root web application
      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;
   }
}
ContextLoader#createWebApplicationContext(通过contextClass判断ApplicationContext类型,并且创建WebApplicationContext)

创建的webApplication必须是ConfigurableWebApplicationContext的子类,并且调用determineContextClass方法,在determineContextClass里面获取<context-param>里面param-name为contextClass。值是具体的WebApplicationContext实现类的全限定类名。

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
		Class<?> contextClass = determineContextClass(sc);
		if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
			throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
					"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
		}
		return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
	}
  1. ContextLoader#determineContextClass(通过contextClass判断WebApplicationContext具体类型)

先是加载<context-param>里面param-name为contextClass的值,如果没有,就去ContextLoader.properties里面加载默认的实现。总之,这个方法是为了判断WebApplicationContext的具体类型。

问题

  1. 默认的文件(ContextLoader.properties)长什么样?

image-20211108225231957.png

protected Class<?> determineContextClass(ServletContext servletContext) {
  // 	public static final String CONTEXT_CLASS_PARAM = "contextClass";
   String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
   if (contextClassName != null) {
      try {
         return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
      }
      catch (ClassNotFoundException ex) {
         throw new ApplicationContextException(
               "Failed to load custom context class [" + contextClassName + "]", ex);
      }
   }
   else {
     // 如果没有就采用默认策略,默认加载ContextLoader.properties文件里面写的内容(默认实现是XmlWebApplicationContext)
      contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
      try {
         return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
      }
      catch (ClassNotFoundException ex) {
         throw new ApplicationContextException(
               "Failed to load default context class [" + contextClassName + "]", ex);
      }
   }
}
ContextLoader#configureAndRefreshWebApplicationContext
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
   
   if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
     // 从配置文件中获取contextId参数,如果没有,就采用默认值。
      String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
      if (idParam != null) {
         wac.setId(idParam);
      }
      else {
         wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
               ObjectUtils.getDisplayString(sc.getContextPath()));
      }
   }
   // 设置Servletcontext给webApplicationContext
   wac.setServletContext(sc);
  
   // 拿到 contextConfigLocation参数,这个就是web.xml中设置的参数,
  // 
   String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
   if (configLocationParam != null) {
     // 设置配置文件路径,
      wac.setConfigLocation(configLocationParam);
   }

 
   // 过早初始化,确保安全。初始化环境。
   ConfigurableEnvironment env = wac.getEnvironment();
   if (env instanceof ConfigurableWebEnvironment) {
      ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
   }
      //调用ApplicationContextInitiaial
   customizeContext(sc, wac);
   // 调用refresh。启动容器。
   wac.refresh();
}
ContextLoader#customizeContext(自定义WebApplicationContext)

自定义WebApplicationContext,通过ApplicationContextInitializer#initialize方法。

protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
   // 拿到ApplicationContextInitializer
   List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
         determineContextInitializerClasses(sc);

      // 循环遍历,验证,判断
   for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
      Class<?> initializerContextClass =
            GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
      if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
         throw new ApplicationContextException(String.format(
               "Could not apply context initializer [%s] since its generic parameter [%s] " +
               "is not assignable from the type of application context used by this " +
               "context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
               wac.getClass().getName()));
      }
      // 实例化
      this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
   }
   // 居然也是可以使用order来做排序的。
   AnnotationAwareOrderComparator.sort(this.contextInitializers);
   // 循环调用
   for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
      initializer.initialize(wac);
   }
}
  1. ContextLoader#determineContextInitializerClasses

    加载web.xml中配置的ApplicationContextInitializer,(分为全局和局部,全局是globalInitializerClasses,局部是contextInitializerClasses)。

protected List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>>
      determineContextInitializerClasses(ServletContext servletContext) {

   List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> classes =
         new ArrayList<>();

   String globalClassNames = servletContext.getInitParameter(GLOBAL_INITIALIZER_CLASSES_PARAM);
   if (globalClassNames != null) {
      for (String className : StringUtils.tokenizeToStringArray(globalClassNames, INIT_PARAM_DELIMITERS)) {
         classes.add(loadInitializerClass(className));
      }
   }

   String localClassNames = servletContext.getInitParameter(CONTEXT_INITIALIZER_CLASSES_PARAM);
   if (localClassNames != null) {
      for (String className : StringUtils.tokenizeToStringArray(localClassNames, INIT_PARAM_DELIMITERS)) {
         classes.add(loadInitializerClass(className));
      }
   }

   return classes;
}

在ContextLoaderListener主要干了下面的几件事情:

  1. 从web.xml中获取需要创建的WebApplicationContext的类型(默认是XmlWebApplicationContext)
  2. 从web.xml中获取contextId,设置给WebApplicationContext。
  3. 从web.xml中获取配置文件的路径,设置给WebApplicationContext。
  4. 初始化环境。
  5. 调用ApplicationContextInitializer来自定义。
  6. 调用refresh方法,启动容器。
  7. 并且会将已经创建好的WebApplicationContext设置为ServletContext的一个属性。

到此,ServletContext关联的rootwebApplication已经启动起来了。并且已经放在ServletContext里面了。到这里rootWebApplicationContext启动已经搞定了,在看看关闭时候的操作(上面忘了写了,写在这里把)

可以看到,这里调用了两个方法,在这个方法里面会关闭调用rootWebApplicationContext。并且移除之前缓存在Servlet中的context的引用的属性,此外,他还会遍历servletContext里面所有的属性,判断这个对象是否是DisposableBean,如果是调用他的destroy方法。

	@Override
	public void contextDestroyed(ServletContextEvent event) {
		closeWebApplicationContext(event.getServletContext());
		ContextCleanupListener.cleanupAttributes(event.getServletContext());
	}

// 可以看到,这里调用了两个方法,在这个方法里面会关闭调用rootWebApplicationContext。并且移除之前缓存在Servlet的属性
public void closeWebApplicationContext(ServletContext servletContext) {
		servletContext.log("Closing Spring root WebApplicationContext");
		try {
			if (this.context instanceof ConfigurableWebApplicationContext) {
				((ConfigurableWebApplicationContext) this.context).close();
			}
		}
		finally {
			ClassLoader ccl = Thread.currentThread().getContextClassLoader();
			if (ccl == ContextLoader.class.getClassLoader()) {
				currentContext = null;
			}
			else if (ccl != null) {
				currentContextPerThread.remove(ccl);
			}
			servletContext.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
		}
	}

static void cleanupAttributes(ServletContext servletContext) {
		Enumeration<String> attrNames = servletContext.getAttributeNames();
		while (attrNames.hasMoreElements()) {
			String attrName = attrNames.nextElement();
       // 是否是Spring的
			if (attrName.startsWith("org.springframework.")) {
				Object attrValue = servletContext.getAttribute(attrName);
        // 是否是DisposableBean
				if (attrValue instanceof DisposableBean) {
					try {
						((DisposableBean) attrValue).destroy();
					}
					catch (Throwable ex) {
						if (logger.isWarnEnabled()) {
							logger.warn("Invocation of destroy method failed on ServletContext " +
									"attribute with name '" + attrName + "'", ex);
						}
					}
				}
			}
		}
	}

DispatcherServlet

image-20211108232306228.png 上面已经介绍了,它继承于HttpServlet,再想想Servlet的生命周期,所以,先看init方法,再看destroy方法,在此之前,先看看DispatchServlet实现的几个特殊的父类。

  1. HttpServletBean

这个bean提供了将servlet的配置变为属性的功能。

  1. FrameworkServlet

    一个基础的实现,在HttpServletBean的基础上,让Servlet和ApplicationContext结合,主要有两个功能

    1. 为每个servlet管理一个WebApplicationContext实例。
    2. 在处理请求的时候发布事件,无论处理是否成功。

好了,说完这些,就开始看init方法和destroy方法,至于service方法后面专门分析一波。

HttpServletBean#init

将servletConfig配置的参数变为这个bean的属性。

问题

  1. 具体是怎么做的?BeanWrapper的功能是什么?怎么做映射的?

    是通过BeanWrapper来做的,先拿到ServletConfig所有的属性,封装为PropertyValues,通过bw做映射。后面专门说说这个

@Override
public final void init() throws ServletException {

   // Set bean properties from init parameters.
   // 将init param设置为bean的属性
   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;
      }
   }

 // 初始化Servlet
   initServletBean();
}
FrameworkServlet#initServletBean

给Servlet创建WebApplicationContext,并且初始化Servlet。

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();
        // 这个地方和是WebApplicationInitializer大体是一样的,但是有各别是不一样的, 具体在方法里面看看
		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");
		}
	}
FrameworkServlet#initWebApplicationContext

初始化WebApplicationContext,并且会将之前在ContextLoaderListener初始化好的rootWebApplicationContext,作为当前ApplicationContext的父容器,并且对创建好的容器设置ServletContext,配置,namespace,还有一个特殊的Listener,给子类留了一个postProcessWebApplicationContext,初始化环境。之后调用onRefresh方法回调。

如果webApplicationContext没有初始化,就会先从ServletContext中获取有没有对应的ApplicationContext,如果没有,才会去创建它。

创建它的时候会调用两个方法

  1. getContextClass (拿到这个context的类)
  2. getContextConfigLocation(拿到配置文件的地址)

那么有问题来了。这俩的属性的值什么?在哪设置的?

首先说,FrameworkServlet是继承于HttpServletBean,在HttpServletBean#init方法里面会将servletConfig(web.xml文件)里面的servlet的属性映射起来。这样就能将属性和配置文件对应起来了。再回头看看属性的字段名字(contextClass,contextConfigLocation)和web.xml文件中的属性名对应起来了。

问题

  1. 这个Listener是什么?有什么作用

    在FrameworkServlet#configureAndRefreshWebApplicationContext方法里面。

    		wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
    

    可以看到,在SourceFilteringListener里面包含了一个ContextRefreshListener,

    ContextRefreshListener利用了代理模式,代理了ContextRefreshListener。具体看看ContextRefreshListener里面onApplicationEvent干了什么事情

    private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {
    
    		@Override
    		public void onApplicationEvent(ContextRefreshedEvent event) {
    			FrameworkServlet.this.onApplicationEvent(event);
    		}
    	}
    
    public void onApplicationEvent(ContextRefreshedEvent event) {
    		this.refreshEventReceived = true;
    		synchronized (this.onRefreshMonitor) {
    			onRefresh(event.getApplicationContext());
    		}
    	}
    

    从上面的代码可以看到,这里就是最终还是调用到onRefresh方法。

protected WebApplicationContext initWebApplicationContext() {

  // 从ServletContext中获取,这个获取是获取之前在ContextLoaderListener里面创建的webApplicationContext。
  // 从ServletContext属性里面获取。属性中获取。因为在创建DispatchServlet的时候就已经把ServletContext设置进来了。
  // 这里直接从属性中获取值就好了。
  WebApplicationContext rootContext =
         WebApplicationContextUtils.getWebApplicationContext(getServletContext());
   WebApplicationContext wac = null;
   
  
  // webApplicationContext就这个servlet初始化的属性。
   if (this.webApplicationContext != null) {
      //如果之前已经有了,就直接将root web ApplicationContext设置为父容器
     // 并且调用configureAndRefreshWebApplicationContext方法
      wac = this.webApplicationContext;
      if (wac instanceof ConfigurableWebApplicationContext) {
         ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
         if (!cwac.isActive()) {
           
            if (cwac.getParent() == null) {
               // 这里会将rootWebApplication设置为父类
               cwac.setParent(rootContext);
            }
          // 和rootwebApplicationContext基本一样的操作。
            configureAndRefreshWebApplicationContext(cwac);
         }
      }
   }
   if (wac == null) {
      // 从servletContext中通过属性名字来获取webApplicationContext,
     // 子类是可以重写这个方法,提供不同的WebApplicationContext检索的策略。
      wac = findWebApplicationContext();
   }
   if (wac == null) {
     // 如果依然还是没有,就会通过getContextClass方法和getContextConfigLocation方法获取对应的参数,和root web ApplicationContext一样
     // 参数的赋值是通过BeanWrapper来做的,
     // 并且将rootWebApplicationContext作为他的父类。
     wac = createWebApplicationContext(rootContext);
   }
   // refreshEventReceived判断onRefresh是否调用过。
  // 一开始是false,在ContextRefreshListener会修改。
   if (!this.refreshEventReceived) {
   
      synchronized (this.onRefreshMonitor) {
         // 留给子类的拓展方法,在具体看dispatchServlet里面
         onRefresh(wac);
      }
   }
      
  // publishContext表示,是否要发布Context,也就是将这个Servlet的Context添加到属性里面去。
   if (this.publishContext) {
    // 属性名字是 SERVLET_CONTEXT_PREFIX + servlet name
      String attrName = getServletContextAttributeName();
      // 将这个servlet的webApplicatin 放在servletContext里面
      getServletContext().setAttribute(attrName, wac);
   }
 
   return wac;
}
DispatcherServlet#onRefresh

ApplicationContext完全初始化好之后的回调Servlet方法,在这个方法里面主要是初始化了一些在处理Request的时候用到的一些bean,因为这个时候ApplicationContext已经初始化好了,这里就是简单的从ApplicationContext获取值,设置就好。

  // 创建好每个servlet相关的webApplicationContext之后的回调方法
 @Override
	protected void onRefresh(ApplicationContext context) {
		initStrategies(context);
	}

	// 初始化这个servlet使用到的东西。
	protected void initStrategies(ApplicationContext context) {
		initMultipartResolver(context);
		initLocaleResolver(context);
		initThemeResolver(context);
		initHandlerMappings(context);
		initHandlerAdapters(context);
		initHandlerExceptionResolvers(context);
		initRequestToViewNameTranslator(context);
		initViewResolvers(context);
		initFlashMapManager(context);
	}

这里初始化的方法都大同小异,这里用initMultipartResolver方法来举例子。

initMultipartResolver

初始化MultipartResolver。方法很简单,就是从ApplicationContext中获取bean,因为这个时候spring的容器已经启动了。所以,就按照最经典的bean的获取方式来,直接通过bean的名字和类型类获取就好。

private void initMultipartResolver(ApplicationContext context) {
		try {
      // 从ApplicationContext中获取MultipartResolver的bean,bean的名字叫做MULTIPART_RESOLVER_BEAN_NAME。
			this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
			if (logger.isTraceEnabled()) {
				logger.trace("Detected " + this.multipartResolver);
			}
			else if (logger.isDebugEnabled()) {
				logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());
			}
		}
		catch (NoSuchBeanDefinitionException ex) {
			// Default is no multipart resolver.
			this.multipartResolver = null;
			if (logger.isTraceEnabled()) {
				logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared");
			}
		}
	}

到这里从源码分析了Servlet和Spring-web的整合操作。下面总结一下

总结:

ContextLoaderListener,是Servlet容器的监听,在Servlet容器初始化的时候,通过监听,就可以为Servlet容器创建一个 ApplicationContext,我叫它为RootApplicationContext,在Servlet容器销毁的时候,在回调里面关闭ApplicationContext,并且调用实现disposeBean接口的bean。

DispatchServlet,是Servlet监听,想想Servlet的生命周期,在Servlet初始化的时候,会为这个Servlet创建一个ApplicationContext,并且在ApplicationContext初始化好之后,会为DispatchServlet初始化一些处理request的时候需要的bean。

创建ApplicationContext的时候,配置文件和ApplicationContext的类型都是在web.xml中指定的,如果不指定ApplicationContext的类型,默认的是XmlWebApplicationContext。所以,web.xml文件中是配置参数名字必须是这个。

注意,ContextLoaderListener带参数的构造方法FrameworkServlet#initWebApplicationContext里面对webApplicationContext的非空判断是有别的用处的,在分析Springboot是怎么做的时候再说

二. Spring中Application的父子容器

结合上面的Servlet容器和Servlet的关系,理解Spring的父子容器就好理解了。

image-20211111000922010.png 关系如下:

  1. 父容器是不能访问子容器。子容器是可以访问父容器
  2. 父容器的环境(Environment)和子容器的环境(Environment)是一样的,因为他有一个合并的操作。

例子:

父容器是不能访问子容器。子容器是可以访问父容器

父容器配置类例子:

@Configuration(proxyBeanMethods = false)
public class ParentConfig {

	@Bean
	public B1 b1(){
		B1 parent = new B1("parent", 12);
		return  parent;
	}
  
  
static public class B1 {
	private String name;
	private Integer age;

	public B1(String name, Integer age) {
		this.name = name;
		this.age = age;
	}

	@Override
	public String toString() {
		return "B1{" +
				"name='" + name + '\'' +
				", age=" + age +
				'}';
	}
}

子容器配置类


@Configuration(proxyBeanMethods = false)
public class SimpleConfiguration {

	@Bean
	public Student student(Environment environment) {
		System.out.println(environment);
		Student student = new Student();
		student.setName("a");
		student.setAge(1);
		return student;
	}
  
  static public class Student {
	private String name;
	private Integer age;

	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String toString() {
		return "Student{" +
				"name='" + name + '\'' +
				", age=" + age +
				'}';
	}
}

}

主测试类

   AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		context.register(SimpleConfiguration.class);
		context.refresh();

		AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(ParentConfig.class);
		context.setParent(parent);


		System.out.println("================");
    // 从子类中获取父类中定义的bean。
		B1 bean = context.getBean(B1.class);
		System.out.println(bean);

		context.close();

答案

image-20211111001850639.png 是可以获取的到的,下面从源码来看看

直接看getBean,最后就会找到这样一段代码。DefaultListableBeanFactory#resolveBean

@Nullable
	private <T> T resolveBean(ResolvableType requiredType, @Nullable Object[] args, boolean nonUniqueAsNull) {
    // 先从自己找
    NamedBeanHolder<T> namedBean = resolveNamedBean(requiredType, args, nonUniqueAsNull);
		if (namedBean != null) {
			return namedBean.getBeanInstance();
		}
     // 从父类找
		BeanFactory parent = getParentBeanFactory();
		if (parent instanceof DefaultListableBeanFactory) {
			return ((DefaultListableBeanFactory) parent).resolveBean(requiredType, args, nonUniqueAsNull);
		}
		else if (parent != null) {
			ObjectProvider<T> parentProvider = parent.getBeanProvider(requiredType);
			if (args != null) {
				return parentProvider.getObject(args);
			}
			else {
				return (nonUniqueAsNull ? parentProvider.getIfUnique() : parentProvider.getIfAvailable());
			}
		}
		return null;
	}

父容器的环境(Environment)和子容器的环境(Environment)是一样的,因为他有一个合并的操作。

  1. 父容器Environment中添加属性,子容器能获取到。
try {
			AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
			context.register(SimpleConfiguration.class);
			context.refresh();

			AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(ParentConfig.class);
			ConfigurableEnvironment environment = parent.getEnvironment();
      
      // 父类中添加属性
			environment.getPropertySources()
					.addLast(new MockPropertySource().withProperty("a","b"));

			context.setParent(parent);

			System.out.println("================");

			B1 bean = context.getBean(B1.class);
			System.out.println(bean);
			System.out.println(context.getEnvironment().getProperty("a", String.class));

			context.close();
		}catch (Throwable e){
			e.printStackTrace();
		}

image-20211111002355542.png

  1. 父容器Environment中添加属性,子容器能获取到。
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
			context.register(SimpleConfiguration.class);


			context.refresh();

			AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(ParentConfig.class);
			ConfigurableEnvironment environment = parent.getEnvironment();

			environment.getPropertySources()
					.addLast(new MockPropertySource().withProperty("a","b"));

			context.setParent(parent);

			System.out.println("================");

			B1 bean = context.getBean(B1.class);
			System.out.println(bean);
			System.out.println(context.getEnvironment().getProperty("a", String.class));

			context.close();

image-20211111002617911.png

源码分析

context.setParent(parent);进去,能找到下面的代码,AbstractApplicationContext#setParent

	public void setParent(@Nullable ApplicationContext parent) {
		this.parent = parent;
		if (parent != null) {
			Environment parentEnvironment = parent.getEnvironment();
			if (parentEnvironment instanceof ConfigurableEnvironment) {
        // 合并操作
				getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
			}
		}
	}

这样设置是为了什么?

我觉得抽取底层,就可以将一些基本的功能,放在父容器中,每个子容器都是自己的东西,这也符合父子容器的概念。

关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢