spring之我见--从spring的启动到ioc容器的创建

spring是JAVA人可能用的最多的框架之一,我也很遗憾在面试时不止一次被问到spring原理问题时而语塞,也下定决心开了新的一个专题,spring之我见,用“之我见”三个字是为了严谨,因为读源码对于我来说不是简单的活儿,搞不好就是理解错误,所以自我勉励吧。

从spring启动谈起

spring 和 spring boot 在启动上还是有区别的,我先以spring4.3为准绳。

我先用idea生成了一个springMVC 项目,web.xml 是首要的配置文件

//这个listener 是 spring ioc 的启动核心,如果你不用spring的ioc,你可以不配置
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    //DispatcherServlet 是 springmvc的前端控制总线,如果你不用spring的controller,你可以不配置,这部分不在我这篇文章的讨论范围
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>
配置ContextLoaderListener的原因

ContextLoaderListener(spring中的类)继承ContextLoader(spring中的类),并实现ServletContextListener(servlet中的接口),ServletContextListener是容器里的类,我用的是tomcat,所以这个类在tomcat的lib里

我们知道,当我们启动tomcat的时候,spring也随之启动,为什么呢?因为通过ContextLoaderListener监听,ContextLoaderListener是作为启动spring的入口。可能这样说还是有点懵,我们先介绍下ServletContext。

1.1.1 ServletContext

ServletContext,Servlet容器在启动时会加载Web应用,并为每个Web应用创建唯一的ServletContext对象。可以把ServletContext看作一个Web应用的服务器端组件的共享内存。在ServletContext中可以存放共享数据。

web.xml新增下面servlet配置

<servlet>
    <servlet-name>TestServlet</servlet-name>
    <servlet-class>TestServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>TestServlet</servlet-name>
    <url-pattern>/testServlet</url-pattern>
</servlet-mapping>
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TestServlet extends HttpServlet {


    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("初始化 TestServlet,在第一次访问的时候初始化");
        super.init(config);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {

        // 获得ServletContext的引用
        ServletContext context = getServletContext();

        // 从ServletContext读取count属性
        Integer count = (Integer) context.getAttribute("count");

        String contextConfigLocation = context.getInitParameter("contextConfigLocation");

        System.out.println("web.xml 中配的context-param 属性contextConfigLocation : " + contextConfigLocation);

        // 如果没有读到count属性,那么创建count属性,并设置初始值为0
        if (count == null) {
            System.out.println("context中还没有count属性呢");
            count = new Integer(0);
            context.setAttribute("count", count);
        }
        count = count + 1;
        // count增加之后还要写回去,引用为什么还要重新存回去
        context.setAttribute("count", count);
        System.out.println("您是第" + count + "个访问的!");

    }

    @Override
    public void destroy() {
        super.destroy();
    }

}

这里写图片描述

从上图看出,该系统的ServletContext是一个ApplicationContextFacade实例,位于tomcat包下。

再从上述代码中可见通过getServletContext()方法可以直接获得ServletContext的引用。好了,到此我们知道了ServletContext是每个web应用必须的,随着tomcat启动而有一个ServletContext实例,而该对象又有一个ServletContextListener的接口,监视ServletContext的创建,这样就可以调用这个接口的回调方法来启动Spring容器了。

1.1.2 ServletContextListener

从一开始的web.xml中我们配置了一个listener ,并且 上节阐述了ServletContext和spring的关系,我们有必要看看ContextLoaderListener是帮助spring启动的,但是在此之前我们得看看ContextLoaderListener的接口,ServletContextListener,了解它方法的定义。

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

先看接口定义

package javax.servlet;

import java.util.EventListener;

/**
 * Implementations of this interface receive notifications about changes to the
 * servlet context of the web application they are part of. To receive
 * notification events, the implementation class must be configured in the
 * deployment descriptor for the web application.
 */

public interface ServletContextListener extends EventListener {

    /**
    * 通知在web程序初始化的时候开始,所有的ServletContextListeners都会在
    * web应用中任何的filter和servlet初始化之前接收到context初始化的时候通知
     ** Notification that the web application initialization process is starting.
     * All ServletContextListeners are notified of context initialization before
     * any filter or servlet in the web application is initialized.
     * @param sce Information about the ServletContext that was initialized
     */
    public void contextInitialized(ServletContextEvent sce);

    /**
     ** Notification that the servlet context is about to be shut down. All
     * servlets and filters have been destroy()ed before any
     * ServletContextListeners are notified of context destruction.
     */
    public void contextDestroyed(ServletContextEvent sce);
}

我们看注释知道 web程序初始化的时候,contextInitialized()方法是一定会调用的,也就是为什么spring会随着web系统一起启动啦,那我们再看一下ContextLoaderListener的contextInitialized方法做了些什么

	/**
	 * Initialize the root web application context.
	 */
	@Override
	public void contextInitialized(ServletContextEvent event) {
		initWebApplicationContext(event.getServletContext());
	}

里面调用了initWebApplicationContext方法,我们进去继续往里看

/**
	为给定的ServletContext初始化Spring容器
	 * Initialize Spring's web application context for the given servlet context,
	 * using the application context provided at construction time, or creating a new one
	 * according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and
	 * "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.
	 * @param servletContext current servlet context
	 * @return the new WebApplicationContext
	 * @see #ContextLoader(WebApplicationContext)
	 * @see #CONTEXT_CLASS_PARAM
	 * @see #CONFIG_LOCATION_PARAM
	 */
	public WebApplicationContext initWebApplicationContext(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 {
			// Store context in local instance variable, to guarantee that
			// it is available on ServletContext shutdown.
			if (this.context == null) {
				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);
				}
			}
			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;
		}
	}

initWebApplicationContext方法是在为spring创建一个上下文,也就是spring核心组件,ApplicationContext,它贯穿整个spring生命周期, 包括IOC容器,说到IOC容器,它存储了所有我们要托管给他们的bean实例,但是这里存的又不是实体,而是元数据BeanDefinition(描述该类一切相关信息的实体),需要的时候就可以通过getBean()去反射创建出它的实体,这也就是为什么标题是从启动到 容器的创建,因为这个过程是无缝连接的。

1.1.3ServletContext 和 ApplicationContext的区别

ApplicationContext是spring的核心,Context通常解释为上下文环境,用“容器”来表述更容易理解一些,ApplicationContext则是“应用的容器了”了。

ServletContext 是Servlet与Servlet容器之间直接通信的接口,Servlet容器在启动一个web应用时,会为它创建一个ServletContext对 象,每个web应用有唯一的ServletContext对象,同一个web应用的所有Servlet对象共享一个 ServletContext,Servlet对象可以通过它来访问容器中的各种资源。

Spring boot 的流程

上面是spring 老的启动流程,当我们迈入spring boot时代后,tomcat等容器被spring当成了内置组件来使用,启动的顺序也发生了改变,流程也不依赖ContextLoaderListener了。我们简单看看spring boot 与 tomcat是怎么关联启动的。

spring boot的启动直接从一个main方法开始,这个就不多说了。下面重要的方法就是createApplicationContext方法。这个方法创建了一个ApplicationContext实例。

接下来就在refreshContext(context)里面执行ApplicationContext的refresh方法,这个方法涵盖了spring ioc容器的启动过程。里面的onRefresh方法主要做的事 就在启动初始化tomcat容器,和将DispatcherServlet放入tomcat的servlet容器中。

public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

这部分有一篇比较好的文章可以细看。
Spring Boot与Spring MVC集成启动过程源码分析

IOC容器的创建

到了这一章,其实我不自己写了,因为有一篇文章已经是从源码角度 仔细剖析了IOC容器从定位,载入、解析和依赖注入已经全部分析完毕。是难得的好文,看他的文章已经可以足够理解IOC容器相关的知识:

http://cmsblogs.com/?p=4047

更新

  • 2019.11.04 更新springboot版本的内容
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值