文档章节

springMVC启动过程源码解析(一)

DJZhu
 DJZhu
发布于 2017/06/19 16:09
字数 2830
阅读 32
收藏 1

之前的文章讨论过,一个简单的DefaultListableBeanFactory是如何启动的,这次再来看看SpringMVC项目是怎样启动的。

我们知道SpringMVC项目底层也是通过BeanFactory在维护众多的Bean,不同的地方在在于SpringMVC的BeanFactory是放在一个web容器中运行的,它的启动也由容器来控制(不同于DefaultListableBeanFactory是在一个main方法中调用)。另外,SpringMVC还涉及到一个复杂的Environment需要我们关心。

web.xml是所有web容器跟容器内项目发生联系的首要内容,通常,一个SpringMVC项目的web.xml看起来是这样的:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         metadata-complete="true">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath*:application-context.xml
        </param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>springServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:/spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

相信有过开发经验的朋友对这样的配置已经是再熟悉不过了,每一个配置的含义这里就不赘述,直接来看看web容器是按什么顺序解析此配置的。

在javax.sevlet包中,有所谓的EventListener,这是为了让开发者可以对servlet中的一些事件作出处理的一套设计。

Event,即事件,可以是web程序在运行过程中的启动、销毁、用户请求、打开/关闭一个会话或者是对web参数(attribute)的修改等等。 Listener,即监听器。是servlet提供的可以对上述事件作出处理的接口。

比如我们常用的ContextLoaderListener,他的继承关系是这样的: 输入图片说明 它的作用是监听servlet的启动,并作出具体的初始化动作。 这里可以简单分析下设计者对此Listener的设计理念:实现ServletContextListener是为了监听,此接口包含两个方法:

    /**
     * Receives notification that the web application initialization
     * process is starting.
     * All ServletContextListeners are notified of context
     * initialization before any filters or servlets in the web
     * application are initialized.
     *
     * @param sce the ServletContextEvent containing the ServletContext that is being initialized
     */
    public void contextInitialized(ServletContextEvent sce);

    /**
     * Receives notification that the ServletContext is about to be
     * shut down.
     * All servlets and filters will have been destroyed before any
     * ServletContextListeners are notified of context
     * destruction.
     *
     * @param sce the ServletContextEvent containing the ServletContext that is being destroyed
     */
    public void contextDestroyed(ServletContextEvent sce);

继承ContextLoader是为了实际处理启动的动作。在此类中,指定了web.xml中的一些启动参数名:比如:

	/**
	 * Name of servlet context parameter (i.e., {@value}) that can specify the
	 * config location for the root context, falling back to the implementation's
	 * default otherwise.
	 * @see org.springframework.web.context.support.XmlWebApplicationContext#DEFAULT_CONFIG_LOCATION
	 */
	public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";

	/**
	 * Config param for the root WebApplicationContext implementation class to use: {@value}
	 * @see #determineContextClass(ServletContext)
	 */
	public static final String CONTEXT_CLASS_PARAM = "contextClass";

spring-mvc就是利用此listener,在启动servlet的时候初始化一个BeanFactory。来看看具体的流程。

  1. 调用public void contextInitialized(ServletContextEvent event)开始执行启动程序
  2. 调用ContextLoader的initedWebApplicationContext(ServletContextEvent event)方法实际执行启动事务
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        //初始化context完成之后,会把构造的context对象放到servletContext的attribute里面,如果发现此时已有context,表示重复初始化,抛异常
        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 {
            // Store context in local instance variable, to guarantee that
            // it is available on ServletContext shutdown.
            if (this.context == null) {
                //实际执行创建webApplicationContext
                this.context = createWebApplicationContext(servletContext);
            }
            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
                    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);
                    }
                    configureAndRefreshWebApplicationContext(cwac, servletContext);
                }
            }
           //初始化完成,把webApplicationContext写进servletContext
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
            if (ccl == ContextLoader.class.getClassLoader()) {
                this.currentContext = this.context;
            }
            else if (ccl != null) {
                this.currentContextPerThread.put(ccl, this.context);
            }
            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;
        }
    }

在这里涉及到一个比较重要的概念,两个context。一个是servletContext,一个是webApplicationContext。前者是web容器里的概念,在javax.servlet包中被定义,跟spring无关,容器在启动一个web应用的时候,会为之创建一个context,它代表了整个web项目,里面的所有servlet共享这个context。而WebApplicationContext是spring中的概念,它在org.springframework.web.context被定义,它代表了一个spring项目的上下文。他们的关系是:后者只是前者中的一个attribute。

此方法中,关键是调用createWebApplicationContext(servletContext)方法,它实现了从servletContext(javaWeb概念)的配置构造一个WebApplicationContext实例(Spring概念)的过程,默认情况下它构造的是一个XmlWebApplicationContext,你也可以通过在web.xml中配置contextClass参数来指定构造一个自定义的Context(必须继承自ConfigurableWebApplicationContext接口,原因下面再说)。

下面是简化的WebApplicationContext的继承关系,及主要节点的所在包,通过包也可以很清晰的发现每次继承所带给WebApplicationContext的新特性。BeanFactory和ListableBeanFacotry在beans.facotry包中,代表他们带来的只是最基础的bean工厂的特性,相应此接口定义的也是一些获取Bean,判断是否存在特定的bean的方法。core.io包中的接口定义的是整个spring体系的基础特性,比如这里的ResourceLoader里面定义的是如何获取资源,这个特性的引入使得整个WebApplicationContext可以读取配置文件信息。context包,如其名,引入的是应用上下文环境的特性,ApplicationContext是这个包中最基础的一个接口,他定义了“获取context的名字”,“获取context启动时间”,“获取context的父级环境”等这样基础的方法。至于web.context则是把context的含义进一步细化到web,在这里我们有了Servlet的概念,可以获取ServletContext。

输入图片说明

在完成context的初始化之后,判断该context是一个ConfigurableWebApplicationContext的实现,继续做两件事:

  1. 设置context的父级context
  2. 配置context

在配置context的时候,主要是执行了refresh()方法。他是在ConfigurableApplicationContext接口中定义的一个接口。此方法执行的动作是根据我们在web.xml中定义的contextConfigLocation的配置。在这里是构造BeanFactory并装载需要的Bean。XmlApplicationContext类是在其继承的AbstractApplicationContext中实现了refresh()方法。

AbstractApplicationContext.refresh()此方法是整个SpringMVC初始化的关键。 它是这样的:

@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// 准备
			prepareRefresh();

			// 调用obtainFreshBeanFactory()方法是为了告知子类构建一个新的BeanFactory
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			//对BeanFactory做一些准备的工作
			prepareBeanFactory(beanFactory);

			try {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
			}

			catch (BeansException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Exception encountered during context initialization - " +
							"cancelling refresh attempt: " + ex);
				}

				// Destroy already created singletons to avoid dangling resources.
				destroyBeans();

				// Reset 'active' flag.
				cancelRefresh(ex);

				// Propagate exception to caller.
				throw ex;
			}

			finally {
				// Reset common introspection caches in Spring's core, since we
				// might not ever need metadata for singleton beans anymore...
				resetCommonCaches();
			}
		}
	}

ConfigurableApplicationContext.refresh()方法是SpringMVC初始化的关键,他的API定义是这样的:

Load or refresh the persistent representation of the configuration, which might an XML file, properties file, or relational database schema.

As this is a startup method, it should destroy already created singletons if it fails, to avoid dangling resources. In other words, after invocation of that method, either all or no singletons at all should be instantiated.

划重点就是:从XML或者properties文件这样的配置中,装载一个唯一的环境,它是一个启动方法,要么成功,环境初始化完成,要么失败,完全退出。如此重要的一个方法,值得我们深入研究。

首先来的是一个prepareRefresh()方法,做的事情是校验context启动的条件是否完备,设置context启动时间,装载必须的配置属性为后续正式的初始化动作做准备。

然后,是关键之一,构造一个BeanFactory:

ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

这个方法的底层是调用由其定义,但实际上由子类AbstractRefreshableApplicationContext实现的refreshBeanFactory()方法:

	@Override
	protected final void refreshBeanFactory() throws BeansException {
		if (hasBeanFactory()) {
			destroyBeans();
			closeBeanFactory();
		}
		try {
                        //这里实现的是DefaultListableBeanFactory,没错,就是之前讨论过的那个基础的BeanFactoty实现类
			DefaultListableBeanFactory beanFactory = createBeanFactory();
                        //设id
			beanFactory.setSerializationId(getId());
                        //设置BeanFactory的属性,比如是否允许循环依赖
			customizeBeanFactory(beanFactory);
                        //通过XmlBeanDefinitionReader读取xml配置装载BeanDefinition
			loadBeanDefinitions(beanFactory);
			synchronized (this.beanFactoryMonitor) {
				this.beanFactory = beanFactory;
			}
		}
		catch (IOException ex) {
			throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
		}
	}

如果有心,看到loadBeanDefinitions()你也许要会心一笑。哈,这里不就使我们之前通过main()方法直接初始化一个BeanFactory的时候,XmlBeanDefinitionReader里面也定义了的一个同名方法么?没错,这里的loadBeanDefinitions虽然不由XmlBeanDefinitionReader定义,但是做的事情却是类似的,读取配置文件,装载BeanDefinition。装载BeanDefinition的过程,就是注册Bean的过程,它通过读取比如xml配置,把bean的信息装入当前环境,等到需要用的时候,再由BeanFactory定义的getBean()方法取出Bean实例来使用。不同的地方在于,我们之前自己写的main()方法虽然也是初始化了一个BeanFactory,可它比较简陋,SpringMVC的启动过程则要复杂得多,你可以通过配置,实例化一些更强大的Bean,添加各种高级属性,比如AOP等等。怎么实现的?继续往下看。

完成refreshBeanFactory()方法之后,context就有了一个BeanFactory,它包含了我们在applicationConfigLocation定义的所有bean的信息,即BeanDefinitionMap:

输入图片说明

下面一句就是:

prepareBeanFactory(beanFactory);

从方法名可以看出,它所做的事情是准备BeanFactory,啥意思?可以这样想:在上一句,我们在指定了context作为一个可以提供Bean的工厂,它应该有哪些bean,这些bean的属性是怎样的。但是这并不是真的在内存中拥有了这些bean的实例,要获取这些实例,还需要告诉context,这些bean从什么地方获取(指定ClassLoader)?在实例化到了什么阶段的时候,通知回调方法执行,让我们可以为bean添加附加的属性等等。这就是这个方法做的。注意!它只是在BeanFactory实例化完成后,补充了相应的信息,并没有真正的完成bean的装配。

	protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
		// Tell the internal bean factory to use the context's class loader etc.
		//告知classLoader的存在
		beanFactory.setBeanClassLoader(getClassLoader());
		//设置bean表达式解析器。
		beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
		beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));

		// Configure the bean factory with context callbacks.
		//设置context回调方法
		beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
		//下面的代码的作用是在装配(autowiring)的时候忽略指定类型的接口,这样做的原因是因为像Enviroment和ResourceLoader这样bean已经完成了装配?暂时做这样的理解,不知道是否准确,看官可以继续深入研究
		beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
		beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
		beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
		beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
		beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
		beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);

		// BeanFactory interface not registered as resolvable type in a plain factory.
		// MessageSource registered (and found for autowiring) as a bean.
		beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
		beanFactory.registerResolvableDependency(ResourceLoader.class, this);
		beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
		beanFactory.registerResolvableDependency(ApplicationContext.class, this);

		// Register early post-processor for detecting inner beans as ApplicationListeners.
		beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));

		// Detect a LoadTimeWeaver and prepare for weaving, if found.
		if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
			beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
			// Set a temporary ClassLoader for type matching.
			beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
		}

		// Register default environment beans.
		if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
			beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
		}
		if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
			beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
		}
		if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
			beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
		}
	}

未完待续。

© 著作权归作者所有

共有 人打赏支持
DJZhu
粉丝 3
博文 26
码字总数 30529
作品 0
广州
程序员
私信 提问
深入理解Spring源码(一)-IOC容器的定位,载入,注册

前言:Spring源码继承,嵌套层次非常多,读起来非常容易晕,小伙伴们在看文章的时候一定要跟着文章的思路自己去源码里点一点,看一看,并且多看几次。就会越来越清晰。下面开始正题 1.Spring...

Meet相识_bfa5
2018/05/01
0
0
Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密...

小致dad
2018/08/03
0
0
spring源码-bean之初始化-1

  一、spring的IOC控制反转:控制反转——Spring通过一种称作控制反转(IOC)的技术促进了松耦合。当应用了IOC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建...

小不点丶
2018/08/09
0
0
Spring源码解析系列之IOC容器(一)

前言 实际上我所有的博客都是原来对原来印象笔记里笔记内容的加工,关于Spring源码自己已经解析了很多遍,但是时间长总是忘记,写一篇博客权当加强记忆,也算再次学习下大师们的设计思想,思...

后厂村老司机
2018/06/02
0
0
spring源码解析bean定义加载一

前言 本文转自“天河聊技术”微信公众号 接着上次的spring上下文加载过程的源码解析 正文 上次主要介绍到刷新beanFactory,找到这个方法org.springframework.context.support.AbstractRefre...

天河2018
2018/05/10
0
0

没有更多内容

加载失败,请刷新页面

加载更多

多表查询

第1章 多表关系实战 1.1 实战1:省和市  方案1:多张表,一对多  方案2:一张表,自关联一对多 1.2 实战2:用户和角色 (比如演员和扮演人物)  多对多关系 1.3 实战3:角色和权限 (比如...

stars永恒
今天
1
0
求推广,德邦快递坑人!!!!

完全没想好怎么来吐槽自己这次苦逼的德邦物流过程了,只好来记一个流水账。 从寄快递开始: 2019年1月15日从 德邦物流 微信小app上下单,截图如下: 可笑的是什么,我预约的是17号上门收件,...

o0无忧亦无怖
昨天
6
0
Mac Vim配置

1.升级 vim   我自己 MacBook Pro 的系统还是 10.11 ,其自带的 vim 版本为 7.3 ,我们将其升至最新版: 使用 homebrew : brew install vim --with-lua --with-override-system-vim 这将下...

Pasenger
昨天
7
0
vmware安装Ubuntu上不了网?上网了安装不了net-tools,无法执行ifconfig?

1.重新设置网络适配器还是不行,如下指定nat 2.还需要指定共享网络,我是在无线环境下 3.无法执行ifconfig https://packages.ubuntu.com/bionic/net-tools到这个网站下载net-tools的deb文件...

noob_chr
昨天
3
0
解决SVN:E210007无法协商认证机制

svn:E210007 svn: Cannot negotiate authentication mechanism 执行下面代码即可 sudo yum install cyrus-sasl cyrus-sasl-plain cyrus-sasl-ldap...

临江仙卜算子
昨天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部