、前言
從本博文開始,正式開啟Spring及SpringBoot源碼分析之旅。這可能是一個漫長的過程,因為本人之前閱讀源碼都是很片面的,對Spring源碼沒有一個系統的認識。從本文開始我會持續更新,爭取在系列文章更完之后,也能讓自己對Spring源碼有一個系統的認識。
在此立下一個flag,希望自己能夠堅持下去。如果有幸讓您能從系列文章中學到丁點的知識,還請評論,關注,或推薦。如有錯誤還請在評論區指出,一起討論共同成長。
二、SpringBoot誕生的歷史背景
隨着使用 Spring 進行開發的個人和企業越來越多,Spring 也慢慢從一個單一簡潔的小框架變成一個大而全的開源軟件,Spring 的邊界不斷的進行擴充,到了后來 Spring 幾乎可以做任何事情了,市面上主流的開源軟件、中間件都有 Spring 對應組件支持,人們在享用 Spring 的這種便利之后,也遇到了一些問題。Spring 每集成一個開源軟件,就需要增加一些基礎配置,慢慢的隨着人們開發的項目越來越龐大,往往需要集成很多開源軟件,因此后期使用 Spirng 開發大型項目需要引入很多配置文件,太多的配置非常難以理解,並容易配置出錯,到了后來人們甚至稱 Spring 為配置地獄。
Spring 似乎也意識到了這些問題,急需有這么一套軟件可以解決這些問題,這個時候微服務的概念也慢慢興起,快速開發微小獨立的應用變得更為急迫,Spring 剛好處在這么一個交叉點上,於 2013 年初開始的 Spring Boot 項目的研發,2014年4月,Spring Boot 1.0.0 發布。
Spring Boot 誕生之初,就受到開源社區的持續關注,陸續有一些個人和企業嘗試着使用了 Spring Boot,並迅速喜歡上了這款開源軟件。直到2016年,在國內 Spring Boot 才被正真使用了起來,期間很多研究 Spring Boot 的開發者在網上寫了大量關於 Spring Boot 的文章,同時有一些公司在企業內部進行了小規模的使用,並將使用經驗分享了出來。從2016年到2018年,使用 Spring Boot 的企業和個人開發者越來越多。2018年SpringBoot2.0的發布,更是將SpringBoot的熱度推向了一個前所未有的高度。
三、SpringBoot誕生的技術基礎 1、Spring的發展歷史

(1)spring1.0時代
Spring的誕生大大促進了JAVA的發展。也降低了企業java應用開發的技術和時間成本。
(2)spring2.0時代
對spring1.0在繁雜的xml配置文件上做了一定的優化,讓配置看起來越來越簡單,但是並沒語完全解決xml冗余的問題。
對spring1.0在繁雜的xml配置文件上做了一定的優化,讓配置看起來越來越簡單,但是並沒語完全解決xml冗余的問題。
(3)spring3.0時代
可以使用spring提供的java注解來取代曾經xml配置上的問題,似乎我們曾經忘記了發生什么,spring變得前所未有的簡單。Spring3.0奠定了SpringBoot自動裝配的基礎。3.0提供的java注解使得我們可以通過注解的方式來配置spring容器。省去了使用類似於spring-context.xml的配置文件。
可以使用spring提供的java注解來取代曾經xml配置上的問題,似乎我們曾經忘記了發生什么,spring變得前所未有的簡單。Spring3.0奠定了SpringBoot自動裝配的基礎。3.0提供的java注解使得我們可以通過注解的方式來配置spring容器。省去了使用類似於spring-context.xml的配置文件。
同年,Servlet3.0規范的誕生為SpringBoot徹底去掉xml(web.xml)奠定了了理論基礎(對於servlet3.0來說,web.xml不再是必需品。但是Servlet3.0規范還是建議保留web.xml)。
(4)spring4.0時代
4.0 時代我們甚至連xml配置文件都不需要了完全使用java源碼級別的配置與spring提供的注解就能快速的開發spring應用程序,但仍然無法改變Java Web應用程序的運行模式,我們仍然需要將war部署到Web Server 上,才能對外提供服務。
4.0 時代我們甚至連xml配置文件都不需要了完全使用java源碼級別的配置與spring提供的注解就能快速的開發spring應用程序,但仍然無法改變Java Web應用程序的運行模式,我們仍然需要將war部署到Web Server 上,才能對外提供服務。
4.0開始全面支持java8.0
同年,Servlet3.1規范誕生(tomcat8開始采用Servlet3.1規范)。
2、Servlet3.0奠定了SpringBoot 零xml配置的基礎
分析SpringBoot如何省去web.xml還得從Servlet3.0的規范說起。Servlet3.0規范規定如下(摘自穆茂強 張開濤翻譯的Servlet3.1規范,3.0和3.1在這一點上只有一些細節上的變換,在此不做過多介紹):
ServletContainerInitializer類通過jar services API查找。對於每一個應用,應用啟動時,由容器創建一個ServletContainerInitializer 實例。 框架提供的ServletContainerInitializer實現必須綁定在 jar 包 的META-INF/services 目錄中的一個叫做 javax.servlet.ServletContainerInitializer 的文件,根據 jar services API,指定 ServletContainerInitializer 的實現。除 ServletContainerInitializer 外,我們還有一個注解@HandlesTypes。在 ServletContainerInitializer 實現上的@HandlesTypes注解用於表示感興趣的一些類,它們可能指定了 HandlesTypes 的 value 中的注解(類型、方法或自動級別的注解),或者是其類型的超類繼承/實現了這些類之一。無論是否設置了 metadata-complete,@HandlesTypes 注解將應用。當檢測一個應用的類看是否它們匹配 ServletContainerInitializer 的 HandlesTypes 指定的條件時,如果應用的一個或多個可選的 JAR 包缺失,容器可能遇到類裝載問題。由於容器不能決定是否這些類型的類裝載失敗將阻止應用正常工作,它必須忽略它們,同時也提供一個將記錄它們的配置選項。如果ServletContainerInitializer 實現沒有@HandlesTypes 注解,或如果沒有匹配任何指定的@HandlesType,那么它會為每個應用使用 null 值的集合調用一次。這將允許 initializer 基於應用中可用的資源決定是否需要初始化 Servlet/Filter。在任何 Servlet Listener 的事件被觸發之前,當應用正在啟動時,ServletContainerInitializer 的 onStartup 方法將被調用。ServletContainerInitializer’s 的onStartup 得到一個類的 Set,其或者繼承/實現 initializer 表示感興趣的類,或者它是使用指定在@HandlesTypes 注解中的任意類注解的。
這個規范如何理解呢?
簡單來說,當實現了Servlet3.0規范的容器(比如tomcat7及以上版本)啟動時,通過SPI擴展機制自動掃描所有已添加的jar包下的META-INF/services/javax.servlet.ServletContainerInitializer中指定的全路徑的類,並實例化該類,然后回調META-INF/services/javax.servlet.ServletContainerInitializer文件中指定的ServletContainerInitializer的實現類的onStartup方法。 如果該類存在@HandlesTypes注解,並且在@HandlesTypes注解中指定了我們感興趣的類,所有實現了這個類的onStartup方法將會被調用。
再直白一點來說,存在web.xml的時候,Servlet容器會根據web.xml中的配置初始化我們的jar包(也可以說web.xml是我們的jar包和Servlet聯系的中介)。而在Servlet3.0容器初始化時會調用jar包META-INF/services/javax.servlet.ServletContainerInitializer中指定的類的實現(javax.servlet.ServletContainerInitializer中的實現替代了web.xml的作用,而所謂的在@HandlesTypes注解中指定的感興趣的類,可以理解為具體實現了web.xml的功能,當然也可以有其他的用途)。
四、從Spring源碼中分析SpringBoot如何省去web.xml1、META-INF/services/javax.servlet.ServletContainerInitializer
上一節中我們介紹了SpringBoot誕生的技術基礎和Servlet3.0規范。這一章節,我們通過Spring源碼來分析,Spring是如何實現省去web.xml的。
如下圖所示,在org.springframework:spring-web工程下,META-INF/services/javax.servlet.ServletContainerInitializer文件中,指定了將會被Servlet容器啟動時回調的類。

2、SpringServletContainerInitializer
查看 SpringServletContainerInitializer 類的源碼,發現確實如如上文所說,實現了 ServletContainerInitializer ,並且也在 @HandlesTypes 注解中指定了,感興趣的類 WebApplicationInitializer
可以看到onStartup方法上有一大段注釋,翻譯一下大致意思:
servlet 3.0+容器啟動時將自動掃描類路徑以查找實現Spring的webapplicationinitializer接口的所有實現,將其放進一個Set集合中,提供給 SpringServletContainerInitializer onStartup的第一個參數(翻譯結束)。
在Servlet容器初始化的時候會調用 SpringServletContainerInitializer 的onStartup方法,繼續看onStartup方法的代碼邏輯,在該onStartup方法中利用逐個調用webapplicationinitializer所有實現類中的onStartup方法。
[Java]
純文本查看 復制代碼
01
02
03
04
05
06
07
08
09
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
|
@HandlesTypes
(WebApplicationInitializer.
class
)
public
class
SpringServletContainerInitializer
implements
ServletContainerInitializer {
/**
* Delegate the {@code ServletContext} to any {@link WebApplicationInitializer}
* implementations present on the application classpath.
* <p>Because this class declares @{@code HandlesTypes(WebApplicationInitializer.class)},
* Servlet 3.0+ containers will automatically scan the classpath for implementations
* of Spring's {@code WebApplicationInitializer} interface and provide the set of all
* such types to the {@code webAppInitializerClasses} parameter of this method.
* <p>If no {@code WebApplicationInitializer} implementations are found on the classpath,
* this method is effectively a no-op. An INFO-level log message will be issued notifying
* the user that the {@code ServletContainerInitializer} has indeed been invoked but that
* no {@code WebApplicationInitializer} implementations were found.
* <p>Assuming that one or more {@code WebApplicationInitializer} types are detected,
* they will be instantiated (and <em>sorted</em> if the @{@link
* org.springframework.core.annotation.Order @Order} annotation is present or
* the {@link org.springframework.core.Ordered Ordered} interface has been
* implemented). Then the {@link WebApplicationInitializer#onStartup(ServletContext)}
* method will be invoked on each instance, delegating the {@code ServletContext} such
* that each instance may register and configure servlets such as Spring's
* {@code DispatcherServlet}, listeners such as Spring's {@code ContextLoaderListener},
* or any other Servlet API componentry such as filters.
* @param webAppInitializerClasses all implementations of
* {@link WebApplicationInitializer} found on the application classpath
* @param servletContext the servlet context to be initialized
* @see WebApplicationInitializer#onStartup(ServletContext)
* @see AnnotationAwareOrderComparator
*/
@Override
public
void
onStartup(
@Nullable
Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws
ServletException {
List<WebApplicationInitializer> initializers =
new
LinkedList<>();
if
(webAppInitializerClasses !=
null
) {
for
(Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if
(!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.
class
.isAssignableFrom(waiClass)) {
try
{
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch
(Throwable ex) {
throw
new
ServletException(
"Failed to instantiate WebApplicationInitializer class"
, ex);
}
}
}
}
if
(initializers.isEmpty()) {
servletContext.log(
"No Spring WebApplicationInitializer types detected on classpath"
);
return
;
}
servletContext.log(initializers.size() +
" Spring WebApplicationInitializers detected on classpath"
);
AnnotationAwareOrderComparator.sort(initializers);
for
(WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
|
3、WebApplicationInitializer
查看 WebApplicationInitializer 接口,這個接口也就是上文中所說的Servlet3.0規范中 @HandlesTypes(WebApplicationInitializer.class) 注解中所指定的感興趣的類。
截取一段很重要的注釋。這段注釋告訴我們實現該接口的類主要需要實現的功能就是web.xml中配置文件中配置的內容。
[XML]
純文本查看 復制代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/*
* <
servlet
>
* <
servlet-name
>dispatcher</
servlet-name
>
* <
servlet-class
>
* org.springframework.web.servlet.DispatcherServlet
* </
servlet-class
>
* <
init-param
>
* <
param-name
>contextConfigLocation</
param-name
>
* <
param-value
>/WEB-INF/spring/dispatcher-config.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
>}</
pre
>
*
*/
public interface WebApplicationInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
|
4、SpringBoot的 WebApplicationInitializer 的實現
查看SpringBoot SpringBootServletInitializer 源碼,該類在spring-boot依賴包中。
仔細看下面的標藍的代碼。不難發現這正是Servlet容器(tomcat)如何找到SpringBoot並啟動它的。
[Java]
純文本查看 復制代碼
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
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
159
160
161
162
163
164
|
package
org.springframework.boot.web.support;
import
javax.servlet.Filter;
import
javax.servlet.Servlet;
import
javax.servlet.ServletContext;
import
javax.servlet.ServletContextEvent;
import
javax.servlet.ServletException;
import
org.apache.commons.logging.Log;
import
org.apache.commons.logging.LogFactory;
import
org.springframework.boot.SpringApplication;
import
org.springframework.boot.builder.ParentContextApplicationContextInitializer;
import
org.springframework.boot.builder.SpringApplicationBuilder;
import
org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext;
import
org.springframework.boot.web.servlet.ServletContextInitializer;
import
org.springframework.context.ApplicationContext;
import
org.springframework.context.annotation.Configuration;
import
org.springframework.core.annotation.AnnotationUtils;
import
org.springframework.util.Assert;
import
org.springframework.web.WebApplicationInitializer;
import
org.springframework.web.context.ContextLoaderListener;
import
org.springframework.web.context.WebApplicationContext;
import
org.springframework.web.context.support.StandardServletEnvironment;
/**
* An opinionated {@link WebApplicationInitializer} to run a {@link SpringApplication}
* from a traditional WAR deployment. Binds {@link Servlet}, {@link Filter} and
* {@link ServletContextInitializer} beans from the application context to the servlet
* container.
* <p>
* To configure the application either override the
* {@link #configure(SpringApplicationBuilder)} method (calling
* {@link SpringApplicationBuilder#sources(Object...)}) or make the initializer itself a
* {@code @Configuration}. If you are using {@link SpringBootServletInitializer} in
* combination with other {@link WebApplicationInitializer WebApplicationInitializers} you
* might also want to add an {@code @Ordered} annotation to configure a specific startup
* order.
* <p>
* Note that a WebApplicationInitializer is only needed if you are building a war file and
* deploying it. If you prefer to run an embedded container then you won't need this at
* all.
*
* @author Dave Syer
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.4.0
* @see #configure(SpringApplicationBuilder)
*/
public
abstract
class
SpringBootServletInitializer
implements
WebApplicationInitializer {
protected
Log logger;
// Don't initialize early
private
boolean
registerErrorPageFilter =
true
;
/**
* Set if the {@link ErrorPageFilter} should be registered. Set to {@code false} if
* error page mappings should be handled via the Servlet container and not Spring
* Boot.
* @param registerErrorPageFilter if the {@link ErrorPageFilter} should be registered.
*/
protected
final
void
setRegisterErrorPageFilter(
boolean
registerErrorPageFilter) {
this
.registerErrorPageFilter = registerErrorPageFilter;
}
@Override
public
void
onStartup(ServletContext servletContext)
throws
ServletException {
// Logger initialization is deferred in case a ordered
// LogServletContextInitializer is being used
this
.logger = LogFactory.getLog(getClass());
WebApplicationContext rootAppContext = createRootApplicationContext(
servletContext);
if
(rootAppContext !=
null
) {
servletContext.addListener(
new
ContextLoaderListener(rootAppContext) {
@Override
public
void
contextInitialized(ServletContextEvent event) {
// no-op because the application context is already initialized
}
});
}
else
{
this
.logger.debug(
"No ContextLoaderListener registered, as "
+
"createRootApplicationContext() did not "
+
"return an application context"
);
}
}
protected
WebApplicationContext createRootApplicationContext(
ServletContext servletContext) {
SpringApplicationBuilder builder = createSpringApplicationBuilder();
StandardServletEnvironment environment =
new
StandardServletEnvironment();
environment.initPropertySources(servletContext,
null
);
builder.environment(environment);
builder.main(getClass());
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
if
(parent !=
null
) {
this
.logger.info(
"Root context already created (using as parent)."
);
servletContext.setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
null
);
builder.initializers(
new
ParentContextApplicationContextInitializer(parent));
}
builder.initializers(
new
ServletContextApplicationContextInitializer(servletContext));
builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.
class
);
builder = configure(builder);
SpringApplication application = builder.build();
if
(application.getSources().isEmpty() && AnnotationUtils
.findAnnotation(getClass(), Configuration.
class
) !=
null
) {
application.getSources().add(getClass());
}
Assert.state(!application.getSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the "
+
"configure method or add an @Configuration annotation"
);
// Ensure error pages are registered
if
(
this
.registerErrorPageFilter) {
application.getSources().add(ErrorPageFilterConfiguration.
class
);
}
return
run(application);
}
/**
* Returns the {@code SpringApplicationBuilder} that is used to configure and create
* the {@link SpringApplication}. The default implementation returns a new
* {@code SpringApplicationBuilder} in its default state.
* @return the {@code SpringApplicationBuilder}.
* @since 1.3.0
*/
protected
SpringApplicationBuilder createSpringApplicationBuilder() {
return
new
SpringApplicationBuilder();
}
/**
* Called to run a fully configured {@link SpringApplication}.
* @param application the application to run
* @return the {@link WebApplicationContext}
*/
protected
WebApplicationContext run(SpringApplication application) {
return
(WebApplicationContext) application.run();
}
private
ApplicationContext getExistingRootWebApplicationContext(
ServletContext servletContext) {
Object context = servletContext.getAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
if
(context
instanceof
ApplicationContext) {
return
(ApplicationContext) context;
}
return
null
;
}
/**
* Configure the application. Normally all you would need to do is to add sources
* (e.g. config classes) because other settings have sensible defaults. You might
* choose (for instance) to add default command line arguments, or set an active
* Spring profile.
* @param builder a builder for the application context
* @return the application builder
* @see SpringApplicationBuilder
*/
protected
SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return
builder;
}
}
|
5、查看Spring官方文檔
查看Spring 5.0.14官方文檔:https://docs.spring.io/spring/docs/5.0.14.RELEASE/spring-framework-reference/web.html#spring-web
文檔中給出在傳統的springMVC中在web.xml中的配置內容
[XML]
純文本查看 復制代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<
web-app
>
<!-- 初始化Spring上下文 -->
<
listener
>
<
listener-class
>org.springframework.web.context.ContextLoaderListener</
listener-class
>
</
listener
>
<!-- 指定Spring的配置文件 -->
<
context-param
>
<
param-name
>contextConfigLocation</
param-name
>
<
param-value
>/WEB-INF/app-context.xml</
param-value
>
</
context-param
>
<!-- 初始化DispatcherServlet -->
<
servlet
>
<
servlet-name
>app</
servlet-name
>
<
servlet-class
>org.springframework.web.servlet.DispatcherServlet</
servlet-class
>
<
init-param
>
<
param-name
>contextConfigLocation</
param-name
>
<
param-value
></
param-value
>
</
init-param
>
<
load-on-startup
>1</
load-on-startup
>
</
servlet
>
<
servlet-mapping
>
<
servlet-name
>app</
servlet-name
>
<
url-pattern
>/app/*</
url-pattern
>
</
servlet-mapping
>
</
web-app
>
|
文檔中提供了一個如何使用基於java代碼的方式配置Servlet容器example
[Java]
純文本查看 復制代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
public
class
MyWebApplicationInitializer
implements
WebApplicationInitializer {
@Override
public
void
onStartup(ServletContext servletCxt) {
// Load Spring web application configuration
//通過注解的方式初始化Spring的上下文
AnnotationConfigWebApplicationContext ac =
new
AnnotationConfigWebApplicationContext();
//注冊spring的配置類(替代傳統項目中xml的configuration)
ac.register(AppConfig.
class
);
ac.refresh();
// Create and register the DispatcherServlet
//基於java代碼的方式初始化DispatcherServlet
DispatcherServlet servlet =
new
DispatcherServlet(ac);
ServletRegistration.Dynamic registration = servletCxt.addServlet(
"app"
, servlet);
registration.setLoadOnStartup(
1
);
registration.addMapping(
"/app/*"
);
}
}
|
對比官方文檔給出的example,不難發現上面這段java代碼就是SpringBoot省去web.xml的具體實現方法。上面 MyWebApplicationInitializer 正是 WebApplicationInitializer ( @HandlesTypes(WebApplicationInitializer.class) ) 接口的實現。
官方文檔提供的 MyWebApplicationInitializer 類正是SpringBoot不依賴與web.xml的關鍵代碼。
SpringBoot中具體實現web.xml中配置的代碼沒有官方文檔中的example這么簡單,SpringBoot中具體初始化 DispatcherServlet 的類是 DispatcherServletAutoConfiguration 。感興趣的話可以斷點調試一下。
五、總結
以上章節介紹了SpringBoot誕生的歷史背景,每一個新技術的誕生,都是場景驅動的。然后介紹了SpringBoot能做到不依賴web.xml的技術條件。最后通過源碼分析了SpringBoot中具體的實現。
下一篇博文將利用本文講到的知識基於Spring springframework內置tomcat簡單模擬SpringBoot的基本功能。簡單說就是實現一個簡易版的SpringBoot。
更多免費技術資料可關注:annalin1203