一、前言
從本博文開始,正式開啟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