Spring启动流程(原理)详解--结合web.xml加载配置分析

Spring启动流程(原理)详解--结合web.xml加载配置分析_第1张图片

注:文章内容有点多,可以先总体大概浏览一遍,心中有个整体类目关系构图,再详细看,不至于迷失在细节中,最好本地也使用开发工具打开源码对着看。

引言:

Spring框架已经成为目前JavaEE企业应用的主流框架,它提供了一个全面的编程和配置模型,适用于任何类型的部署平台。

Spring的一个关键元素是应用程序级别的基础设施支持:Spring专注于企业应用程序的“管道”,这样我们开发人员就可以专注于应用程序的业务逻辑开发,而不用关注于底层对象的管理与环境配置等。

说到Spring,我们基本都能说出它的两大特性:IoC、AOP,但底层的实现原理、启动流程等就不一定有所深入了解。

Spring 特性:

  • 核心技术:依赖注入、事件、资源、i18n、验证、数据绑定、类型转换、SpEL、AOP。
  • 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。
  • 数据访问:事务、DAO支持、JDBC、ORM、编组XML。
  • Spring MVC和Spring WebFlux web框架。
  • 集成:远程调用、JMS、JCA、JMX、电子邮件、任务、调度、缓存。
  • 语言:Kotlin、Groovy、动态语言。

Spring架构:

Spring框架是一个分层架构,总共包含20多个模块,包含一系列的功能要素,由 1300 多个不同的文件构成。所有组件被分别整合在核心容器(Core Container)AOP(Aspect Oriented Programming) 、Aspects(切面)、设备支持(Instrmentation)数据访问及集成(Data Access/Integeration)Web报文发送(Messaging)Test 8个模块集合中,Spring 架构图如下:

Spring启动流程(原理)详解--结合web.xml加载配置分析_第2张图片

注:篇幅有限,这块内容也不是本文重点,每一个架构模块,可参考其他资料。

下面将结合源码分析Spring的启动流程:

Spring的启动是建立在servlet/web容器(Tomcat、JBoss、Jetty等)之上的,一个常规的Spring应用,在web容器启动时,默认会先去加载/WEB-INF/web.xml,它配置了:servletContext上下文、监听器(Listener)、过滤器(Filter)、Servlet等

常规的web.xml示例:


    
        contextConfigLocation
        /WEB-INF/applicationContext.xml
    

    
        
            org.springframework.web.context.ContextLoaderListener
        
    




    CharacterEncodingFilter
    org.springframework.web.filter.CharacterEncodingFilter
    
        encoding
        utf-8
    


    CharacterEncodingFilter
    /*



    
        dispatchServlet
        org.springframework.web.servlet.DispatcherServlet
        
            contextConfigLocation
            classpath:spring/springmvc.xml
        
        1
    
    
        dispatchServlet
        *.do
    

web.xml 加载顺序为: context-param < listener < filter < servlet

当然,也可以自定义自己的监听器、过滤器、拦截器等。这里只是截取主要的常用配置,下面将结合web.xml详细介绍它的启动流程。

一、ServletContext上下文

JavaEE标准规定,servlet容器需要在应用项目启动时,给应用程序初始化一个ServletContext作为公共环境容器存放公共信息,ServletContext中的信息都是由容器提供的。servlet规范当中,使用了Listener监听器机制来进行web容器相关组件的生命周期管理以及Event事件监听器来实现组件之间的交互

其中一个重要的生命周期监听器是 ServletContextListener 。web容器在创建和初始化 ServletContext 的时候,会产生一个ServletContextEvent 事件,其中 ServletContextEvent 包含该 ServletContext 的引用。然后交给在web.xml中配置的,注册到这个ServletContext 的监听器 ServletContextListener。ServletContextListener 在其 contextInitialized 方法中定义处理逻辑,源码如下:

package javax.servlet;

import java.util.EventListener;

public interface ServletContextListener extends EventListener {
    void contextInitialized(ServletContextEvent var1);

    void contextDestroyed(ServletContextEvent var1);
}

从 contextInitialized 方法的注释可知:通知所有的ServletContextListeners,当前的web应用正在启动,而且这些ServletContextListeners是在Filters和Servlets创建之前接收到通知的。所以在这个时候,web应用还不能接收请求,故可以在这里完成底层处理请求的组件的加载,这样等之后接收请求的Filters和Servlets创建时,则可以使用这些创建好的组件了。spring相关的bean就是这里所说的底层处理请求的组件,如数据库连接池,数据库事务管理器等。

举例:

通过自定义contextListener获取web.xml中配置的参数,需要实现 ServletContextListener 接口。

1.容器启动时,优先会加载web.xml 配置文件标签中的内容,作为键值对放到ServletContext中。

2.然后找到对应的标签 ,容器调用指定的监听器的 contextInitialized(ServletContextEvent event) 方法,执行其中的操作。

例如:在web.xml中配置


   key
   value

 
   com.stwen.spring.listener.MyContextListener

其中,MyContextListener 是自定义实现ServletContextListener 接口的类:

package com.stwen.spring.listener;
 
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
 
public class MyContextListener implements ServletContextListener {
    
    @Override
    public void contextDestroyed(ServletContextEvent event) {
        System.out.println("===========销毁自定义的监听器:MyContextListener=============");
    }
    
    @Override
    public void contextInitialized(ServletContextEvent event) {
        System.out.println("===========初始化自定义的监听器:MyContextListener===========");
        ServletContext servletContext = event.getServletContext();
        System.out.println("key:"+servletContext.getInitParameter("key"));
    }
    
}

web.xml中可以定义两种参数:

  •     一个是全局参数(ServletContext),通过定义,如上;
  •     一个是servlet参数,通过在servlet中声明:
  
    param1
    avalible in servlet init()   
  

第一种参数在servlet里面可以通过 getServletContext().getInitParameter("context/param") 得到;

第二种参数只能在Servlet的 init() 方法中通过 this.getInitParameter("param1")取得:

package javax.servlet;

import java.io.IOException;

public interface Servlet {
    void init(ServletConfig var1) throws ServletException;

    ServletConfig getServletConfig();

    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

    String getServletInfo();

    void destroy();
}

注:servlet其实是一个接口,接口就是规定了一些规范,使得一些具有某些共性的类都能实现这个接口,从而都遵循某些规范。广义上可以把实现servlet的实现类,也统称为一个servlet。Servlet容器默认是采用单实例多线程的方式处理多个请求的,不熟悉servlet 的读者可以参考其他资料。

Spring启动流程(原理)详解--结合web.xml加载配置分析_第3张图片

二、Spring应用上下文

spring为我们提供了实现 ServletContextListener 接口的上下文初始化监听器实现类:org.springframework.web.context.ContextLoaderListener

Spring启动流程(原理)详解--结合web.xml加载配置分析_第4张图片

ContextLoaderListener:spring-web包的ContextLoaderListener就是一个ServletContextListener的实现类。ContextLoaderListener主要用来获取spring项目的整体配置信息,并创建对应的WebApplicationContext来保存bean的信息,以及创建这些bean的对象实例。默认去WEB-INF下加载applicationContext.xml配置,如果applicationContext.xml放在其他位置,或者使用其他不同的名称,或者使用多个xml文件,则与指定contextConfigLocation。

如下配置:


    
        contextConfigLocation
        
            /WEB-INF/applicationContext.xml
            classpath:spring/spring-mvc.xml,
            classpath:spring/spring-redis.xml,
            classpath:spring/dubbo-provider.xml
        
    

    
        
            org.springframework.web.context.ContextLoaderListener
        
    

Spring的启动其实就是IOC容器的启动初始化过程,如上标签中内容便是初始化上下文,然后通过读取标签中配置的Spring上下文初始化监听器, 执行其中的contextInitialized,其中,参数名称必须配置为:contextConfigLocation

ContextLoaderListener 源码如下,同时可以看到,它是上面 ServletContextListener的一个实现类:

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    public ContextLoaderListener() {
    }

    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }
    //初始化ServletContext上下文配置
    public void contextInitialized(ServletContextEvent event) {
        this.initWebApplicationContext(event.getServletContext());
    }

    //销毁ServletContext上下文
    public void contextDestroyed(ServletContextEvent event) {
        this.closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}

可以看到,contextInitialized()调用了父类ContextLoaderinitWebApplicationContext(event.getServletContext());方法,很显然,这是对 ApplicationContext 的初始化方法,也就是到这里正式进入了 Spring IOC 的初始化

接下来具体看 initWebApplicationContext方法,由于该方法较长,下面拆分小模块解读,如下:

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    //1、判断是否已经初始化过了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!");
    }
    //2、日志记录,显示初始化Spring WebApplicationContext
    servletContext.log("Initializing Spring root WebApplicationContext");
    Log logger = LogFactory.getLog(ContextLoader.class);
    if (logger.isInfoEnabled()) {
        logger.info("Root WebApplicationContext: initialization started");
    }
    //3、记录启动耗时/ms
    long startTime = System.currentTimeMillis();

    try {
        ...
        return this.context;
    }
    catch (RuntimeException | Error ex) {
        ...
    }
}

首先判断是否创建了WebApplicationContext,正常情况下创建了一个WebApplicationContext后,会把context set到ServletContext中,setAttribute的本质其实是往LinkedHashMap中set值:

servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

如果从servletContext中根据key获得了WebApplicationContext对象,则表示之前已经创建过了根上下文WebApplicationContext,此时抛出异常,提示检查web.xml中的配置,避免重复创建root上下文,保证应用只有一个Spring容器。

然后,再看其中try{ } catch { } 部分逻辑:

        try {
            // 将上下文存储在本地实例变量中,以确保它在ServletContext关闭时可用
            if (this.context == null) {
                //(1)
                this.context = createWebApplicationContext(servletContext);
            }
             //(2)
            if (this.context instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
                if (!cwac.isActive()) {
                    // 上下文还没有刷新——>提供了诸如设置父上下文、设置应用程序上下文id等服务
                    if (cwac.getParent() == null) {
                        // 上下文实例是在没有显式父类的情况下注入的——如果有根web应用程序上下文,则确定它为父类
                        ApplicationContext parent = loadParentContext(servletContext);
                        cwac.setParent(parent);
                    }
                    //(3)
                    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;
        }

其中,try模块中主要有调用了两个核心方法:

  • createWebApplicationContext()
  • configureAndRefreshWebApplicationContext()

根据上面代码标注的(1)(2)(3),逐步往下看:

(1)createWebApplicationContext()

首先是判断 this.context == null,是则调用 createWebApplicationContext ()方法先创建一个 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);
	}

determineContextClass,字面意思为检测Context的class类型,会读取servletContext的初始化参数contextClass,大部分情况下我们不会配置此参数,在未配置的情况下,Spring会去org.springframework.web.context包中的ContextLoader.properties配置文件读取默认配置XmlWebApplicationContext 

# 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

通过Spring提供的ClassUtil进行反射,反射出XmlWebApplicationContext类,再通过BeanUtils进行反射,调用无参构造器,instance出一个WebApplicationContext并返回ConfigurableWebApplicationContext。

注:determineContextClass 详解

在这个方法中,Spring会初始化一个启动上下文,这个上下文被称为根上下文,即WebApplicationContext,这是一个接口类,确切的说,其实际的实现类是 XmlWebApplicationContext ,这个就是spring的IoC容器,其对应的Bean定义的配置,就是由上面web.xml中的 context-param 标签指定

determineContextClass (sc) 其返回的是一个WebApplicationContext的实现类,可以是默认的 XmlWebApplicationContext,也可以是指定一个自定义的类。源码如下:

	protected Class determineContextClass(ServletContext servletContext) {
		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);
			}
		}
	}

首先通过 servletContext.getInitParameter(CONTEXT_CLASS_PARAM) 读取 CONTEXT_CLASS_PARAM 参数,检查是否有用户自定义的 WebApplicationContext 实现类,该属性是在 ContextLoader 类中定义的一个静态常量:

Spring启动流程(原理)详解--结合web.xml加载配置分析_第5张图片

自定义contextClass,其配置方式需要在web.xml中配置属性名为contextClass


      contextClass
      com.xxx.yyy.zzzClassName
  

示例:使用@Configuration 注解的方式代替 XML的配置方式,需要指定 WebApplicationContext 的实现类,如下的AnnotationConfigWebApplicationContext  


    contextClass
    
        org.springframework.web.context.
        support.AnnotationConfigWebApplicationContext
    


    contextConfigLocation
    
        com.stwen.spring.MyConfiguration
    
  • @Configuration:相当于Spring的xml配置文件中的标签,即Spring容器的上下文。
  • @Bean:相当于Spring的xml配置文件中的标签,即注册一个bean对象。
@Configuration
public class MyConfiguration {
    
    @Bean
    public UserService userService(){
        return new UserServiceImpl();
    }
    
    ...
}

如果没有自定义配置contextClass的话,配置默认的 contextConfigLocation ,将会默认加载WebApplicationContext实现类 org.springframework.web.context.support.XmlWebApplicationContext

(2)ConfigurableWebApplicationContext

再来看try 中的第(2)个核心点部分:

    if (this.context instanceof ConfigurableWebApplicationContext) {
        ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
        if (!cwac.isActive()) {
            // 上下文还没有刷新——>提供了诸如设置父上下文、设置应用程序上下文id等服务
            if (cwac.getParent() == null) {
                // 上下文实例是在没有显式父类的情况下注入的——如果有根web应用程序上下文,则确定它为父类
                ApplicationContext parent = loadParentContext(servletContext);
                cwac.setParent(parent);
            }
            configureAndRefreshWebApplicationContext(cwac, servletContext);
        }
    }

第(1)步中获得了 WebApplicationContext 应用上下文后,第(2)步先是判断获得的context是否为 ConfigurableWebApplicationContext的实例,而上面默认返回的XmlWebApplicationContext实现类满足判断条件:

XmlWebApplicationContext --> AbstractRefreshableWebApplicationContext --> ConfigurableWebApplicationContext

然后,判断父上下文是否为active激活状态,如果active为false下(未激活),再判断父上下文是否为null,如果parent上下文为null的情况,则执行loadParentContext()。loadParentContext()方法,是一个默认的是模板实现方法,用于加载/获取ApplicationContext,此context为根WebApplicationContext的父上下文

(3)configureAndRefreshWebApplicationContext()

紧接着上面(2)中的最后一步,便是调用 configureAndRefreshWebApplicationContext()方法:

	protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
                // (3.1) 
		if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
			// 应用程序上下文id仍然设置为其原始默认值——>根据可用信息分配一个更有用的id
			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()));
			}
		}
                // (3.2) 设置sc到wac中,便于Spring获得ServletContext
		wac.setServletContext(sc);
		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
		ConfigurableEnvironment env = wac.getEnvironment();
		if (env instanceof ConfigurableWebEnvironment) {
			((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
		}

                // (3.3)
		customizeContext(sc, wac);
                // (3.4)
		wac.refresh();
	}

(3.1)将上面创建的XmlWebApplicationContext进行初始化操作,主要创建一个默认id,org.springframework.web.context.WebApplicationContext:+项目的ContextPath 。创建好id后,把servletContext放入创建好的XmlWebApplicationContext中,便于spring后续获得servletContext上下文。

(3.2)紧接着会去读取web.xml中 contextConfigLocation 的配置,如下:

    
        contextConfigLocation
        
            classpath:spring/applicationContext.xml,
            classpath:spring/spring-redis.xml,
            classpath:spring/dubbo-provider.xml
        
    

如果在web.xml中未配置,则会去读取WEB-INF下面的applicationContext.xml配置文件,即 XmlWebApplicationContext中的属性 DEFAULT_CONFIG_LOCATION的默认值:

读取到 contextConfigLocation 相关文件配置路径后,设置到 XmlWebApplicationContext 中,用于加载指定路径的配置文件。

(3.3) customizeContext(sc, wac); 主要用于自定义context相关配置,比如定义bean是否可以循环引用,bean定义是否可以被覆盖等,通常情况下不做任何操作。

(3.4)wac.refresh(); 整个容器启动的最核心方法,在这个refresh()方法中,会完成资源文件的加载、配置文件解析、Bean定义的注册、组件的初始化等核心工作。

refresh()源码如下:

	@Override
	public void refresh() throws BeansException, IllegalStateException {
        //加锁同步,防止refresh() 还没结束,并发又来个启动或销毁容器的操作
		synchronized (this.startupShutdownMonitor) {
			// 准备刷新 context上下文:记录下容器的启动时间、标记已激活状态、处理配置文件中的占位符等
			prepareRefresh();
			// 核心,这步完成后,配置文件就会解析成一个个 Bean 定义,注册到 BeanFactory 中,
                      // 当然,这里说的 Bean 还没有初始化,只是配置信息都提取出来了,
                      // 注册也只是将这些信息都保存到了注册中心(说到底核心是一个 beanName-> beanDefinition 的 map)
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
			// 准备bean工厂,以便在此上下文中使用
			prepareBeanFactory(beanFactory);
			try {
				// 这里是提供给子类的扩展点,到这里的时候,所有的 Bean 都加载、注册完成了,但是都还没有初始化
                             // 具体的子类可以在这步的时候添加一些特殊的 BeanFactoryPostProcessor 的实现类
				postProcessBeanFactory(beanFactory);
				// 调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 方法
				invokeBeanFactoryPostProcessors(beanFactory);
				// 注册拦截bean创建的bean处理器
				registerBeanPostProcessors(beanFactory);
				// 初始化此上下文的消息源
				initMessageSource();
				// 初始化此上下文的事件多播程序multicaster事件
				initApplicationEventMulticaster();
				// 在特定上下文子类中初始化其他特殊bean
				onRefresh();
				// 检查监听器 Listener beans并注册它们。
				registerListeners();
				// 实例化所有剩余的(非延迟加载)单例。
				finishBeanFactoryInitialization(beanFactory);
				// 最后一步:发布相应的事件。
				finishRefresh();
			}
			catch (BeansException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Exception encountered during context initialization - " +
							"cancelling refresh attempt: " + ex);
				}
				// 销毁已经创建的单例,以避免挂起资源
				destroyBeans();
				// 重置活跃标志
				cancelRefresh(ex);
				// 将异常传播给调用者
				throw ex;
			}

			finally {
				// 重置Spring核心中的公用内部默认缓存,因为我们
                // 可能再也不需要单例bean的元数据了……
				resetCommonCaches();
			}
		}
	}

注:篇幅有限,该 refresh 方法都可以单独一篇文章介绍。可参考:Spring启动过程(二) - 20岁的King的个人空间 - OSCHINA - 中文开源技术交流社区

总结:

整个初始化Spring应用上下文的方法 initWebApplicationContext(),主要以下3点:

  1. 创建WebApplicationContext。
  2. 加载对应的spring配置文件中的Bean。
  3. 将WebApplicationContext放入ServletContext(Java Web的全局变量)中

Spring的启动过程,就是其IoC容器的启动过程,本质就是创建和初始化bean的工厂(BeanFactory),BeanFactory其实就是整个Spring IoC的核心,Spring 使用 BeanFactory 来实例化、配置和管理 Bean。

 对于web程序,IoC容器启动过程即是建立上下文的过程,在web应用中,web容器会提供一个全局的ServletContext上下文环境,ServletContext上下文为Spring IoC提供了一个宿主环境。

    Spring应用在Web容器中启动的整个过程如下:

Spring启动流程(原理)详解--结合web.xml加载配置分析_第6张图片

三、SpringMVC的启动过程

上面的 contextLoaderListener监听器初始化完毕后,开始初始化web.xml中配置的Servlet,这个servlet可以配置多个,以最常见的DispatcherServlet为例,这个servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个servlet请求

DispatcherServlet上下文在初始化的时候会建立自己的IoC上下文,用以持有spring mvc相关的bean。在建立DispatcherServlet自己的IoC上下文时,会利用

WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE

先从ServletContext中获取之前的根上下文 (即WebApplicationContext) 作为自己上下文的parent上下文。有了这个parent上下文之后,再初始化自己持有的上下文。

SpringMVC相关的web.xml配置:


    
        dispatchServlet
        org.springframework.web.servlet.DispatcherServlet
        
            contextConfigLocation
            classpath:spring/springmvc.xml
        
        1
    
    
        dispatchServlet
        *.do
    

其中,中配置的便是SpringMVC的请求分发器,这个DispatcherServlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个servlet请求。

Spring启动流程(原理)详解--结合web.xml加载配置分析_第7张图片

如上类图,便可清晰看到继承关系,而DispatcherServlet 继承 FrameworkServlet 抽象类,FrameworkServlet 又继承 HttpServletBean 类。web容器启动的时候会调用 HttpServletBean 的init方法,这个方法覆盖了GenericServlet中的init方法。源码如下:

    public final void init() throws ServletException {
        PropertyValues pvs = new HttpServletBean.ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
        if (!pvs.isEmpty()) {
            try {
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
                this.initBeanWrapper(bw);
                bw.setPropertyValues(pvs, true);
            } catch (BeansException var4) {
                if (this.logger.isErrorEnabled()) {
                    this.logger.error("Failed to set bean properties on servlet '" + this.getServletName() + "'", var4);
                }

                throw var4;
            }
        }

        this.initServletBean();
    }

该初始化方法的主要作用

将Servlet初始化参数(init-param)设置到该组件上(如contextAttribute、contextClass、namespace、contextConfigLocation),通过BeanWrapper简化设值过程,方便后续使用;提供给子类初始化扩展点initServletBean(),该方法由子类 FrameworkServlet覆写。

FrameworkServlet 继承 HttpServletBean,通过 initServletBean() 进行Web上下文初始化,该方法主要两个功能点:

  • 初始化web上下文
  • 提供给子类初始化扩展点
    protected final void initServletBean() throws ServletException {
        this.getServletContext().log("Initializing Spring " + this.getClass().getSimpleName() + " '" + this.getServletName() + "'");
        if (this.logger.isInfoEnabled()) {
            this.logger.info("Initializing Servlet '" + this.getServletName() + "'");
        }

        long startTime = System.currentTimeMillis();

        try {
            this.webApplicationContext = this.initWebApplicationContext();
            this.initFrameworkServlet();
        } catch (RuntimeException | ServletException var4) {
            this.logger.error("Context initialization failed", var4);
            throw var4;
        }

        if (this.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";
            this.logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + "': request parameters and headers will be " + value);
        }

        if (this.logger.isInfoEnabled()) {
            this.logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
        }

    }

DispatcherServlet继承FrameworkServlet,并实现了onRefresh()方法,提供一些前端控制器相关的配置。

整个DispatcherServlet初始化的过程主要做了两件事情:

1、初始化Spring Web MVC使用的Web上下文,并且指定父容器为WebApplicationContext(上面分析的ContextLoaderListener加载了的根上下文,Spring父容器);

2、初始化DispatcherServlet使用的策略,如HandlerMapping、HandlerAdapter等。

DispatcherServlet 部分源码如下:

    protected void onRefresh(ApplicationContext context) {
        this.initStrategies(context);
    }

    protected void initStrategies(ApplicationContext context) {
        this.initMultipartResolver(context);
        this.initLocaleResolver(context);
        this.initThemeResolver(context);
        this.initHandlerMappings(context);
        this.initHandlerAdapters(context);
        this.initHandlerExceptionResolvers(context);
        this.initRequestToViewNameTranslator(context);
        this.initViewResolvers(context);
        this.initFlashMapManager(context);
    }

这个DispatcherServlet初始化自己上下文的工作在其 initStrategies 方法中可以看到,大概的工作就是初始化处理器映射、视图解析等。这个servlet自己持有的上下文默认实现类也是 xmlWebApplicationContext 。初始化完毕后,spring以与servlet的名字相关(此处不是简单的以servlet名为Key,而是通过一些转换,具体可自行查看源码) 的属性为属性Key,也将其存到 ServletContext 中,以便后续使用。这样每个servlet就持有自己的上下文,即拥有自己独立的bean空间,同时各个servlet共享相同的bean,即根上下文(第2步中初始化的上下文)定义的那些bean。

注:spring配置的原因,为什么在applicationContext.xml中排除controller,而在spring-mvc.xml中incloud这个controller ?

既然知道了spring的启动流程,那么web容器初始化webApplicationContext时作为公共的上下文环境,只需要将service、dao等的配置信息在这里加载,而servlet自己的上下文环境信息不需要加载。故,在applicationContext.xml中将@Controller注释的组件排除在外,而在dispatcherServlet加载的配置文件中将@Controller注释的组件加载进来,方便dispatcherServlet进行控制和查找。

applicationContext.xml:


 
       
 
 

spring-mvc.xml:

  
 
       
 
 

四、总结

1. 首先,对于一个web应用,其部署在web容器中,web容器提供其一个全局的上下文环境,这个上下文就是ServletContext,其为后面的Spring IoC容器提供宿主环境;

2. 其次,在web.xml中会提供有 contextLoaderListener。在web容器启动时,会触发容器初始化事件,此时contextLoaderListener会监听到这个事件,其 contextInitialized 方法会被调用,在这个方法中,spring会初始化一个启动上下文,这个上下文被称为根上下文,即WebApplicationContext,这是一个接口类,确切的说,其实际的实现类是 XmlWebApplicationContext ,这个就是spring的IoC容器,其对应的Bean定义的配置由web.xml中的 context-param 标签指定。在这个IoC容器初始化完毕后,spring以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE为属性Key,将其存储到ServletContext中,便于获取;

3. 再次,contextLoaderListener监听器初始化完毕后,开始初始化web.xml中配置的Servlet,这个servlet可以配置多个,以最常见的DispatcherServlet为例,这个servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个servlet请求。DispatcherServlet上下文在初始化的时候会建立自己的IoC上下文,用以持有spring mvc相关的bean。在建立DispatcherServlet自己的IoC上下文时,会利用WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 先从ServletContext中获取之前的根上下文(即WebApplicationContext)作为自己上下文的parent上下文。有了这个parent上下文之后,再初始化自己持有的上下文。

这个DispatcherServlet初始化自己上下文的工作在其 initStrategies 方法中可以看到,大概的工作就是初始化处理器映射、视图解析等。这个servlet自己持有的上下文默认实现类也是xmlWebApplicationContext。初始化完毕后,spring以与servlet的名字相关(此处不是简单的以servlet名为Key,而是通过一些转换,具体可自行查看源码)的属性为属性Key,也将其存到ServletContext中,以便后续使用。这样每个servlet就持有自己的上下文,即拥有自己独立的bean空间,同时各个servlet共享相同的bean,即根上下文(第2步中初始化的上下文)定义的那些bean。

本文来自:CSDN  作者:stwen_gan  Spring启动流程(原理)详解--结合web.xml加载配置分析_架构攻城之路的博客-CSDN博客_spring启动加载顺序及原理

觉得有用点个赞再走吧0.0,整理不易,转载请注明出处,谢谢。

参考:
spring官网:https://spring.io/
https://my.oschina.net/klausprince/blog/1791357

●史上最强Tomcat8性能优化

●阿里巴巴为什么能抗住90秒100亿?--服务端高并发分布式架构演进之路

●B2B电商平台--ChinaPay银联电子支付功能

●学会Zookeeper分布式锁,让面试官对你刮目相看

●SpringCloud电商秒杀微服务-Redisson分布式锁方案

查看更多好文,进入公众号--撩我--往期精彩

一只 有深度 有灵魂 的公众号0.0

你可能感兴趣的:(Spring,Spring,SpringMVC,web.xml,架构,Java)