概述

使用Spring 开发已经有一段时间了,偶尔会有个疑问,我们的工程从什么时候开始执行的?然后就在网上不停的搜索相关的资料, 然而网上更多的文章是讲解spring ioc的,再深入一点的就是讲解web.xml的contextLoaderListener,然后讲解也是虎头蛇尾, 模棱两可,经历了阵痛之后,忽然想到我们的spring工程都是要运行在tomcat中,那么应该是在tomcat中唤醒的吧,网上一搜果然如此, 因此花了点事件来看了一下Tomcat的源码,既然是java工程,那么应该是有个main函数作为启动整个应用的入口,该入口就在BootStrap类中

代码走读

万物之始-main函数

无论多么复杂的工程,总是有一个入口吧,对于java工程来说,大概就是我们的main函数了吧,嗯,进去看一下:

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
public static void main(String args[]) {

if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
}
.....
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to prevent
// a range of class not found exceptions.
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}

try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}....

if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
} .......
} ......

}

上面我们省去了大部分代码,由于我们现在只关心tomcat的启动,因此也把tomcat的其他的操作选项都去掉了。整个启动流程做了一下几件事:

  • 创建bootstrap对象,并调用init方法初始化类加载器,其中做的最重要的事情就是通过反射的方式创建了一个Catalina对象,并将其设置为catalinaDeamon,具体代码如下:
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
public void init() throws Exception {

// 初始化容器中要用到的类加载器
initClassLoaders();

// 既然是上下文,那么必然是整个应用公共的变量,设置公共变量的类加载器
Thread.currentThread().setContextClassLoader(catalinaLoader);

// 使用catalinaLoader预加载对应的类
SecurityClassLoad.securityClassLoad(catalinaLoader);

// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");

// 加载catalina实现类并实例化catalina对象
Class<?> startupClass =
catalinaLoader.loadClass
("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.newInstance();

// Set the shared extensions class loader
// 设置Catalina父类加载器
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);

catalinaDaemon = startupInstance;

}

上面代码主要部分都已经注释了,最核心的就是initClassLoaders()构建了类加载器的父子关系用于实现不同应用不同jar包的命名空间的隔离,并通过反射实例化了Catalina对象,而该对象持有一个变量server(还没有初始化,初始化的过程是在加载解析server.xml的过程中做的)。

  • bootstrap类的load方法最终会通过反射调用catalina的load方法,该方法主要是解析server.xml并创建容器,并将根结点对应的容器设置为catalina的server变量,对应的server.xml文件如下:

    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
    <?xml version='1.0' encoding='utf-8'?>
    <Server port="8005" shutdown="SHUTDOWN">
    <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
    <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
    <!-- Prevent memory leaks due to use of particular java/javax APIs-->
    <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
    <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
    <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

    <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
    type="org.apache.catalina.UserDatabase"
    description="User database that can be updated and saved"
    factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
    pathname="conf/tomcat-users.xml" />
    </GlobalNamingResources>
    <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1"
    connectionTimeout="20000"
    redirectPort="8443" />
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">
    <Realm className="org.apache.catalina.realm.LockOutRealm">
    <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
    resourceName="UserDatabase"/>
    </Realm>

    <Host name="localhost" appBase="webapps"
    unpackWARs="true" autoDeploy="true">
    <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
    prefix="localhost_access_log" suffix=".txt"
    pattern="%h %l %u %t &quot;%r&quot; %s %b" />

    </Host>
    </Engine>
    </Service>
    </Server>
对应解析、初始化代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void load() {

。。。。。。。。。。。
// Create and execute our Digester
// 创建解析server.xml文件的解析器
Digester digester = createStartDigester();

。。。。。。。。。。。

try {
inputSource.setByteStream(inputStream);
digester.push(this);
digester.parse(inputSource);
} 。。。。。。。。。
// Start the new server
try {
getServer().init();
} 。。。。。。
}

已省略不重要的部分,上述load的过程大致可以分成:

- 创建digester解析起用于解析对应的server.xml文件,并创建文件中所包含的容器
- 获取catalina对应的server并执行init方法进行初始化,对应的init方法如下:

1
2
3
4
5
6
7
8
9
10
    @Override
public final synchronized void init() throws LifecycleException {
........
setStateInternal(LifecycleState.INITIALIZING, null, false);

try {
initInternal();
} ........
setStateInternal(LifecycleState.INITIALIZED, null, false);
}
可以看到上述对应容器初始化的过程中首先是设置容器的状态,这个操作会使得当前容器对应的listener监听到当前容器状态的数据,进而唤醒内层容器初始化的过程,最终通过层层的递归调用到最内层容器状态的设置,然后再依次由内而外的调用相关容器的`initInternal`方法,值得一提的是`initInternal`是采用了模板的模式,具体的实现是放到了不同的容器中。 上述递归调用的最内层的容器就是`StandardContext`,该容器的初始化会触发ContextConfig的方法调用,其中ContextConfig也是listener的一种实现,有兴趣的话可以查询相关类的继承关系。如下为ContextConfig接收到相关消息之后的处理方法调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void lifecycleEvent(LifecycleEvent event) {
........
// Process the event that has occurred
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
configureStart();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
// Restore docBase for management tools
if (originalDocBase != null) {
context.setDocBase(originalDocBase);
}
} else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
configureStop();
} else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
init();
} else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
destroy();
}

}
上面的方法我们主要关注`configureStart`方法即可,该方法比较长,最终会进入核心方法`webConfig`的调用:
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
protected void webConfig() {
/*
* Anything and everything can override the global and host defaults.
* This is implemented in two parts
* - Handle as a web fragment that gets added after everything else so
* everything else takes priority
* - Mark Servlets as overridable so SCI configuration can replace
* configuration from the defaults
*/

/*
* The rules for annotation scanning are not as clear-cut as one might
* think. Tomcat implements the following process:
* - As per SRV.1.6.2, Tomcat will scan for annotations regardless of
* which Servlet spec version is declared in web.xml. The EG has
* confirmed this is the expected behaviour.
* - As per http://java.net/jira/browse/SERVLET_SPEC-36, if the main
* web.xml is marked as metadata-complete, JARs are still processed
* for SCIs.
* - If metadata-complete=true and an absolute ordering is specified,
* JARs excluded from the ordering are also excluded from the SCI
* processing.
* - If an SCI has a @HandlesType annotation then all classes (except
* those in JARs excluded from an absolute ordering) need to be
* scanned to check if they match.
*/
WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
context.getXmlValidation(), context.getXmlBlockExternal());

Set<WebXml> defaults = new HashSet<>();
defaults.add(getDefaultWebXmlFragment(webXmlParser));

WebXml webXml = createWebXml();

// Parse context level web.xml
InputSource contextWebXml = getContextWebXmlSource();
if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
ok = false;
}

ServletContext sContext = context.getServletContext();

// Ordering is important here

// Step 1. Identify all the JARs packaged with the application and those
// provided by the container. If any of the application JARs have a
// web-fragment.xml it will be parsed at this point. web-fragment.xml
// files are ignored for container provided JARs.
Map<String,WebXml> fragments = processJarsForWebFragments(webXml, webXmlParser);

// Step 2. Order the fragments.
Set<WebXml> orderedFragments = null;
orderedFragments =
WebXml.orderWebFragments(webXml, fragments, sContext);

// Step 3. Look for ServletContainerInitializer implementations
// 核心方法:扫描所有的jar包,并收集ServletContainerInitializer相关的实现类,在收集的过程中会实现了该接口类上面@HandleTypes中指定的类相关的信息,最终形成一个Map<SCI,Class<handletypes>>的map集合,这个集合尤其重要,会在后面看到如何唤醒web应用的
if (ok) {
processServletContainerInitializers();
}

if (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
// Step 4. Process /WEB-INF/classes for annotations and
// @HandlesTypes matches
Map<String,JavaClassCacheEntry> javaClassCache = new HashMap<>();

if (ok) {
WebResource[] webResources =
context.getResources().listResources("/WEB-INF/classes");

for (WebResource webResource : webResources) {
processAnnotationsWebResource(webResource, webXml,
webXml.isMetadataComplete(), javaClassCache);
}
}

// Step 5. Process JARs for annotations for annotations and
// @HandlesTypes matches - only need to process those fragments we
// are going to use (remember orderedFragments includes any
// container fragments)
if (ok) {
processAnnotations(
orderedFragments, webXml.isMetadataComplete(), javaClassCache);
}

// Cache, if used, is no longer required so clear it
javaClassCache.clear();
}

if (!webXml.isMetadataComplete()) {
// Step 6. Merge web-fragment.xml files into the main web.xml
// file.
if (ok) {
ok = webXml.merge(orderedFragments);
}

// Step 7. Apply global defaults
// Have to merge defaults before JSP conversion since defaults
// provide JSP servlet definition.
webXml.merge(defaults);

// Step 8. Convert explicitly mentioned jsps to servlets
if (ok) {
// 收集包含的jsp相关的信息,并缓存起来,该过程不涉及对应jsp对象的创建
convertJsps(webXml);
}

// Step 9. Apply merged web.xml to Context
if (ok) {
configureContext(webXml);
}
} else {
webXml.merge(defaults);
convertJsps(webXml);
configureContext(webXml);
}

if (context.getLogEffectiveWebXml()) {
log.info("web.xml:\n" + webXml.toXml());
}

// Always need to look for static resources
// Step 10. Look for static resources packaged in JARs
// 寻找jar包中的静态资源,只是把jar保重的resource加载并保存进来
if (ok) {
// Spec does not define an order.
// Use ordered JARs followed by remaining JARs
Set<WebXml> resourceJars = new LinkedHashSet<>();
for (WebXml fragment : orderedFragments) {
resourceJars.add(fragment);
}
for (WebXml fragment : fragments.values()) {
if (!resourceJars.contains(fragment)) {
resourceJars.add(fragment);
}
}
processResourceJARs(resourceJars);
// See also StandardContext.resourcesStart() for
// WEB-INF/classes/META-INF/resources configuration
}

// Step 11. Apply the ServletContainerInitializer config to the
// context
// 这一步就会将步骤3中收集到的SCI以及注解包含的类给纳管到context中
if (ok) {
for (Map.Entry<ServletContainerInitializer,
Set<Class<?>>> entry :
initializerClassMap.entrySet()) {
if (entry.getValue().isEmpty()) {
context.addServletContainerInitializer(
entry.getKey(), null);
} else {
context.addServletContainerInitializer(
entry.getKey(), entry.getValue());
}
}
}
}
该方法比较长,我们仅在关键地方做相关的注释,这里最重要的一步操作就是扫描jar包获取SCI的实现类以及类上使用@HandleType注解指定的类。

上述整个过程都是在listener中进行的,执行完之后就回归到我们的主流程StandardContext中的initInternal();方法了,该方法没有什么意义,因此略过。

  • 回到上面的主要流程还差一步调用bootstrap的start方法启动容器,该方法最终会走到Catalina中的start方法,如下:
1
2
3
4
5
6
public void start() {
.........
// Start the new server
try {
getServer().start();
} c.......

核心方法只有一个,就是获取对应的server并调用其start方法,该方法具体如下:

1
2
3
4
5
6
7
public final synchronized void start() throws LifecycleException {
.....
setStateInternal(LifecycleState.STARTING_PREP, null, false);

try {
startInternal();
} .......

最终调用了startInternal()方法,该方法的核心操作就是会从外到内依次调用相关容器的start方法,而start方法又会调用内层的容器的startInternal()方法,最终走到StandardContext中,对应startInternal()方法中包含了一行最重要的代码:

1
2
3
4
5
6
7
8
9
10
11
12
// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
entry.getKey().onStartup(entry.getValue(),
getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}

上面这一步会依次获取前面容器的SCI并调用这些实现了SCI接口实现类的onStartup方法,该方法接收的参数是SCI接口实现类上面声明的@HandleType所标注的类。这么说会有点抽象,我们还是以一个具体的web应用的例子来演示一下,下面搭建一个springmvc+tomcat的web应用,查看SCI在工程中的实现,可以知道其实现在spring-web下,对应代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
。。。。。。
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}

}

分析该代码可以知道,SCI最终会调用WebApplicationInitializer实现类的onStartup方法,那我们看一下WebApplicationInitializer究竟有那些实现类,可以找到两个核心的类,分别是:AbstractContextLoaderInitializerAbstractDispatcherServletInitializer这两个类中的onStartup会分别初始化ContextLoaderListener、DispatcherServlet,其中ContextLoaderListener涉及到springmvc中容器的初始化,DispatcherServlet则涉及前段请求相关资源的初始化,最终就完成了整个web应用的初始化,并可以处理来自客户端的请求。 如下ContextLoaderListener中对应的容器的初始化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}

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

public void contextInitialized(ServletContextEvent event) {
this.initWebApplicationContext(event.getServletContext());
}

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

我们在上面看到了this.initWebApplicationContext(event.getServletContext());这一行代码,继续跟进去发现会使用contextConfigLocation创建一个 applicationContext对象,也就是初始化我们的spring容器了,这也就衔接了我们之前的spring ioc代码及流程的分析。

总结

上面仅仅是代码的走读,后面如果有时间还需要好好地细读一下。