1. 目录

[TOC]

2. 说明

梳理源码就从最原始的方式开始,使用xml加入tomcat是最直接引用spring的方式。
之所以不用springboot,虽然它搭建简单,但springboot脚手架为我们做了很多事,引入spring的地方不直观,不利于分析源码。

首先配置web工程,web工程大家都知道是通过tomcat启动的,在web.xml配置参数,调用spring对应的类,这样spring就启动了

3. 依赖关系

4. 源码解析

4.1. tomcat的启动

tomcat的启动流程咱就不分析了,这里主要从Tomcat提供的钩子函数开始。主要配置在web.xml
主要配置有:

  • filter: 配置过滤器
  • listener:配置监听器,这也是spring启动最主要的钩子函数,必须继承javax.servlet.ServletContextListener
  • servlet: 配置servlet启动,这里主要是spring-mvc请求的钩子函数
  • context-param:自定义配置,需要的时候可以从ServletContext中获取
  • welcome-file-list:首页,这个暂时不考虑

启动配置示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>com.kewen.listener.MySessionListener</listener-class>
</listener>
<listener>
<listener-class>com.kewen.listener.MyRequestListener</listener-class>
</listener>
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>com.kewen.MyDispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
<!--<param-value>com.kewen.interceptor.GlobalInterceptor</param-value>-->
<!--<param-value>/WEB-INF/dispatcher-servlet.xml</param-value>-->
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

<welcome-file-list>
<welcome-file>/page/hello.jsp</welcome-file>
</welcome-file-list>
</web-app>

这里配置了监听器org.springframework.web.context.ContextLoaderListener,Tomcat执行到org.apache.catalina.core.StandardContext#listenerStart()方法的时候会执行监听器方法listener.contextInitialized(event);,通过此方法调用起xml自定义配置的监听器,此处配置了spring的类org.springframework.web.context.ContextLoaderListener,从名字可以看出,这是spring的上下文加载监听器,也就是spring上下文初始化的入口函数。

另外,通过<servlet-mapping>标签和<servlet>标签映射浏览器路径对应的servlet类,这里配置的时候直接配置了/路径,即所有路径均对应MyDispaterServlet类,springmvc即通过此配置映射请求,进入处理的,这个后续在解析。

4.2. spring 上下文初始化

我们已经得知,spring用org.springframework.web.context.ContextLoaderListener来初始化容器的,我们看看它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
//Tomcat加载的是通过这个构造器加载的
public ContextLoaderListener() {
}

public ContextLoaderListener(WebApplicationContext context) {
super(context);
}

// 入口钩子函数,继承至tomcat的类ServletContextListener,调起类
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}

@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}

}

4.2.1. 调起spring

tomcat通过无参构造创建类,调起ServletContextListener#contextInitialized()方法,ServletContextListener重写此方法,调用内部方法initWebApplicationContext(),此时正式进入到spring流程,除了ServletContext需要用到外,我们不在关注tomcat的其余东西。

4.2.2. ContextLoaderListener

ContextLoaderListener继承至ContextLoader,自己类本身就只是为了唤起spring的加载流程,唤起了之后主要就是ContextLoader类的职责了,正式开始初始化spring

4.2.3. ContextLoader

ContextLoader 上下文加载器,用于加载spring的全流程。
接下来我们分析ContextLoader源码。

4.2.3.1. ContextLoader#initWebApplicationContext()

我们本着最小代码流程的原则来分析仅启动用到的分支逻辑,对于未用到的暂不分析,这样可以直观的保证整个流程疏通,再扩展将会变得容易。

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {

//判断是不是已经有了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!");
}

//记录日志,不用管
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();

try {

//上下文是空的,因此会走这里实例化上下文逻辑,其内部就根据spring框架中默认的ContextLoader.properties中配置的类来创建
//org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}

//实例化的上下文为WebApplicationContext,继承至ConfigurableWebApplicationContext,因此进入逻辑配置
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
//这儿非活动的,我也不知道啥意思
if (!cwac.isActive()) {
//parent,指的是员阿里的servletContext是否已经有了,这里是没有的。
//说明一下,上下文是可以有父级的,比如springmvc创建的时候会将spring创建的上下文作为父级
if (cwac.getParent() == null) {
//servletContext中也没有,此处是空的
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
//配置和刷新,此方法里面做了主要的配置工作,完成后上下文就已经加载完成了
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
//在servlet中设置已经加载过了,呼应前面最开始的判断
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;
}
}

此处主要创建上下文,然后调用配置和刷新方法configureAndRefreshWebApplicationContext

4.2.3.2. ContextLoader#configureAndRefreshWebApplicationContext()

源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
//设置ID,会进,然后走else,可以不管
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
else {
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}

//将ServletContext设置到context中
wac.setServletContext(sc);
/*
CONFIG_LOCATION_PARAM的值为"contextConfigLocation",是不是很熟,这里就对应了web.xml中的context-param了,相当于设置了context的配置文件路径classpath:applicationContext.xml,这也是spring的配置文件,因此修改配置文件就改context-param的值和classpath下的文件名就好了
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
*/
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}

// 初始化环境变量 ,会进if,
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}

/*
将自定义的ApplicationContextInitializer加入到上下文中,并执行initialize方法
*/
customizeContext(sc, wac);

//刷新上下文,这里的方法也很重要,会将上下文完整的初始化,同时把bean加入容器中,所有的bean生命周期
wac.refresh();
}

4.2.3.3. ContextLoader#customizeContext()

主要加载ApplicationContextInitializer并执行初始化方法initialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {


/*
从web.xml中加载配置contextInitializerClasses
<context-param>
<param-name>contextInitializerClasses</param-name>
<param-value>com.kewen.initializer.MyApplicationContextInitializer</param-value>
</context-param>
*/
List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
determineContextInitializerClasses(sc);

//加载ApplicationContextInitializer
for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
Class<?> initializerContextClass =
GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
throw new ApplicationContextException(String.format(
"Could not apply context initializer [%s] since its generic parameter [%s] " +
"is not assignable from the type of application context used by this " +
"context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
wac.getClass().getName()));
}
this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
}
//根据ordered排序
AnnotationAwareOrderComparator.sort(this.contextInitializers);
//执行初始化方法
for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
initializer.initialize(wac);
}
}

4.2.3.4. XmlWebApplicationContext#refresh()

刷新完成之后spring就正式启动了,关于刷新流程,专门用一节来讲,涉及到整个初始化过程

总结

  1. spring通过ContextLoaderListener实现tomcat的ServletContextListener,从而唤起spring的初始化
  2. ContextLoaderListenerinitWebApplication()方法开始初始化上下文
  3. 初始化上下文中首先会创建上下文,创建的时候首先找servletcontext中有没,实际没有,然后根据spring自带的配置ContextLoader.properties指定webApplication
  4. 初始化完成后从ServletContext中获取父上下文(第一次创建肯定没)
  5. 配置和刷新
    1. servletContext中存储的xml中的配置信息传给上下文(主要是contextConfigLocation:classpath:applicationContext.xml即指定spring配置文件)
    2. 初始化环境变量
    3. 加载springxml中自定义的ApplicationContextInitializer并执行initialize()方法( 从web.xml中加载配置contextInitializerClasses)
  6. 刷新容器