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版本的内容