首页 > 学院 > 开发设计 > 正文

Spring4.3.x 容器在web应用中的初始化过程

2019-11-10 19:42:50
字体:
来源:转载
供稿:网友

概述

SPRing在web应用中的默认容器类为xmlWebapplicationContext,这个容器类通过xml文件获取所有的配置信息。它的继承结构如下图,(点此查看大图) XmlWebApplicationContext继承结构

在web应用中,不管是ContextLoaderListener,还是DispatcherServlet初始化的时候,都是以XmlWebApplicationContext为默认容器。在下面的研究中,我将以ContextLoaderListener的初始化过程介绍spring容器在web应用的初始化。

ContextLoaderListener的初始化过程中最主要的任务时加载spring容器,并把此容器加入到ServletContext中作为整个web应用的跟容器。ContextLoaderListener加载spring容器大致分为两个阶段,第一个阶段是解析web.xml文件中的初始化参数以对spring容器做定制化操作,简单的说就是定制spring容器;第二阶段是spring容器的刷新过程。下面分别对这两个阶段进行探讨。

第一阶段 定制spring容器

ContextLoaderListener实现了ServletContextListener,因此web容器启动的时候就会执行它的contextInitialized方法,此方法的代码如下。

public void contextInitialized(ServletContextEvent event) { // 获取ContextLoader对象 this.contextLoader = createContextLoader(); if (this.contextLoader == null) { this.contextLoader = this; } this.contextLoader.initWebApplicationContext(event.getServletContext()); } @Override public void contextInitialized(ServletContextEvent event) { // 执行父类ContextLoader的initWebApplicationContext方法 initWebApplicationContext(event.getServletContext()); }

这段代码主要是调用父类ContextLoader的initWebApplicationContext(ServletContext servletContext)方法,下面是initWebApplicationContext方法在ContextLoader类中的代码。

public WebApplicationContext initWebApplicationContext(ServletContext 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!"); } Log logger = LogFactory.getLog(ContextLoader.class); servletContext.log("Initializing Spring root WebApplicationContext"); if (logger.isInfoEnabled()) { logger.info("Root WebApplicationContext: initialization started"); } long startTime = System.currentTimeMillis(); try { if (this.context == null) { // 创建容器,据contextClass初始化参数指定或者使用默认的XmlWebApplicationContext类 this.context = createWebApplicationContext(servletContext); } if (this.context instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; if (!cwac.isActive()) { // 如果容器没有被刷新,执行以下操作 // 设置父容器 if (cwac.getParent() == null) { // 加载父容器。 // 通过locatorFactorySelector上下文初始化参数指定父容器所在的配置文件路径 // 通过parentContextKey上下文初始化参数指定父容器的名称 ApplicationContext parent = loadParentContext(servletContext); cwac.setParent(parent); } // 配置并执行容器刷新操作 configureAndRefreshWebApplicationContext(cwac, servletContext); } } // 把spring容器加入到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.isDebugEnabled()) { logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]"); } if (logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms"); } return this.context; } catch (RuntimeException ex) { logger.error("Context initialization failed", ex); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); throw ex; } catch (Error err) { logger.error("Context initialization failed", err); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err); throw err; } }

这段代码分成4步,首先通过调用ContextLoader的createWebApplicationContext(ServletContext sc)方法来创建spring容器,然后设置spring容器的父容器,接着调用ContextLoader的configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc)方法来配置并刷新spring容器,最后把容器保存到servlet容器中。最后一步的代码已经在上面体现了,下面我们来解析前三步的代码。

1. 创建spring容器。

调用ContextLoader的createWebApplicationContext(ServletContext sc)方法,这个方法的代码如下。

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); }

上面主要是通过ContextLoader的determineContextClass(ServletContext servletContext)方法获取容器类对象,然后实例化容器。下面是determineContextClass方法的代码。

/** * 获取容器类对象 **/ 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 { 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); } } }

determineContextClass从ServletContext对象中获取contextClass初始化参数的值,如果这个参数有值,则使用这个参数指定的容器类,否则使用默认的容器类XmlWebApplicationContext。如果不使用spring的默认容器,可以在web.xml中通过配置contextClass指定其他容器类,比如。

<!-- 定义contextClass参数 --> <context-param> <param-name>contextClass</param-name> <param-value> com.chyohn.context.XmlWebApplicationContext </param-value> </context-param>

2. 指定父容器。

创建完spring容器后,initWebApplicationContext中会调用ContextLoader的 loadParentContext(ServletContext servletContext)方法来获取父容器,并把这个父容器与刚创建的容器关联上。loadParentContext方法的代码如下。

protected ApplicationContext loadParentContext(ServletContext servletContext) { ApplicationContext parentContext = null; // 声明:public static final String LOCATOR_FACTORY_SELECTOR_PARAM = "locatorFactorySelector"; String locatorFactorySelector = servletContext.getInitParameter(LOCATOR_FACTORY_SELECTOR_PARAM); // 声明:public static final String LOCATOR_FACTORY_KEY_PARAM = "parentContextKey"; String parentContextKey = servletContext.getInitParameter(LOCATOR_FACTORY_KEY_PARAM); if (parentContextKey != null) { // locatorFactorySelector可能会为null, 则会使用默认的 "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(); } return parentContext; }

这里通过ServletContext 获取初始化参数locatorFactorySelector指定的定义父容器的xml文件的地址,同时获取初始化参数parentContextKey指定的父容器在前面xml文件中设置的bean名称。下面是一个列子。

第一步在classes路径下创建名为parentBeanRefContext.xml的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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> <!-- 指定容器 --> <bean id="parentContext" class="org.springframework.context.support.ClassPathXmlApplicationContext"> <constructor-arg> <list> <value>parentOfRootContext.xml</value> </list> </constructor-arg> </bean></beans>

其中parentOfRootContext.xml文件为一个普通的spring配置文件,这里就不举例了。

第二步,在web.xml文件中做如下配置。

<!-- 定义locatorFactorySelector参数 --> <context-param> <param-name>locatorFactorySelector</param-name> <param-value> classpath:parentBeanRefContext.xml </param-value> </context-param> <!-- 定义parentContextKey参数 --> <context-param> <param-name>parentContextKey</param-name> <param-value>parentContext</param-value> </context-param>

这样就向容器中指定了一个父容器。在这里如果在第一步中创建的xml文件的名称为beanRefContext.xml,那么在web.xml文件中就不用配置locatorFactorySelector参数。

3. 配置并刷新spring容器

设置了父容器后,执行ContextLoader的configureAndRefreshWebApplicationContext方法,在容器刷新前对容器进行初始化配置,代码如下

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) { if (ObjectUtils.identityToString(wac).equals(wac.getId())) { // 为容器设置一个有用的ID // 声明:public static final String CONTEXT_ID_PARAM = "contextId"; String idParam = sc.getInitParameter(CONTEXT_ID_PARAM); if (idParam != null) { wac.setId(idParam); } else { // 生成一个默认ID if (sc.getMajorVersion() == 2 && sc.getMinorVersion() < 5) { // servlet 2.5以前的版本 wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getServletContextName())); } else { // servlet 2.5及其以上的版本wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath())); } } } // 把ServletContext保存到容器中 wac.setServletContext(sc); // 设置容器要加载的配置文件所在的路径 // 通过contextConfigLocation上下文参数指定配置文件路径 // 声明:public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation"; String initParameter = sc.getInitParameter(CONFIG_LOCATION_PARAM); if (initParameter != null) { wac.setConfigLocation(initParameter); } // 在容器刷新前,自定义容器。 // 执行用户通过contextInitializerClasses上下文参数指定的容器初始化器 customizeContext(sc, wac); // 刷新容器 wac.refresh(); } protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) { if (ObjectUtils.identityToString(wac).equals(wac.getId())) { // 为容器设置一个有用的ID // 声明:public static final String CONTEXT_ID_PARAM = "contextId"; String idParam = sc.getInitParameter(CONTEXT_ID_PARAM); if (idParam != null) { wac.setId(idParam); } else { // 创建一个默认的id wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath())); } } // 把ServletContext保存到容器中 wac.setServletContext(sc); // 设置容器要加载的配置文件所在的路径 // 通过contextConfigLocation上下文参数指定配置文件路径 // 声明:public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation"; String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM); if (configLocationParam != null) { wac.setConfigLocation(configLocationParam); } // 提前执行容器环境对象的initPropertySources方法,以确保servlet属性资源可应用于容器刷新前的任何初始化操作。 ConfigurableEnvironment env = wac.getEnvironment(); if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment) env).initPropertySources(sc, null); } // 在容器刷新前,自定义容器。 // 执行用户通过contextInitializerClasses上下文参数指定的容器初始化器 customizeContext(sc, wac); // 刷新容器 wac.refresh(); }

这段代码处理配置在web.xml中的2个初始化参数,第一个是用于标志容器id的contextId初始化参数,第二是用于指定配置文件地址的contextConfigLocation初始化参数。对于contextId参数没有过多的探讨,至于contextConfigLocation参数,可以配置,也可以不配置。如果需要通过contextConfigLocation参数指定多个配置文件,配置文件地址之间可以通过英文逗号、分号、空格、制表符、换行符隔开。如果没有配置contextConfigLocation参数,XmlWebApplicationContext将使用WEB-INF目录下的默认配置文件地址,代码如下。

/** Default config location for the root context */ public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml"; /** Default prefix for building a config location for a namespace */ public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/"; /** Default suffix for building a config location for a namespace */ public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml"; @Override protected String[] getDefaultConfigLocations() { if (getNamespace() != null) { return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX}; } else { // 返回配置地址/WEB-INF/applicationContext.xml return new String[] {DEFAULT_CONFIG_LOCATION}; } }

根据这段代码,可以获得两个信息。 其一:如果spring容器有命名空间,则开发者可以在WEB-INF目录下创建以命名空间为名称的xml配置文件。 其二:如果spring容器没有命名空间,则开发者可以在WEB-INF目录下创建以applicationContext为名称的xml配置文件。

在configureAndRefreshWebApplicationContext方法中还调用ContextLoader的customizeContext方法来执行用户指定ApplicationContextInitializer对象,下面是customizeContext方法的代码。

protected void customizeContext(ServletContext servletContext, ConfigurableWebApplicationContext applicationContext) { // 获取ApplicationContextInitializer类对象列表 List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses = determineContextInitializerClasses(servletContext); if (initializerClasses.size() == 0) { // 没有指定任何 ApplicationContextInitializers对象,则什么都不做,直接返回 return; } Class<?> contextClass = applicationContext.getClass(); ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerInstances = new ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>>(); for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) { // 从initializerClass对象获取ApplicationContextInitializer的泛型类对象 Class<?> initializerContextClass = GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class); // 检查contextClass是否是initializerContextClass类对象指定的类的实现 Assert.isAssignable(initializerContextClass, contextClass, String.format( "Could not add context initializer [%s] as its generic parameter [%s] " + "is not assignable from the type of application context used by this " + "context loader [%s]: ", initializerClass.getName(), initializerContextClass.getName(), contextClass.getName())); initializerInstances.add(BeanUtils.instantiateClass(initializerClass)); } ConfigurableEnvironment env = applicationContext.getEnvironment(); if (env instanceof ConfigurableWebEnvironment) { // 初始化Property资源 ((ConfigurableWebEnvironment)env).initPropertySources(servletContext, null); } // 一个一个的执行ApplicationContextInitializer对象 Collections.sort(initializerInstances, new AnnotationAwareOrderComparator()); for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : initializerInstances) { initializer.initialize(applicationContext); } }

这段代码主要是调用ContextLoader对象的determineContextInitializerClasses方法来获取ApplicationContextInitializer类对象列表,并使用每个ApplicationContextInitializer对象来对spring容器做更多的初始化操作。下面是determineContextInitializerClasses的代码。

protected List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> determineContextInitializerClasses(ServletContext servletContext) { List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> classes = new ArrayList<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>>(); // 从ServletContext中获取应用于所有spring web应用容器的ApplicationContextInitializer实现类全名称 // 声明有:public static final String GLOBAL_INITIALIZER_CLASSES_PARAM = "globalInitializerClasses"; String globalClassNames = servletContext.getInitParameter(GLOBAL_INITIALIZER_CLASSES_PARAM); if (globalClassNames != null) { for (String className : StringUtils.tokenizeToStringArray(globalClassNames, INIT_PARAM_DELIMITERS)) { classes.add(loadInitializerClass(className)); } } // 从ServletContext中获取专为为此容器指定的ApplicationContextInitializer实现类全名称 // 声明:public static final String CONTEXT_INITIALIZER_CLASSES_PARAM = "contextInitializerClasses"; 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; } private Class<ApplicationContextInitializer<ConfigurableApplicationContext>> loadInitializerClass(String className) { try { Class<?> clazz = ClassUtils.forName(className, ClassUtils.getDefaultClassLoader()); Assert.isAssignable(ApplicationContextInitializer.class, clazz); return (Class<ApplicationContextInitializer<ConfigurableApplicationContext>>) clazz; } catch (ClassNotFoundException ex) { throw new ApplicationContextException("Failed to load context initializer class [" + className + "]", ex); } }

determineContextInitializerClasses方法从ServletContext中获取初始化参数contextInitializerClasses和globalInitializerClasses的值,这个值指定了用户自定义的ApplicationContextInitializer实现类全名称。然后根据类的全名称创建Class对象。其中如果需要指定多个ApplicationContextInitializer实现类,那么实现类的全名称之间使用英文逗号隔开,比如下面的配置。

<!-- 定义globalInitializerClasses参数 --> <context-param> <param-name>globalInitializerClasses</param-name> <param-value> com.damuzee.web.app.GlobalApplicationContextInitializer1, com.damuzee.web.app.GlobalApplicationContextInitializer2, com.damuzee.web.app.GlobalApplicationContextInitializer3 </param-value> </context-param> <!-- 定义contextInitializerClasses参数 --> <context-param> <param-name>contextInitializerClasses</param-name> <param-value> com.damuzee.web.app.XmlApplicationContextInitializer1, com.damuzee.web.app.XmlApplicationContextInitializer2, com.damuzee.web.app.XmlApplicationContextInitializer3 </param-value> </context-param>

到此,在web应用中spring容器初始化的第一个阶段就完成了。configureAndRefreshWebApplicationContext方法通过调用容器的refresh()方法进入容器初始化的第二阶段——容器的刷新过程

第二阶段: 容器的刷新过程

关于spring容器的刷新过程已经在另一篇文章中描述了,详见 Spring ApplicationContext的刷新过程

总结

在spring容器初始化的第一个阶段,我们可以通过web.xml文件的配置来定制spring容器。通过web.xml文件,我们可以指定其他容器类、父容器、容器的id、需要加载的配置文件地址、以及自定义容器初始化器ApplicationContextInitializer对象。具体例子如下。

通过设置contextClass参数指定容器,例如 <context-param> <param-name>contextClass</param-name> <param-value> com.chyohn.context.XmlWebApplicationContext </param-value> </context-param>设置locatorFactorySelector和parentContextKey参数指定父容器,例如 <!-- 定义locatorFactorySelector参数 --> <context-param> <param-name>locatorFactorySelector</param-name> <param-value> classpath:parentBeanRefContext.xml </param-value> </context-param> <!-- 定义parentContextKey参数 --> <context-param> <param-name>parentContextKey</param-name> <param-value>parentContext</param-value> </context-param>

上面的parentBeanRefContext.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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> <!-- 指定容器 --> <bean id="parentContext" class="org.springframework.context.support.ClassPathXmlApplicationContext"> <constructor-arg> <list> <value>parentOfRootContext.xml</value> </list> </constructor-arg> </bean></beans> 设置contextId参数指定容器的Id,配置如下。 <context-param> <param-name>contextId</param-name> <param-value>myContextId</param-value> </context-param>设置contextConfigLocation参数指定加载的配置文件地址,配置如下。 <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:webApplicationContent.xml classpath:application-service.xml </param-value> </context-param>设置contextInitializerClasses参数指定容器初始化器,该初始化器将在容器刷新前执行,如果有多个初始化器,使用英文逗号“,”隔开,例如 <!-- 定义contextInitializerClasses参数 --> <context-param> <param-name>contextInitializerClasses</param-name> <param-value> com.damuzee.web.app.XmlApplicationContextInitializer1, com.damuzee.web.app.XmlApplicationContextInitializer2, com.damuzee.web.app.XmlApplicationContextInitializer3 </param-value> </context-param>设置globalInitializerClasses参数指定所有容器公共的初始化器,该初始化器将在容器刷新前执行,如果有多个初始化器,使用英文逗号“,”隔开,例如 <!-- 定义globalInitializerClasses参数 --> <context-param> <param-name>globalInitializerClasses</param-name> <param-value> com.damuzee.web.app.GlobalApplicationContextInitializer1, com.damuzee.web.app.GlobalApplicationContextInitializer2, com.damuzee.web.app.GlobalApplicationContextInitializer3 </param-value> </context-param>

有了ApplicationContextInitializer对象,可以对容器的初始化做更多操作,比如设置容器id、设置父容器、设置加载的配置文件地址、添加容器级的bean工厂后处理器、监听器等等。


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表