SpringBoot源碼篇:深度分析SpringBoot如何省去web.xml

1、前言

  從本博文開始,正式開啓Spring及SpringBoot源碼分析之旅。這多是一個漫長的過程,由於本人以前閱讀源碼都是很片面的,對Spring源碼沒有一個系統的認識。從本文開始我會持續更新,爭取在系列文章更完以後,也能讓本身對Spring源碼有一個系統的認識。html

  在此立下一個flag,但願本身可以堅持下去。若是有幸讓您能從系列文章中學到丁點的知識,還請評論,關注,或推薦。若有錯誤還請在評論區指出,一塊兒討論共同成長。java

2、SpringBoot誕生的歷史背景

   隨着使用 Spring 進行開發的我的和企業愈來愈多,Spring 也慢慢從一個單一簡潔的小框架變成一個大而全的開源軟件,Spring 的邊界不斷的進行擴充,到了後來 Spring 幾乎能夠作任何事情了,市面上主流的開源軟件、中間件都有 Spring 對應組件支持,人們在享用 Spring 的這種便利以後,也遇到了一些問題。Spring 每集成一個開源軟件,就須要增長一些基礎配置,慢慢的隨着人們開發的項目愈來愈龐大,每每須要集成不少開源軟件,所以後期使用 Spirng 開發大型項目須要引入不少配置文件,太多的配置很是難以理解,並容易配置出錯,到了後來人們甚至稱 Spring 爲配置地獄。web

  Spring 彷佛也意識到了這些問題,急需有這麼一套軟件能夠解決這些問題,這個時候微服務的概念也慢慢興起,快速開發微小獨立的應用變得更爲急迫,Spring 恰好處在這麼一個交叉點上,於 2013 年初開始的 Spring Boot 項目的研發,2014年4月,Spring Boot 1.0.0 發佈。spring

  Spring Boot 誕生之初,就受到開源社區的持續關注,陸續有一些我的和企業嘗試着使用了 Spring Boot,並迅速喜歡上了這款開源軟件。直到2016年,在國內 Spring Boot 才被正真使用了起來,期間不少研究 Spring Boot 的開發者在網上寫了大量關於 Spring Boot 的文章,同時有一些公司在企業內部進行了小規模的使用,並將使用經驗分享了出來。從2016年到2018年,使用 Spring Boot 的企業和我的開發者愈來愈多。2018年SpringBoot2.0的發佈,更是將SpringBoot的熱度推向了一個史無前例的高度。apache

3、SpringBoot誕生的技術基礎

 一、Spring的發展歷史

(1)spring1.0時代tomcat

   Spring的誕生大大促進了JAVA的發展。也下降了企業java應用開發的技術和時間成本。app

(2)spring2.0時代
  對spring1.0在繁雜的xml配置文件上作了必定的優化,讓配置看起來愈來愈簡單,可是並沒語徹底解決xml冗餘的問題。框架

(3)spring3.0時代
  可使用spring提供的java註解來取代曾經xml配置上的問題,彷佛咱們曾經忘記了發生什麼,spring變得史無前例的簡單。Spring3.0奠基了SpringBoot自動裝配的基礎。3.0提供的java註解使得咱們能夠經過註解的方式來配置spring容器。省去了使用相似於spring-context.xml的配置文件。webapp

  同年,Servlet3.0規範的誕生爲SpringBoot完全去掉xml(web.xml)奠基了了理論基礎(對於servlet3.0來講,web.xml再也不是必需品。可是Servlet3.0規範仍是建議保留web.xml)。ide

(4)spring4.0時代
  4.0 時代咱們甚至連xml配置文件都不須要了徹底使用java源碼級別的配置與spring提供的註解就能快速的開發spring應用程序,但仍然沒法改變Java Web應用程序的運行模式,咱們仍然須要將war部署到Web Server 上,才能對外提供服務。

  4.0開始全面支持java8.0

  同年,Servlet3.1規範誕生(tomcat8開始採用Servlet3.1規範)。

  二、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的功能,固然也能夠有其餘的用途)。

4、從Spring源碼中分析SpringBoot如何省去web.xml

一、META-INF/services/javax.servlet.ServletContainerInitializer

上一節中咱們介紹了SpringBoot誕生的技術基礎和Servlet3.0規範。這一章節,咱們經過Spring源碼來分析,Spring是如何實現省去web.xml的。

以下圖所示,在org.springframework:spring-web工程下,META-INF/services/javax.servlet.ServletContainerInitializer文件中,指定了將會被Servlet容器啓動時回調的類。

二、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 }

 三、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 }

四、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 }

 

 五、查看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 。感興趣的話能夠斷點調試一下。

 

5、總結

以上章節介紹了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

相關文章
相關標籤/搜索