
作者:陌北有棵樹,Java人,架構師社區合伙人! 【一】總述SpringBoot的誕生,極大的簡化了Spring框架的使用過程,提升了開發效率,可以把它理解為一個整合包,使用了SpringBoot,就可以不用自己去進行繁瑣的配置,通過幾個簡單的注解,就可以構建一個基于REST的服務。同時,SpringBoot的快速構建部署的特性,為當下大熱的微服務落地提供了極大的便利,可以說是構建微服務的理想框架。 歸納來說SpringBoot的特性有如下幾點: 自動配置 內置tomcat、jetty、undertow 三大web容器 將Web應用打成jar包啟動
那么SpringBoot是怎樣做到上述三個特性的呢?是我接下來的研究方向,本篇主要研究的是后兩個特點,如何內嵌了Web容易,將應用打成jar包,怎么還能像Web程序一樣運行。 本文是筆者患難與共的好兄弟Dewey Ding及筆者Debug了若干天的成果,謹以此篇留作紀念。 【二】問題引出和總體思路按照常規的Web容器的啟動方式,明顯是無法和SpringBoot這種jar包的運行方式兼容的,那么他們之間是如何做到無縫銜接的合作呢? 最終運行的依然是SpringMVC框架,那么SpringBoot又是如何做到在內置了Tomcat的同時,又和SpringMVC無縫銜接的呢? 綜上所述,整個系列需要研究的技術點如下(本文并未完全覆蓋): SpringBoot啟動Tomcat SpringMVC初始化DispatcherServlet Tomcat的兩種啟動方式 Tomcat在SpringBoot中如何拿到配置
【三】SpringBoot啟動過程具體分析在SpringBoot中,一個Web應用從啟動到接收請求,我粗略將它分為四步: SpringBoot初始化 Tomcat初始化 Tomcat接收請求 SpringMVC初始化
這一部分的學習真可謂一波三折,每次Debug SpringApplication的run方法,都會迷失在茫茫的源碼之中,看書和博客也都是云里霧里,所以這次決定換一種學法,先從宏觀上了解都要做什么,至于具體細節,等到需要的時候再去分析。比如今天要了解的是和Tomcat啟動相關的部分,那么就先只了解這個模塊。 關于SpringBoot和Tomcat是如何合作的,在實際Debug之前,我們先拋出如下幾個問題: SpringBoot有main()方法,可以直接打成jar包運行;SpringMVC沒有main方法,所以無法自己啟動,要依賴Tomcat,Tomcat本質是個容器,所以Tomcat中一定有main方法或者線程的start()方法來啟動Spring程序 /WEB-INF是Web應用需要的,SpringMVC配置了這個,是為了Tomcat讀取,從而啟動Web容器 項目要部署到webapp目錄下,才能被Tomcat運行 那么問題來了,SpringBoot沒有做這些配置,是怎么做到內置Tomcat容器,并讓Tomcat啟動的呢?
【SpringBoot和Tomcat的初始化】我們先來看Tomcat的啟動時在SpringBoot啟動的哪一步?這里只列舉比較關鍵的幾步: SpringApplication的run方法 org.springframework.boot.SpringApplication#run(java.lang.String...)
刷新IOC容器(Bean的實例化) org.springframework.boot.SpringApplication#refreshContext() org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh()
創建WebServer 在org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh調用了createWebServer org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#createWebServer()
private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { // 重點:這時初始化了dispatcherServlet ServletWebServerFactory factory = getWebServerFactory(); // 創建 this.webServer = factory.getWebServer(getSelfInitializer()); } else if (servletContext != null) { try { // 啟動 getSelfInitializer().onStartup(servletContext); } catch (ServletException ex) { throw new ApplicationContextException('Cannot initialize servlet context', ex); } } initPropertySources(); }
這里就是Tomcat的創建和啟動過程了,關于下面這兩行代碼中,蘊含著我們尚未去發現的秘密,后面會繼續分析(蘊含著的秘密真的坑苦了我們,竟然與它擦肩而過,然后繞了一大圈才回來,淚奔中~~): this.webServer = factory.getWebServer(getSelfInitializer()); ······ getSelfInitializer().onStartup(servletContext);
在這里創建了Tomcat,Connector,Host,Engine并且設置一些屬性,關于Tomcat的具體內容,由于內容比較多,就不在此篇詳細展開,后續會專門研究。這里我們發現,在Tomcat啟動的時候,servletClass還沒有獲取到dispatcherServlet,而在第一次收到請求時,servletClass就變成了dispatcherServlet,這里就對我們形成了一定誤導,以為是第一次收到請求時Tomcat才加載了默認的wrapper,后來才發現出現了偏差,經歷了無數次斷點后,才回到正確的路上。 

這里不得不提到,一個最開始被我們忽略,后來才發現是個重要的地方:getSelfInitializer() 。關于SpringBoot是如何把“/”和“dispatcherServlet”的關聯給到Tomcat這件事,就蘊含在下面這段代碼之中: //注意 selfInitialize private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() { return this::selfInitialize; }
//注意 getServletContextInitializerBeans() private void selfInitialize(ServletContext servletContext) throws ServletException { prepareWebApplicationContext(servletContext); registerApplicationScope(servletContext); WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext); for (ServletContextInitializer beans : getServletContextInitializerBeans()) { beans.onStartup(servletContext); } }
//注意 ServletContextInitializerBeans() protected Collection<ServletContextInitializer> getServletContextInitializerBeans() { return new ServletContextInitializerBeans(getBeanFactory()); }
// 注意 this.initializers 和 addServletContextInitializerBeans public ServletContextInitializerBeans(ListableBeanFactory beanFactory, Class<? extends ServletContextInitializer>... initializerTypes) { this.initializers = new LinkedMultiValueMap<>(); this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes) : Collections.singletonList(ServletContextInitializer.class); addServletContextInitializerBeans(beanFactory); addAdaptableBeans(beanFactory); List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream() .flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE)) .collect(Collectors.toList()); this.sortedList = Collections.unmodifiableList(sortedInitializers); logMappings(this.initializers); }
讓我們來看兩張對比圖: 

通過上面兩個Debug的斷點圖,我們可以看到,在執行了addServletContextInitializerBeans(beanFactory)和addAdaptableBeans(beanFactory)方法之后,this.initializers的賦值發生了變化,兩個Servlet,四個Filter都被賦到里面,至于這兩個方法中的處理邏輯,此時已經心累到不想去看,暫記jira。 接下來的onStartup()方法,調用了接口org.springframework.boot.web.servlet.ServletContextInitializeronStartup()方法。
這里有下面幾點需要注意: SpringBoot創建Tomcat時,會先創建一個根上下文,webapplicationcontext傳給tomcat 啟動web容器,要先getWebserver,會創建tomcat的Webserver - 這里會把根上下文作為參數給org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#getWebServer,這里和tomcat的context進行merge 初始化servletcontext - 會把root上下文放進去,后面初始化dispatcherServlet時,會通過servletcontext拿到根上下文 啟動tomcat:調用Tomcat中Host、Engine的啟動方法
【DIspatcherServlet的初始化】初始化DispatcherServlet,是在第一次發起Web請求的時候(AbstractAnnotationConfigDispatcherServletInitializer) 具體調用過程如下: javax.servlet.GenericServlet#init(javax.servlet.ServletConfig) 

org.springframework.web.servlet.HttpServletBean#init org.springframework.web.servlet.FrameworkServlet#initServletBean protected WebApplicationContext initWebApplicationContext() { WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null;
if (this.webApplicationContext != null) { // A context instance was injected at construction time -> use it wac = this.webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; if (!cwac.isActive()) { // The context has not yet been refreshed -> provide services such as // setting the parent context, setting the application context id, etc if (cwac.getParent() == null) { // The context instance was injected without an explicit parent -> set // the root application context (if any; may be null) as the parent cwac.setParent(rootContext); } configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null) { // No context instance was injected at construction time -> see if one // has been registered in the servlet context. If one exists, it is assumed // that the parent context (if any) has already been set and that the // user has performed any initialization such as setting the context id wac = findWebApplicationContext(); } if (wac == null) { // No context instance is defined for this servlet -> create a local one wac = createWebApplicationContext(rootContext); }
if (!this.refreshEventReceived) { // Either the context is not a ConfigurableApplicationContext with refresh // support or the context injected at construction time had already been // refreshed -> trigger initial onRefresh manually here. synchronized (this.onRefreshMonitor) { //重要的刷新上下文操作 onRefresh(wac); } }
if (this.publishContext) { // Publish the context as a servlet context attribute. String attrName = getServletContextAttributeName(); getServletContext().setAttribute(attrName, wac); }
return wac; }
獲取根上下文(這時根上下文已經在SpringBoot啟動時初始化好了) 然后把根上下文給webApplicationContext org.springframework.web.servlet.DispatcherServlet#onRefresh - 這時就要刷新子上下文了,刷新上下文要刷新HandlerMapping,HandlerAdapter這些 protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); }
這里記錄幾個重要的時間點:



【四】SpringMVC和SpringBoot在啟動過程中不同點歸納關于SpringMVC、SpringBoot與Tomcat合作中,啟動方式不同點的總結如下 SpringMVC 先啟動tomcat,tomcat會去找web.xml,找到的是DispatcherServlet 初始化DispatcherServlet的上下文,要從ServletContext中找根上下文,這時是沒有的 創建根上下文
Springboot 在啟動過程中,SpringBoot會將DispatcherServlet給到Tomcat的Service的defaultWrapper中(如何給到的后面會說明) Tomcat啟動時,就會把“/”路徑和DispatcherServlet匹配 當第一次發起Web請求時,初始化DispatcherServlet,包括HandlerMapping,HandlerAdapter
|