Spring Boot以War包啟動


1.IDEA Spring Initializer自動構建的war包項目,自動生成的Initializer類,用於外部Tomcat容器啟動該項目時調用,如果仍然使用主類main函數方式啟動則與此類無關(Debug驗證過了)

2.自動構建的war包項目,pom.xml中引入了:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<!--<scope>provided</scope>-->
</dependency>

注釋的scope是我注釋的,生成時打開着,這樣引入,scope造成main方式運行時沒有內嵌Tomcat(只有編譯時有),雖然引入了:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

仍然提示錯誤:

Unregistering JMX-exposed beans on shutdown

所以需要注釋掉scope,這樣以main方式運行,內嵌的Tomcat可以啟動Spring Boot Web項目。參考:https://blog.csdn.net/sun20100912/article/details/52013463

但打war包放在外置Tomcat時就不需要了,要使用exclude干掉內嵌Tomcat,或像這里IDEA自動構建的war包項目一樣,使用scope在運行時不使用內嵌Tomcat,這時需要外置Tomcat使用這里生成的Initializer類:

package com.xiaobai.springbootwebdemo;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SpringbootwebdemoApplication.class);
}

}

Initializer類分析

跟蹤方法:通過啟動IDEA斷點Debug,一直按F8,無論哪里有斷點。這樣走完所有我們打斷點的類后可以進入更外層類,即Web容器啟動時先行運行的Tomcat的類當中,再一路F8,注意這時是層層向上倒置返回的,不斷層層向上打斷點,從這一層的子類方法到上一層調用它的父類模板方法(因為進入的一些層的類不過是前一層類的抽象父類,是父類的模板方法調用了子類的方法,實際仍然在子類實例中),從這一層的方法到上一層另一個類調用它的方法。F8走完和打完這次斷點后,再從頭Debug一遍,F8調試,這樣可以先進入上層,哪里F8后程序直接跑完了,就刪除這個斷點(因為這個斷點並不是一個會倒置返回的上層調用,而是一個完整模塊調用),再從頭Debug,F8,直到從上層最終能進入我們最初打斷點的內層,繼續,內層已跟蹤過的斷點適當使用Alt+F9跳過,走到差不多內層出口處再換回F8,進入外層。這樣層層向上找,打斷點,從頭Debug,F8,修改斷點,再Debug,F8,最終找到最初啟動入口類和方法,它初始化加載的文件,創建的類,傳入的參數等

0.Tomcat啟動時調用MBeanFactory的下面方法:

public String createStandardContext(String parent, String path, String docBase) throws Exception {
return this.createStandardContext(parent, path, docBase, false, false);
}

該方法調用:

public String createStandardContext(String parent, String path, String docBase, boolean xmlValidation, boolean xmlNamespaceAware) throws Exception {
StandardContext context = new StandardContext();
path = this.getPathStr(path);
context.setPath(path);
context.setDocBase(docBase);
context.setXmlValidation(xmlValidation);
context.setXmlNamespaceAware(xmlNamespaceAware);
ContextConfig contextConfig = new ContextConfig();
context.addLifecycleListener(contextConfig);
ObjectName pname = new ObjectName(parent);
ObjectName deployer = new ObjectName(pname.getDomain() + ":type=Deployer,host=" + pname.getKeyProperty("host"));
if (mserver.isRegistered(deployer)) {
String contextName = context.getName();
mserver.invoke(deployer, "addServiced", new Object[]{contextName}, new String[]{"java.lang.String"});
String configPath = (String)mserver.getAttribute(deployer, "configBaseName");
String baseName = context.getBaseName();
File configFile = new File(new File(configPath), baseName + ".xml");
if (configFile.isFile()) {
context.setConfigFile(configFile.toURI().toURL());
}

mserver.invoke(deployer, "manageApp", new Object[]{context}, new String[]{"org.apache.catalina.Context"});
mserver.invoke(deployer, "removeServiced", new Object[]{contextName}, new String[]{"java.lang.String"});
} else {
log.warn("Deployer not found for " + pname.getKeyProperty("host"));
Service service = this.getService(pname);
Engine engine = service.getContainer();
Host host = (Host)engine.findChild(pname.getKeyProperty("host"));
host.addChild(context);
}

return context.getObjectName().toString();
}

方法,創建了org.apache.catalina.core.StandardContext

注意里面加粗的反射調用邏輯,與創建的org.apache.catalina.core.StandardContext有關,留意一下這些類中的屬性字段尤其是static字段,有些可能事先由其他類加載並初始化了,在一些方法里直接用。

這時使用Alt+F9跳到我們的下一個斷點:org.apache.catalina.core.StandardContextstartInternal方法

1.(循環)調用org.apache.catalina.core.StandardContextstartInternal方法,其中一個調用走到我們的Web服務,在這個調用里循環調用該服務的每個ServletContainerInitializeronStartup方法:

while(i$.hasNext()) {
Entry entry = (Entry)i$.next();

try {
((ServletContainerInitializer)entry.getKey()).onStartup((Set)entry.getValue(), this.getServletContext());
} catch (ServletException var22) {
log.error(sm.getString("standardContext.sciFail"), var22);
ok = false;
break;
}
}

這里傳入的ServletContext是使用上面創建的org.apache.catalina.core.StandardContext初始化的:

this.context = new ApplicationContext(this);//this:org.apache.catalina.core.StandardContext

其中一個就是SpringServletContainerInitializeronStartup方法(也是循環調用,多個SpringServletContainerInitializer),該方法最后通過:

while(var4.hasNext()) {
WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
initializer.onStartup(servletContext);
}

這是一個循環調用,其中一個調用SpringBootServletInitializeronStartup方法(ServletContext已傳入):

public void onStartup(ServletContext servletContext) throws ServletException {
this.logger = LogFactory.getLog(this.getClass());
WebApplicationContext rootAppContext = this.createRootApplicationContext(servletContext);
if (rootAppContext != null) {
servletContext.addListener(new ContextLoaderListener(rootAppContext) {
public void contextInitialized(ServletContextEvent event) {
}
});
} else {
this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not return an application context");
}

}

該方法調用方法(繼續傳入ServletContext):

protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
SpringApplicationBuilder builder = this.createSpringApplicationBuilder();
builder.main(this.getClass());
ApplicationContext parent = this.getExistingRootWebApplicationContext(servletContext);
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, (Object)null);
builder.initializers(new ApplicationContextInitializer[]{new ParentContextApplicationContextInitializer(parent)});
}

builder.initializers(new ApplicationContextInitializer[]{new ServletContextApplicationContextInitializer(servletContext)});
builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
builder = this.configure(builder);
builder.listeners(new ApplicationListener[]{new SpringBootServletInitializer.WebEnvironmentPropertySourceInitializer(servletContext, null)});
SpringApplication application = builder.build();
if (application.getAllSources().isEmpty() && AnnotationUtils.findAnnotation(this.getClass(), Configuration.class) != null) {
application.addPrimarySources(Collections.singleton(this.getClass()));
}

Assert.state(!application.getAllSources().isEmpty(), "No SpringApplication sources have been defined. Either override the configure method or add an @Configuration annotation");
if (this.registerErrorPageFilter) {
application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
}

return this.run(application);
}

該方法創建了一個SpringApplicationBuilder,以Spring Boot War項目自動生成的我們的ServletInitializer為mainApplicationClass,使用傳入的ServletContext(Tomcat為我們的應用所創建)設置了Spring Boot上下文的Initializer和Listener(將Web環境和Spring Boot互相加入),調用了我們ServletInitializer中重載的:

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SpringbootwebdemoApplication.class);
}

將帶有@SpringBootApplication注解的我們的SpringbootwebdemoApplication類注入到SpringApplicationBuilder作為source.說明無論是main函數啟動方式還是Tomcat以Web項目啟動方式,關鍵的是運行帶有@SpringBootApplication注解的類完成Spring Boot配置。

然后SpringApplication由SpringApplicationBuilder來build出來:

SpringApplication application = builder.build();

並運行起來:

return this.run(application);

該方法調用SpringApplication本身的run方法,和main方法啟動方式完全一樣了。這樣就由Tomcat初始化並啟動了Spring Boot,並加入到了Web環境(ServletContext)

最后調用SpringApplication本身的run方法是創建了一個AnnotationConfigServletWebServerApplicationContext作為Spring Boot的Web環境上下文對象

2.org.apache.catalina.core.StandardContextstartInternal方法接續1繼續執行:

if (ok && !this.listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}

進入org.apache.catalina.core.StandardContextlistenerStart方法(也循環多次調用了該方法),該方法方法體內調用:

for(int i = 0; i < instances.length; ++i) {
if (instances[i] instanceof ServletContextListener) {
ServletContextListener listener = (ServletContextListener)instances[i];

try {
this.fireContainerEvent("beforeContextInitialized", listener);
if (this.noPluggabilityListeners.contains(listener)) {
listener.contextInitialized(tldEvent);
} else {
listener.contextInitialized(event);
}

this.fireContainerEvent("afterContextInitialized", listener);
} catch (Throwable var12) {
ExceptionUtils.handleThrowable(var12);
this.fireContainerEvent("afterContextInitialized", listener);
this.getLogger().error(sm.getString("standardContext.listenerStart", new Object[]{instances[i].getClass().getName()}), var12);
ok = false;
}
}
}

這又是一個循環,其中有一個就是上面SpringBootServletInitializeronStartup方法中注冊的該服務的Listener:

    if (rootAppContext != null) {
servletContext.addListener(new ContextLoaderListener(rootAppContext) {
public void contextInitialized(ServletContextEvent event) {
}
});
}

執行其contextInitialized方法。

這里注冊的ContextLoaderListener重寫的contextInitialize方法里面沒有任何實現,所以什么都不做。

至此我們的服務已經啟動完成。

 

進一步研究提示:StandardContext的外層相關類包括

LifecycleBase
ContainerBase
StandardHost
HostConfig

其中LifecycleBase為LifecycleMBeanBase父類,LifecycleMBeanBase為ContainerBase父類,ContainerBase為StandardContextStandardHost父類,那么調用采用的是模板模式,由父類方法調用子類方法。

更外層的HostConfig使用了StandardHost:

this.host.addChild(context);

因為StandardHost實現了Host接口。

最外面還有一個工具類BaseModelMBean不斷執行其invoke方法,反射執行一些方法,反復配合第0步開始的MBeanFactorycreateStandardContext方法里面的:

mserver.invoke(deployer, "manageApp", new Object[]{context}, new String[]{"org.apache.catalina.Context"});
mserver.invoke(deployer, "removeServiced", new Object[]{contextName}, new String[]{"java.lang.String"});

最后從這里(MBeanFactorycreateStandardContext方法,經過Debug,正向層次調用和逆向層次返回,整個上面都是從這個方法出發的各層次調用流程,最后回到該方法)結束Tomcat所有啟動流程,項目可通過瀏覽器訪問了。

 

整合jsp提示:無法通過main/jar啟動方式配置、訪問jsp,只能以Tomcat運行,IDEA配置Web模塊及其根目錄,再在application.yml中配置mvc view相關前后綴屬性。

嘗試main方式訪問jsp的一個錯誤參考:

https://blog.csdn.net/universsky2015/article/details/77965402

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM