SpringBoot原始碼篇(一):深度分析SpringBoot如何省去web.xml

超級小小黑發表於2019-05-29

一、前言

  從本博文開始,正式開啟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冗餘的問題。

(3)spring3.0時代
  可以使用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開始全面支援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.xml

1、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方法。

 

 1 @HandlesTypes(WebApplicationInitializer.class)
 2 public class SpringServletContainerInitializer implements ServletContainerInitializer {
 3 
 4     /**
 5      * Delegate the {@code ServletContext} to any {@link WebApplicationInitializer}
 6      * implementations present on the application classpath.
 7      * <p>Because this class declares @{@code HandlesTypes(WebApplicationInitializer.class)},
 8      * Servlet 3.0+ containers will automatically scan the classpath for implementations
 9      * of Spring's {@code WebApplicationInitializer} interface and provide the set of all
10      * such types to the {@code webAppInitializerClasses} parameter of this method.
11      * <p>If no {@code WebApplicationInitializer} implementations are found on the classpath,
12      * this method is effectively a no-op. An INFO-level log message will be issued notifying
13      * the user that the {@code ServletContainerInitializer} has indeed been invoked but that
14      * no {@code WebApplicationInitializer} implementations were found.
15      * <p>Assuming that one or more {@code WebApplicationInitializer} types are detected,
16      * they will be instantiated (and <em>sorted</em> if the @{@link
17      * org.springframework.core.annotation.Order @Order} annotation is present or
18      * the {@link org.springframework.core.Ordered Ordered} interface has been
19      * implemented). Then the {@link WebApplicationInitializer#onStartup(ServletContext)}
20      * method will be invoked on each instance, delegating the {@code ServletContext} such
21      * that each instance may register and configure servlets such as Spring's
22      * {@code DispatcherServlet}, listeners such as Spring's {@code ContextLoaderListener},
23      * or any other Servlet API componentry such as filters.
24      * @param webAppInitializerClasses all implementations of
25      * {@link WebApplicationInitializer} found on the application classpath
26      * @param servletContext the servlet context to be initialized
27      * @see WebApplicationInitializer#onStartup(ServletContext)
28      * @see AnnotationAwareOrderComparator
29      */
30     @Override
31     public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
32             throws ServletException {
33 
34         List<WebApplicationInitializer> initializers = new LinkedList<>();
35 
36         if (webAppInitializerClasses != null) {
37             for (Class<?> waiClass : webAppInitializerClasses) {
38                 // Be defensive: Some servlet containers provide us with invalid classes,
39                 // no matter what @HandlesTypes says...
40                 if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
41                         WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
42                     try {
43                         initializers.add((WebApplicationInitializer)
44                                 ReflectionUtils.accessibleConstructor(waiClass).newInstance());
45                     }
46                     catch (Throwable ex) {
47                         throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
48                     }
49                 }
50             }
51         }
52 
53         if (initializers.isEmpty()) {
54             servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
55             return;
56         }
57 
58         servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
59         AnnotationAwareOrderComparator.sort(initializers);
60         for (WebApplicationInitializer initializer : initializers) {
61             initializer.onStartup(servletContext);
62         }
63     }
64 
65 }

 3、WebApplicationInitializer 

檢視 WebApplicationInitializer  介面,這個介面也就是上文中所說的Servlet3.0規範中 @HandlesTypes(WebApplicationInitializer.class) 註解中所指定的感興趣的類。

擷取一段很重要的註釋。這段註釋告訴我們實現該介面的類主要需要實現的功能就是web.xml中配置檔案中配置的內容。

 1 /*
 2  * <servlet>
 3  *   <servlet-name>dispatcher</servlet-name>
 4  *   <servlet-class>
 5  *     org.springframework.web.servlet.DispatcherServlet
 6  *   </servlet-class>
 7  *   <init-param>
 8  *     <param-name>contextConfigLocation</param-name>
 9  *     <param-value>/WEB-INF/spring/dispatcher-config.xml</param-value>
10  *   </init-param>
11  *   <load-on-startup>1</load-on-startup>
12  * </servlet>
13  *
14  * <servlet-mapping>
15  *   <servlet-name>dispatcher</servlet-name>
16  *   <url-pattern>/</url-pattern>
17  * </servlet-mapping>}</pre>
18  *
19  */
20 public interface WebApplicationInitializer {
21     void onStartup(ServletContext servletContext) throws ServletException;
22 }

4、SpringBoot的 WebApplicationInitializer 的實現

檢視SpringBoot  SpringBootServletInitializer 原始碼,該類在spring-boot依賴包中。

仔細看下面的標藍的程式碼。不難發現這正是Servlet容器(tomcat)如何找到SpringBoot並啟動它的。

  1 package org.springframework.boot.web.support;
  2 
  3 import javax.servlet.Filter;
  4 import javax.servlet.Servlet;
  5 import javax.servlet.ServletContext;
  6 import javax.servlet.ServletContextEvent;
  7 import javax.servlet.ServletException;
  8 
  9 import org.apache.commons.logging.Log;
 10 import org.apache.commons.logging.LogFactory;
 11 
 12 import org.springframework.boot.SpringApplication;
 13 import org.springframework.boot.builder.ParentContextApplicationContextInitializer;
 14 import org.springframework.boot.builder.SpringApplicationBuilder;
 15 import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext;
 16 import org.springframework.boot.web.servlet.ServletContextInitializer;
 17 import org.springframework.context.ApplicationContext;
 18 import org.springframework.context.annotation.Configuration;
 19 import org.springframework.core.annotation.AnnotationUtils;
 20 import org.springframework.util.Assert;
 21 import org.springframework.web.WebApplicationInitializer;
 22 import org.springframework.web.context.ContextLoaderListener;
 23 import org.springframework.web.context.WebApplicationContext;
 24 import org.springframework.web.context.support.StandardServletEnvironment;
 25 
 26 /**
 27  * An opinionated {@link WebApplicationInitializer} to run a {@link SpringApplication}
 28  * from a traditional WAR deployment. Binds {@link Servlet}, {@link Filter} and
 29  * {@link ServletContextInitializer} beans from the application context to the servlet
 30  * container.
 31  * <p>
 32  * To configure the application either override the
 33  * {@link #configure(SpringApplicationBuilder)} method (calling
 34  * {@link SpringApplicationBuilder#sources(Object...)}) or make the initializer itself a
 35  * {@code @Configuration}. If you are using {@link SpringBootServletInitializer} in
 36  * combination with other {@link WebApplicationInitializer WebApplicationInitializers} you
 37  * might also want to add an {@code @Ordered} annotation to configure a specific startup
 38  * order.
 39  * <p>
 40  * Note that a WebApplicationInitializer is only needed if you are building a war file and
 41  * deploying it. If you prefer to run an embedded container then you won't need this at
 42  * all.
 43  *
 44  * @author Dave Syer
 45  * @author Phillip Webb
 46  * @author Andy Wilkinson
 47  * @since 1.4.0
 48  * @see #configure(SpringApplicationBuilder)
 49  */
 50 public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
 51 
 52     protected Log logger; // Don't initialize early
 53 
 54     private boolean registerErrorPageFilter = true;
 55 
 56     /**
 57      * Set if the {@link ErrorPageFilter} should be registered. Set to {@code false} if
 58      * error page mappings should be handled via the Servlet container and not Spring
 59      * Boot.
 60      * @param registerErrorPageFilter if the {@link ErrorPageFilter} should be registered.
 61      */
 62     protected final void setRegisterErrorPageFilter(boolean registerErrorPageFilter) {
 63         this.registerErrorPageFilter = registerErrorPageFilter;
 64     }
 65 
 66     @Override
 67     public void onStartup(ServletContext servletContext) throws ServletException {
 68         // Logger initialization is deferred in case a ordered
 69         // LogServletContextInitializer is being used
 70         this.logger = LogFactory.getLog(getClass());
 71         WebApplicationContext rootAppContext = createRootApplicationContext(
 72                 servletContext);
 73         if (rootAppContext != null) {
 74             servletContext.addListener(new ContextLoaderListener(rootAppContext) {
 75                 @Override
 76                 public void contextInitialized(ServletContextEvent event) {
 77                     // no-op because the application context is already initialized
 78                 }
 79             });
 80         }
 81         else {
 82             this.logger.debug("No ContextLoaderListener registered, as "
 83                     + "createRootApplicationContext() did not "
 84                     + "return an application context");
 85         }
 86     }
 87 
 88     protected WebApplicationContext createRootApplicationContext(
 89             ServletContext servletContext) {
 90         SpringApplicationBuilder builder = createSpringApplicationBuilder();
 91         StandardServletEnvironment environment = new StandardServletEnvironment();
 92         environment.initPropertySources(servletContext, null);
 93         builder.environment(environment);
 94         builder.main(getClass());
 95         ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
 96         if (parent != null) {
 97             this.logger.info("Root context already created (using as parent).");
 98             servletContext.setAttribute(
 99                     WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
100             builder.initializers(new ParentContextApplicationContextInitializer(parent));
101         }
102         builder.initializers(
103                 new ServletContextApplicationContextInitializer(servletContext));
104         builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
105         builder = configure(builder);
106         SpringApplication application = builder.build();
107         if (application.getSources().isEmpty() && AnnotationUtils
108                 .findAnnotation(getClass(), Configuration.class) != null) {
109             application.getSources().add(getClass());
110         }
111         Assert.state(!application.getSources().isEmpty(),
112                 "No SpringApplication sources have been defined. Either override the "
113                         + "configure method or add an @Configuration annotation");
114         // Ensure error pages are registered
115         if (this.registerErrorPageFilter) {
116             application.getSources().add(ErrorPageFilterConfiguration.class);
117         }
118         return run(application);
119     }
120 
121     /**
122      * Returns the {@code SpringApplicationBuilder} that is used to configure and create
123      * the {@link SpringApplication}. The default implementation returns a new
124      * {@code SpringApplicationBuilder} in its default state.
125      * @return the {@code SpringApplicationBuilder}.
126      * @since 1.3.0
127      */
128     protected SpringApplicationBuilder createSpringApplicationBuilder() {
129         return new SpringApplicationBuilder();
130     }
131 
132     /**
133      * Called to run a fully configured {@link SpringApplication}.
134      * @param application the application to run
135      * @return the {@link WebApplicationContext}
136      */
137     protected WebApplicationContext run(SpringApplication application) {
138         return (WebApplicationContext) application.run();
139     }
140 
141     private ApplicationContext getExistingRootWebApplicationContext(
142             ServletContext servletContext) {
143         Object context = servletContext.getAttribute(
144                 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
145         if (context instanceof ApplicationContext) {
146             return (ApplicationContext) context;
147         }
148         return null;
149     }
150 
151     /**
152      * Configure the application. Normally all you would need to do is to add sources
153      * (e.g. config classes) because other settings have sensible defaults. You might
154      * choose (for instance) to add default command line arguments, or set an active
155      * Spring profile.
156      * @param builder a builder for the application context
157      * @return the application builder
158      * @see SpringApplicationBuilder
159      */
160     protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
161         return builder;
162     }
163 
164 }

 

 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中的配置內容

 

 1 <web-app>
 2     <!-- 初始化Spring上下文 -->
 3     <listener>
 4         <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 5     </listener>
 6     <!-- 指定Spring的配置檔案 -->
 7     <context-param>
 8         <param-name>contextConfigLocation</param-name>
 9         <param-value>/WEB-INF/app-context.xml</param-value>
10     </context-param>
11     <!-- 初始化DispatcherServlet -->
12     <servlet>
13         <servlet-name>app</servlet-name>
14         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
15         <init-param>
16             <param-name>contextConfigLocation</param-name>
17             <param-value></param-value>
18         </init-param>
19         <load-on-startup>1</load-on-startup>
20     </servlet>
21     <servlet-mapping>
22         <servlet-name>app</servlet-name>
23         <url-pattern>/app/*</url-pattern>
24     </servlet-mapping>
25 </web-app>

 

文件中提供了一個如何使用基於java程式碼的方式配置Servlet容器example

 1 public class MyWebApplicationInitializer implements WebApplicationInitializer {
 2 
 3     @Override
 4     public void onStartup(ServletContext servletCxt) {
 5 
 6         // Load Spring web application configuration
 7         //通過註解的方式初始化Spring的上下文
 8         AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
 9         //註冊spring的配置類(替代傳統專案中xml的configuration)
10         ac.register(AppConfig.class);
11         ac.refresh();
12 
13         // Create and register the DispatcherServlet
14         //基於java程式碼的方式初始化DispatcherServlet
15         DispatcherServlet servlet = new DispatcherServlet(ac);
16         ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
17         registration.setLoadOnStartup(1);
18         registration.addMapping("/app/*");
19     }
20 }

 

 對比官方文件給出的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。

 

本文是筆者查閱大量資料,閱讀大量Spring原始碼總結出來的,原創不易,轉載請註明出處。

如有錯誤請在評論區留言指正。

 參考文獻:

https://blog.csdn.net/adingyb/article/details/80707471

https://www.jianshu.com/p/c6f4df3d720c

相關文章