SpringBoot有啥高科技?是怎麼做到XML零配置的?

前言:剛畢業我就接觸到了SpringBoot,當初感覺必成大器,第一印象就是內置了所有環境,打完包丟哪裡都能跑起來,簡化了tomcat Xml配置的一系列部署操作


SpringBoot有啥高科技?是怎麼做到XML零配置的?


1.SpringMvc XML配置

說到配置SpringMvc,大家第一時間反應就是xml配置,目前國內的各類博客或者各類老師都是套用這種方式,一直都是認為這種方式是唯一的方式,再說Spring官方一直支持。

1.1 配置web.xml

web.xml是servlet容器的配置文件,當啟動一個WEB項目時,servlet容器首先會讀取項目中的webapp/WEB-INFO文件夾的web.xml配置文件裡的配置,主要用來配置監聽器listener,servlet,上下文參數context-param。

<code>          
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener/<listener-class>
/<listener>



<servlet>
  <servlet-name>dispatcherServlet/<servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet/<servlet-class>
  /<servlet>


<context-param>
<param-name>contextConfigLocation/<param-name>
<param-value>/WEB-INF/spring-config/*.xml/<param-value>
/<context-param>

/<code>

ContextLoaderListener(上下文加載監聽) 繼承了ServletContextListener(Servlet上下文監聽),當ServletContext的生命週期發生變化會觸發相應的事件

通過這個監聽器進來的會通過createWebApplicationContext獲取ConfigurableWebApplicationContext

具體代碼: ContextLoader ->configureAndRefreshWebApplicationContext方法

<code>  protected void configureAndRefreshWebApplicationContext
(ConfigurableWebApplicationContext wac, ServletContext sc) {
//添加ServletContext
\twac.setServletContext(sc);
\tString configLocationParam = sc.getInitParameter("contextConfigLocation");
//添加Spring*.xml
\twac.setConfigLocation(configLocationParam);
customizeContext(sc, wac);
\t//讀取配置加載,刷新Spring上下文
wac.refresh();
}
/<code>

DispatcherServlet 用來接收SpringMVC所有請求的servlet程序,會註冊到ServletContext中。

1.2 配置applicationContext.xml

主要掃描業務類,AOP切面配置,事務配置,數據源配置等

<code>
<component-scan>
<exclude-filter>
/<component-scan>
/<code>

1.3 配置springmvc.xml

主要掃描Controller,攔截器,視圖轉換等

<code>
<component-scan>
\t <include-filter>
/<component-scan>
/<code>

1.4 啟動大概流程

在啟動Servlet容器,會去讀取web.xml配置文件註冊Servlet,然後異步執行ServletContextListener的contextInitialized方法讀取用戶自定義的xml配置文件並創建bean,刷新Spring上下文。

2.SpringMvc 另外一種配置

2.1 怎麼註冊DispatcherServlet ?

猜想1:也是xml配置方式。但是Spring官網都把零xml的配置當成一種優勢,那顯然不科學。

猜想2:@WebServlet。我們找找DispatcherServlet這個類?居然沒有@WebServlet註解

那隻能看看SpringMvc的文檔,發現SpringMvc官方配置也推薦使用javaConfig的配置方式。

具體代碼:

<code>public class MyWebApplicationInitializer implements WebApplicationInitializer {

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
//SpringWeb註解配置應用程序上下本
AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
//註冊帶有註解的類
ac.register(AppConfig.class);
//spring上下文刷新

ac.refresh();

//創建DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(ac);

//servlet中註冊DispatcherServlet
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
/<code>

我們發現這個自定義類會實現WebApplicationInitializer接口

onStartup方法會拿到tomcat傳過來的ServletContext(如果是jetty 就是jetty提供的 如果是tomcat 就是 tomcat提供的)

AnnotationConfigWebApplicationContext是繼承了上面的ConfigurableWebApplicationContext ,用來實例化IOC容器,也就是做Spring上下文刷新的,

它不僅支持掃描@Configuration註解,任何@Compnent註解的類或者 按照JSR-330註解的類都被支持 。

AppConfig類上會加入@ComponentSan註解,掃描指定包下所有的業務層和控制層的bean

而我們之前的xml配置的DispatcherServlet是通過new DispatcherServlet()出來的

2.2 onStartup啥時候能調到?

傳統配置的web.xml 是在servlet容器啟動的時候加載的,那實現webApplicationInitializer的自定義的類應該也要在servlet容器啟動的時候被加載到

是不是tomcat也學Spring一樣得到所有WebApplicationInitializer的實現,然後調用它的onStartup呢。

但是他們工程師絕對不會這麼幹,因為WebApplicationInitializer是Spring提供的,一個實現Servlet規範的容器不可能依賴Spring的jar包。

那接下來我們來看看SpringMvc的啟動核心科技

首先tomcat是一個Servlet容器,遵循並實現了Servlet的規範,tomcat7之後是3.0的,在3.0的有個新的特性

就是它 :ServletContainerInitializer(Servlet容器初始化器)

在web容器啟動時為提供給第三方組件做一些初始化的工作,例如註冊servlet或者listener等。

前提是必須在對應的jar包的META-INF/services 目錄創建一個名為javax.servlet.ServletContainerInitializer的文件,文件內容指定具體的ServletContainerInitializer實現類

一般伴隨著ServletContainerInitializer一起使用的還有@HandlesTypes註解,他會在調用onStartup方法的時候會把所有實現的類集合傳給你。

具體代碼: SpringServletContainerInitializer->onStartup

<code>@Override
\tpublic void onStartup(@Nullable Set<class>> webAppInitializerClasses, ServletContext servletContext)
\t\t\tthrows ServletException {
//一個裝WebApplicationInitializer實現的集合
\t\tList<webapplicationinitializer> initializers = new LinkedList<>();
\t\tfor (Class> waiClass : webAppInitializerClasses) {
//通過反射拿到HandlesTypes註解指定的class的實現類
\t\t\tinitializers.add(ReflectionUtils.accessibleConstructor(waiClass).newInstance());
\t\t}
//進行排序
\t\tAnnotationAwareOrderComparator.sort(initializers);
//循環調用所有集合裡的onStartup方法
\t\tfor (WebApplicationInitializer initializer : initializers) {
\t\t\tinitializer.onStartup(servletContext);
\t\t}
\t}
/<webapplicationinitializer>/<class>/<code>

這是spring慣用方法,將所有實現WebApplicationInitializer的實現類,遍歷執行onStartup方法

大家看到這裡是不是就大概清楚SpringBoot是怎麼才能做到零配置的。

3.SpringBoot具體是怎麼配置的SpringMvc

在Spring代碼中有一個DispatcherServletAutoConfiguration靜態類,聲明瞭一個DispatcherServlet的Bean

具體代碼:DispatcherServletAutoConfiguration

<code>protected static class DispatcherServletConfiguration {

\t\t@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
\t\tpublic DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties) {
\t\t\tDispatcherServlet dispatcherServlet = new DispatcherServlet();
\t\t\tdispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
\t\t\tdispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
\t\t\tdispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
\t\t\tdispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
\t\t\tdispatcherServlet.setEnableLoggingRequestDetails(httpProperties.isLogRequestDetails());
\t\t\treturn dispatcherServlet;
\t\t}
}/<code>

這裡就需要藉助一下SpringBoot啟動流程

main方法啟動之後首先會檢查是否是web項目,怎麼檢查呢?

對的 大家猜的對,就是嘗試forName(加載類)加載一下初始化這個寫死的類路徑javax.servlet.Servlet,如果能實例化就代表是。

具體代碼:WebApplicationType

<code>if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
/<code>

如果是Servlet環境就會使用AnnotationConfigServletWebServerApplicationContext去刷新spring上下文

這樣DispatcherServlet被作為一個普通Bean被實例化並註冊到IOC容器中。

在調用刷新spring上下文之後調用createWebServer方法

<code>\t@Override
\tprotected void onRefresh() {
\t\tsuper.onRefresh();
\t\ttry {
\t\t\tcreateWebServer();
\t\t}
\t\tcatch (Throwable ex) {
\t\t\tthrow new ApplicationContextException("Unable to start web server", ex);
\t\t}
\t}/<code>

createWebServer()

<code>private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
ServletWebServerFactory factory = getWebServerFactory();
//根據工廠模式調用對應的servlet實現的容器,如果是tomcat就調用TomcatServletWebServerFactory的getWebServer()
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();
}/<code>

創建tomcat實例並啟動

<code>@Override
\tpublic WebServer getWebServer(ServletContextInitializer... initializers) {
\t\tif (this.disableMBeanRegistry) {
\t\t\tRegistry.disableRegistry();
\t\t}
\t\tTomcat tomcat = new Tomcat();
\t\tFile baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
\t\ttomcat.setBaseDir(baseDir.getAbsolutePath());
\t\tConnector connector = new Connector(this.protocol);
\t\tconnector.setThrowOnFailure(true);
\t\ttomcat.getService().addConnector(connector);
\t\tcustomizeConnector(connector);
\t\ttomcat.setConnector(connector);
\t\ttomcat.getHost().setAutoDeploy(false);
\t\tconfigureEngine(tomcat.getEngine());
\t\tfor (Connector additionalConnector : this.additionalTomcatConnectors) {
\t\t\ttomcat.getService().addConnector(additionalConnector);
\t\t}
//準備上下文,主要會提前放入他自己的ServletContextInitializer(Servlet上下文是初始化器)
//實現類,供SpringBoot在ServletContainerInitializer的onStartup()裡遍歷調用自己的onStartup()
//為了註冊用戶自定義的Filter和Servlet到ServletContext中
\t\tprepareContext(tomcat.getHost(), initializers);
//啟動tomcat
\t\treturn getTomcatWebServer(tomcat);
\t}/<code>

具體代碼:org.springframework.boot.web.embedded.tomcat.TomcatWebServer ->initialize();

<code>private void initialize() throws WebServerException {
\t// Start the server to trigger initialization listeners
\tthis.tomcat.start();
}/<code>

在這時候按照servlet3.0的標準,Tomcat啟動的時候會調用ServletContainerInitializer(Servlet容器初始化器)所有實現類的onStartup()方法

具體代碼:TomcatStarter->onStartup();

<code>@Override
public void onStartup(Set<class>> classes, ServletContext servletContext) throws ServletException {


for (ServletContextInitializer initializer : this.initializers) {
initializer.onStartup(servletContext);
}

}/<class>/<code>

具體代碼:ServletRegistrationBean->onStartup();

<code>@Override
public final void onStartup(ServletContext servletContext) throws ServletException {
String description = getDescription();
if (!isEnabled()) {
logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");
return;
}
//拿到Tomcat傳過來的ServletContext進行註冊
register(description, servletContext);
}/<code>

具體代碼:ServletRegistrationBean->addRegistration();

<code>@Override
protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {
String name = getServletName();
return servletContext.addServlet(name, this.servlet);
}/<code>

這樣 DispatcherServlet 就註冊進去了。

這也為啥SpringBoot只支持Servlet3.0的容器,只不過趕上了3.0的好特性,才讓我們開發者體驗到非常友善的傻白甜的開發。

4.擴展

首先SpringBoot有兩種部署方式 丟Tomcat和java -jar運行。

對於兩種,它的啟動的也不一樣

4.1 SpringBoot內置的容器

首先Springboot並不是web應用,在你只引入

<code><dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter/<artifactid>
<version>2.1.6.RELEASE/<version>
<scope>compile/<scope>
/<dependency>
/<code>

它只不過是一個Spring的項目

那官方說的內置servlet容器,默認使用的tomcat是誰引進來的?

如果是web項目就必須得引入spring-boot-starter-web,而它依賴了spring-boot-starter-tomcat

<code><dependency>
<groupid>org.apache.tomcat.embed/<groupid>
<artifactid>tomcat-embed-core/<artifactid>
<version>9.0.21/<version>
<scope>compile/<scope>
<exclusions>
<exclusion>
<artifactid>tomcat-annotations-api/<artifactid>
<groupid>org.apache.tomcat/<groupid>
/<exclusion>
/<exclusions>
/<dependency>
/<code>

4.2丟war包的方式

配置很簡單,只需要繼承SpringBootServletInitializer,而SpringBootServletInitializer實現了WebApplicationInitializer接口

<code>package com.example;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

public class SpringBootServletStart extends SpringBootServletInitializer {

@Override

protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
//這裡是@SpringBootApplication類
return application.sources(DemoApplication.class);
}

}
/<code>

這種方式和SpringMvc的javaConfig一樣方式,tomcat啟動的時候去找WebApplicationInitializer的實現類

當執行到SpringBootServletInitializer的onStartup方法的時候,new SpringBootApplication.run()

4.3 java -jar運行

maven package打的jar是不能直接運行的。

為啥我們 maven package一下就可以,那是因為SpringBoot項目都有一個插件

<code> <plugin>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-maven-plugin/<artifactid>
/<plugin>
/<code>

mvn package spring-boot:repackage

所以SpringBoot 打完包在tagger裡看到兩個,一個是.jar.original 一個是.jar ,也就是說Maven首先在package階段打包生成*.jar文件;然後執行spring-boot:repackage重新打包,會把項目運行的所有依賴的jar包都整合到一個單獨的jar包中,並配置Manifest文件以及JarLauncherhttps://docs.spring.io/spring-boot/docs/current/reference/html/build-tool-plugins.html#build-tool-plugins-maven-plugin

<code>Manifest-Version: 1.0
Created-By: Maven Archiver 3.4.0
Build-Jdk-Spec: 13
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Version: 2.2.2.RELEASE

Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
/<code>



分享到:


相關文章: