Spring深入挖掘:Spring在Web容器中是如何启动的 - Go语言中文社区

Spring深入挖掘:Spring在Web容器中是如何启动的


引言   

 记得刚刚接触Spring时,只是知道在web.xml配一个ContextLoaderListener,再通过contextConfigLocation配置一个配置文件就可以了。具体容器启动时到底做了些什么?Spring窗口是如何启动的?一直觉得是很底层很高深的东西。在那时,容器启动时出了问题往往是一头雾水,更不用说通过扩展插手Spring的启动过程了。

典型的Spring Web应用web.xml配置:

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath:cps/tpm/config/spring/applicationContext.xml</param-value>
 </context-param>
 
 <listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
 
 <servlet>
  <servlet-name>spring</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  
  <init-param>
   <param-name>contextConfigLocation</param-name>
   <param-value>classpath:cps/tpm/config/spring/webContext.xml</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
 </servlet>
 <servlet-mapping>
  <servlet-name>spring</servlet-name>
  <url-pattern>*.htm</url-pattern>
 </servlet-mapping>
 
 
 
 
 

一、Spring在容器中是如何启动的

  Spring在容器中的启动分两个阶段,一个是根上下文的初始化和启动,另一个是Web应用上下文的初始化和启动,这两个上下文是存在层关系的,前者是后者的父容器。

1.1 根上下文的初始化和启动


  根上下文的初始化和启动主要依靠ContextLoaderListener类,它是一个监听器
  从继承体系上看,ContextLoaderListener继承了ContextLoader,实现了ServletContextListener。
ServletContextListener是Servlet API中的接口,它能监听ServletContext的生命周期,也就是web应用的生命周期。
ServletContextListener接口中定义了两个方法,分别用于监听web应用的创建和销毁:
contextInitialized(ServletContextEvent sce):当Servlet容器启动Web应用时调用该方法。在调用完该方法之后,对那些在Web应用启动时就需要被初始化的Servlet进行初始化。
contextDestroyed(ServletContextEvent sce):当Servlet容器终止Web应用时调用该方法。在调用该方法之前,容器会先销毁所有的Servlet和Filter过滤器。

下面我们来看看ContextLoaderListener在这两个阶段分别做了些什么:
容器启动阶段
a)  查找根上下文RootWebApplicationContext的父容器
    在ContextLoader的loadParentContext方法里完成对父窗口的查找,系统去web.xml的context-param查找param-name为locatorFactorySelector和parentContextKey的值,如果找到,则初始化并设置为根上下文的父上下文。部分源码如下:

  String locatorFactorySelector = servletContext.getInitParameter(LOCATOR_FACTORY_SELECTOR_PARAM);
  String parentContextKey = servletContext.getInitParameter(LOCATOR_FACTORY_KEY_PARAM);

  if (parentContextKey != null) {
   // locatorFactorySelector may be null, indicating the default "classpath*:beanRefContext.xml"
   BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance(locatorFactorySelector);
   Log logger = LogFactory.getLog(ContextLoader.class);
   if (logger.isDebugEnabled()) {

                               logger.debug("Getting parent context definition: using parent context key of '" +                                               parentContextKey + "' with BeanFactoryLocator");

   }     this.parentContextRef = locator.useBeanFactory(parentContextKey);
   parentContext = (ApplicationContext) this.parentContextRef.getFactory();
  }

b)  创建WebApplicationContext容器并启动
    系统先通过determineContextClass()方法查找是否自定义了容器实现类型,如果没有,则使用默认的XmlWebApplicationContext。然后再实例化一个ConfigurableWebApplicationContext的实例,设置相关属性,如父上下文、ServletContext、配置文件等。最后 在ConfigurableWebApplicationContext创建后,调用其refresh()启动了Spring容器

  Class<?> contextClass = determineContextClass(sc);
  ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
  
  wac.setId(...);

  wac.setParent(parent);
  wac.setServletContext(sc);
  wac.setConfigLocation(sc.getInitParameter(CONFIG_LOCATION_PARAM));
  customizeContext(sc, wac);
  wac.refresh();

这里为什么要把WebApplicationContext的容器实现类型要强制转换成ConfigurableWebApplicationContext呢?因为在此方法里容器创建完毕后要启动整个容器,而启动容器的能力是ConfigurableWebApplicationContext所具有的(通过调用其refesh()方法)。有人或许会问,强制转换不会产生转换错误吗?我们先来看看它们关系简图:


由于我们默认的容器实现是XmlWebApplicationContext,由图可知, 所以它一定具有ConfigurableApplitionContext的所有能力,所以不会出现转换类型的问题。
窗口启动后如下:
Root WebApplicationContext: startup date [Wed Aug 31 14:05:35 CST 2011]; root of context hierarchy

c)  绑定WebApplicationContext容器到ServletContext
    绑定之后,我们可以在任何时间通过WebApplicationContextUtils工具类来获取WebApplicationContext
 示例代码:
  ServletContext sc  = null;
  WebApplicationContextUtils.getWebApplicationContext(sc);

容器销毁阶段
contextDestroyed
a) 调用ConfigurableWebApplicationContext的close方法关闭容器
b) 销毁ClassLoader
c) 取消ebApplicationContext到ServletContext的绑定
d) 遍历绑定到ServletContext上的对象,如果是DisposableBean类型的,调用destroy()方法将其销毁

1.2 Web应用上下文的初始化和启动

   Web应用上下文的初始化和启动基本上由HttpServletBean,FrameworkServlet和DispatcherServlet来完成,三个类各司其职。HttpServletBean负责初始化配置参数、注册自定义Editor并注册到BeanWrapper;FrameworkServlet负责初始化并启动Web应用上下文;DispatcherServlet负责初始化Web组件,如:ThemeResolver、HandlerMapping、ViewResolver等。这三个类也给我们留下的丰富的扩展点,三类的关系及主要方法如下简图:


 
a) 初始化配置参数、注册自定义Editor并注册到BeanWrapper。这一步在HttpServletBean的init()方法内完成
   PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
   BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
   ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
   bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader));
   initBeanWrapper(bw); //扩展点
   bw.setPropertyValues(pvs, true);

b) 初始化并启动Web应用上下文。在这一步骤里会ServletContext里获取根上下文,即通过ContextLoaderListener创建的上下文,并设置为自己的父上下文。
   WebApplicationContext parent =
     WebApplicationContextUtils.getWebApplicationContext(getServletContext());//获取根上下文
   wac = createWebApplicationContext(parent);//创建Web应用上下文

还有装配ServletContext、ServletConfig、Namespace、ConfigLocation等。其中初始化初始化Web应用上下文的过程与初始化根上下文的过程基本一致。
  ConfigurableWebApplicationContext wac =
    (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

  ServletContext sc = getServletContext();
   wac.setId(...);

  wac.setParent(parent);//设置父上下文
  wac.setServletContext(getServletContext());
  wac.setServletConfig(getServletConfig());
  wac.setNamespace(getNamespace());
  wac.setConfigLocation(getContextConfigLocation());
  wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

  postProcessWebApplicationContext(wac);//扩展点,容器启动前的后处理
  wac.refresh();//启动容器

c) 还会通过回调抽象的onRefresh方法来初始化Web组件。这一步委托给了DispatchServlet的onRefresh去完成。
在DispatchServlet内部,又会将初始化Web组件的任务委托给initStrategies()方法去完成

/**
  * Initialize the strategy objects that this servlet uses.
  * <p>May be overridden in subclasses in order to initialize further strategy objects.
  */
 protected void initStrategies(ApplicationContext context) {
  initMultipartResolver(context);
  initLocaleResolver(context);
  initThemeResolver(context);
  initHandlerMappings(context);
  initHandlerAdapters(context);
  initHandlerExceptionResolvers(context);
  initRequestToViewNameTranslator(context);
  initViewResolvers(context);
 } 

   org.springframework.web.context.WebApplicationContext.ROOT

Web应用上下文启动后如下:
WebApplicationContext for namespace 'spring-servlet': startup date [Wed Aug 31 14:14:52 CST 2011]; parent: Root WebApplicationContext

二、Spring留给我们的扩展点:

2.1 自定义根上下文的父上下文

  如果在context-param设置了locatorFactorySelector和parentContextKey,则容器会根据设置的locatorFactorySelector从ContextSingletonBeanFactoryLocator中获取BeanFactoryLocator,继而获取parentContext。locatorFactorySelector默认从classpath的中beanRefContext.xml获取

String locatorFactorySelector = servletContext.getInitParameter(LOCATOR_FACTORY_SELECTOR_PARAM);
String parentContextKey = servletContext.getInitParameter(LOCATOR_FACTORY_KEY_PARAM);
if (parentContextKey != null) {
 // locatorFactorySelector may be null, indicating the default "classpath*:beanRefContext.xml"
 BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance(locatorFactorySelector);
 
 this.parentContextRef = locator.useBeanFactory(parentContextKey);
 parentContext = (ApplicationContext) this.parentContextRef.getFactory();
}

2.2 自定义根上下文容器实现。
  Spring在Web容器中启动时,默认使用XmlWebApplicationContext。在spring-web-3.0.2.RELEASE.jar里的ContextLoader.properties里有如下定义:

# Default WebApplicationContext implementation class for ContextLoader.
# Used as fallback when no explicit context implementation has been specified as context-param.
# Not meant to be customized by application developers.
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

这是为ContextLoader提供的默认的WebApplicationContext的实现。当没有在web.xml中的context-param中显示定义时使用。
如果我们想自定义,可以在web.xml通过contextClass指定,容器会先使用我们自定义的Context。

String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
if (contextClassName != null) {
    return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
 
}
else {
    contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
                 }

使用方法如下:
 <context-param>
  <param-name>contextClass</param-name>
  <param-value>com.bill99.cps.MyWebApplicationContext</param-value>
 </context-param>

2.3 插手根上下文的启动
 
 可以在配置文件(config locations)设置之后,容器(ConfigurableWebApplicationContext)启动之前对容器做一些个性化动作。先来看看ContextLoaderk中createWebApplicationContext()方法的实现
  wac.setParent(parent);//设置父下文件
  wac.setServletContext(sc);//设置ServletContext
  wac.setConfigLocation(sc.getInitParameter(CONFIG_LOCATION_PARAM));//设置配置文件
  customizeContext(sc, wac);//扩展接口
  wac.refresh();//启动容器

 Spring留给我们的扩展接口如下:

/**
  * Customize the   * ContextLoader after config locations have been supplied to the context
  * but before the context is <em>refreshed</em>.
  * <p>The default implementation is empty but can be overridden in subclasses
  * to customize the application context.
  * @param servletContext the current servlet context
  * @param applicationContext the newly created application context
  * @see #createWebApplicationContext(ServletContext, ApplicationContext)
  */
  protected void customizeContext(
   ServletContext servletContext, ConfigurableWebApplicationContext applicationContext) #
  # 
 

可以看到ServletContext和WebApplicationContext已经都给我们了。我们可以在容器启动之前根据自己的业务需求做一些定制化的操作。
举一个应用场景:假设我们应用中的Quartz服务在设计的时候是启动的时候可以根据传入的参数来决定是否启用的,则可以利用这个扩展接口,我们自己写一个类来继承ContextLoaderListener,然后在web.xml中用我们自己的Listener来替换Spring的ContextLoaderListener。
扩展示例:

public class ContextLoaderListener extends org.springframework.web.context.ContextLoaderListener {
 
 public static final String QUARTZ_SKIP = "quartz.skip";
 public static final String QUARTZ_XML_CONFIG = "classpath:cps/console/config/spring/quartzContext.xml";
 
 @Override
 protected void customizeContext(ServletContext servletContext, ConfigurableWebApplicationContext applicationContext) {
 
  
  if(StringUtils.equalsIgnoreCase(System.getProperty(QUARTZ_SKIP), "true")){
   
   String[] configLocations = applicationContext.getConfigLocations();
   configLocations = (String[]) ArrayUtils.removeElement(configLocations, QUARTZ_XML_CONFIG);
   
   applicationContext.setConfigLocations(configLocations);
  }
  
  super.customizeContext(servletContext, applicationContext);
 }
 

这样在启动的时候,通过-D传参可以决定 Quartz是否启动:-Dquartz.skip=true

2.4 自定义ContextLoader
在早期的Spring版本里,ContextLoaderListener可以接受外部传入的ContextLoader,通过保护的方法createContextLoader()对外提供了扩展点:

protected ContextLoader createContextLoader() {
  return null;
 }
 

Spring 3.0后,ContextLoaderListener自身继承了ContextLoader,已经不推荐使用createContextLoader()方法。但此方法依然可用,Spring先判断是否通过createContextLoader()创建了ClassLoader,如果没创建,则使用this.

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
 
 
       private ContextLoader contextLoader;
 
 
 
 
       /**
        * Initialize the root web application context.
        */
       public void contextInitialized(ServletContextEvent event) {
               this.contextLoader = createContextLoader();
               if (this.contextLoader == null) {
                       this.contextLoader = this;
               }
               this.contextLoader.initWebApplicationContext(event.getServletContext());
       }
&nbsp; 
 
 
 

2.5  自定义BeanWrapper
在BeanWrapper实例化之后,Web应用上下文启动之前调用此方法

       /**
        * Initialize the BeanWrapper for this HttpServletBean,
        * possibly with custom editors.
        * <p>This default implementation is empty.
        * @param bw the BeanWrapper to initialize
        * @throws BeansException if thrown by BeanWrapper methods
        * @see org.springframework.beans.BeanWrapper#registerCustomEditor
        */
       protected void initBeanWrapper(BeanWrapper bw) throws BeansException {
       }
&nbsp; 
 
 
 
 

2.6 插手We上下文的启动 在Web应用上下文初始化完成启动之前会调用此方法。

       /**
        * Post-process the given WebApplicationContext before it is refreshed
        * and activated as context for this servlet.
        * <p>The default implementation is empty. <code>refresh()</code> will
        * be called automatically after this method returns.
        * @param wac the configured WebApplicationContext (not refreshed yet)
        * @see #createWebApplicationContext
        * @see ConfigurableWebApplicationContext#refresh()
        */
       protected void postProcessWebApplicationContext(ConfigurableWebApplicationContext wac) {
       }
&nbsp; 
 
 
 
 

         2.7
在Web应用上下文初始化并启动完成后调用此方法

/**
 * This method will be invoked after any bean properties have been set and
 * the WebApplicationContext has been loaded. The default implementation is empty;
 * subclasses may override this method to perform any initialization they require.
 * @throws ServletException in case of an initialization exception
 */
protected void initFrameworkServlet() throws ServletException {
}

  三、存在的疑问

1,在早期的Spring版本里,ContextLoaderListener可以接受外部传入的ContextLoader,通过保护的方法createContextLoader()对外提供了扩展点。Spring 3.0后,ContextLoaderListener自身继承了ContextLoader,已经不推荐使用createContextLoader()方法。做法的意图是什么?
2,ConfigurableApplicationContext作为ApplicationContext的SPI,具有了启动容器的功能,AbstractApplicationContext是其
的一个抽象类,此抽象类有两个实现,分别是AbstractRefreshableApplicationContext和GenericApplicationContext.自成两派,这两派系又各有众多弟子。 当时划分两派系的设计理念是什么?每个派系又分别侧重什么用途?
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/wo240/article/details/46972615
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-01 22:42:55
  • 阅读 ( 1066 )
  • 分类:Go深入理解

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢