本文使用的Spring版本為Spring6,SpringBoot版本為3,JDK為17,可能會和之前有細微不同,但整體流程差不太大。
部署到webapps目錄啟動
如果部署應用到tomcat webapps目錄下面啟動,則需要在項目中配置web.xml檔案
web.xml檔案
配置Spring應用上下文
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/application-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
複制代碼
context-param
context-param标簽是用于在Web應用程式的上下文範圍内設定初始化參數。這些參數可以在整個Web應用程式中使用,并且可以通過ServletContext對象的getInitParameter()方法擷取。
ContextLoaderListener
ContextLoaderListener實作了ServletContextListener接口,這個接口是tomcat留給應用程式初始化上下文環境的接口,用于在Web應用程式啟動時加載ApplicationContext。
ServletContextListener有兩個預設方法
// 在所有的servlet和filter初始化之前被調用
default public void contextInitialized(ServletContextEvent sce) {
}
// 在所有的servlet和filter銷毀之後被調用
default public void contextDestroyed(ServletContextEvent sce) {
}
複制代碼
ContextLoaderListener還繼承了ContextLoader類,所有的context操作都在此類進行。
ContextLoaderListener實作contextInitialized方法,然後調用父類ContextLoader的initWebApplicationContext方法,把ServletContext傳進去。
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
複制代碼
初始化Spring Context。
initWebApplicationContext方法關鍵代碼
...
if (this.context == null) {
// 建立ApplicationContext
this.context = createWebApplicationContext(servletContext);
}
...
// 重新整理ApplicationContext
configureAndRefreshWebApplicationContext(cwac, servletContext);
...
// 将目前ApplicationContext添加到ServletContext的屬性中,後面有用再說
// String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
...
複制代碼
建立ApplicationContext
在createWebApplicationContext方法中,先調用determineContextClass方法确定使用哪個ApplicationContext,找到之後,執行個體化。
determineContextClass這個方法,主要是确定使用的ApplicationContext,首先從web.xml中加載,如果使用者有定義,直接使用使用者自定義的。
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
複制代碼
web.xml中配置如下,
<context-param>
<param-name>contextClass</param-name>
<param-value>com.xxx.XxxContext</param-value>
</context-param>
複制代碼
如果沒有配置,則使用Spring預設的XmlWebApplicationContext類。
這個類在ContextLoader同路徑包下面的ContextLoader.properties檔案中定義。
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
複制代碼
配置和重新整理ApplicationContext
configureAndRefreshWebApplicationContext關鍵代碼
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac,ServletContext sc) {
// ...
// 擷取web.xml中配置的contextConfigLocation參數
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
// ...
// 重新整理上下文
wac.refresh();
}
複制代碼
至此Tomcat已經啟動Spring環境了,後續就是Spring的初始化流程,這裡不再叙述。
初始化DispatcherServlet
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/dispatcher-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
複制代碼
此處的contextConfigLocation屬于DispatcherServlet的父類FrameworkServlet,主要用來加載SpringMVC相關的配置,示例如下:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd">
<!-- 掃描控制器和其他元件 -->
<context:component-scan base-package="com.example.controller" />
<!-- 配置視圖解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- 啟用Spring MVC注解支援 -->
<mvc:annotation-driven />
</beans>
複制代碼
DispatcherServlet類圖
可以看到DispatcherServlet實作了Servlet接口,Servlet接口中有init方法,SpringMVC的配置就是在初始化的時候被加載的。
關鍵代碼在HttpServletBean.init()和FrameworkServlet.initServletBean()方法中。
HttpServletBean.init()
public final void init() throws ServletException {
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
initServletBean();
}
複制代碼
FrameworkServlet.initServletBean()
protected final void initServletBean() throws ServletException {
...
// 在這裡初始化ApplicationContext
this.webApplicationContext = initWebApplicationContext();
// 初始化servlet
initFrameworkServlet();
}
複制代碼
FrameworkServlet.initWebApplicationContext()
protected WebApplicationContext initWebApplicationContext() {
// 此處擷取根容器,就是Spring初始化的XmlWebApplicationContext,
// 在上面把它添加到了ServletContext的屬性中,标記根容器,這裡把它擷取出來
// String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
// servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
// 此時webApplicationContext還是null,因為DispatchServlet是被tomcat建立的,需要無參構造器
// 構造器中沒有設定webApplicationContext的代碼,是以此時webApplicationContext還是null
// 注意:在SpringBoot使用嵌入式Tomcat時,這個webApplicationContext不為null,因為FrameworkServlet還
// 實作了ApplicationContextAware接口,是以當SpringBoot的上下文準備好之後,會回調setApplicationContext方法
// 注入ApplicationContext,後面在細說
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext cwac && !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
// 此處主要是擷取web.xml配置的WebApplicationContext
// 可以通過設定參數contextAttribute來設定加載SpringMVC的ApplicationContext
// 比如下面這樣。除非項目中有多個WebApplicationContext,需要使用其他WebApplicationContext才會用到
// 一般都是null
// <context-param>
// <param-name>contextAttribute</param-name>
// <param-value>myWebApplicationContext</param-value>
// </context-param>
wac = findWebApplicationContext();
}
if (wac == null) {
// 現在進入到建立SpringMVC的ApplicationContext流程
// 也就是加載contextConfigLocation定義的xml檔案
// 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) {
// 初始化政策對象
// 比如:HandlerMapping,HandlerAdapter,ViewResolver等等
onRefresh(wac);
}
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
複制代碼
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
// SpringMVC所使用的contextClass,可以在<servlet>标簽下設定
// <init-param>
// <param-name>contextClass</param-name>
// <param-value>org.springframework.web.context.support.XmlWebApplicationContext</param-value>
// </init-param>
// 預設為XmlWebApplicationContext
Class<?> contextClass = getContextClass();
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(
"Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
// 執行個體化ApplicationContext
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
// 設定環境參數
wac.setEnvironment(getEnvironment());
// 設定父容器為Spring的ApplicationContext
wac.setParent(parent);
// 擷取SpringMVC的contextConfigLocation檔案
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
// 配置并重新整理ApplicationContext
configureAndRefreshWebApplicationContext(wac);
return wac;
}
複制代碼
DispatchServlet初始化完成
為什麼需要父子容器
父子容器的作用主要是劃分架構邊界和實作bean的複用。
- 在J2EE三層架構中,在service層我們一般使用Spring架構,而在web層則有多種選擇,如Spring MVC、Struts等。為了讓web層能夠使用service層的bean,我們需要将service層的容器作為web層容器的父容器,這樣就可以實作架構的整合。
- 父子容器的作用在于,當我們嘗試從子容器(Servlet WebApplicationContext)中擷取一個bean時,如果找不到,則會委派給父容器(Root WebApplicationContext)進行查找。這樣可以避免在多個子容器中重複定義相同的bean,提高了代碼的複用性和可維護性。
接收請求
請求先進入doService,然後調用doDispatch進行處理。
doDispatch關鍵代碼
...
// 首先根據目前請求HttpServletRequest,周遊所有的HandlerMapping執行handle方法,傳回可用的HandlerExecutionChain對象。
mappedHandler = getHandler(processedRequest);
// 然後根據handler擷取支援的擴充卡
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 執行HandlerInterceptor.preHandle,在controller的方法被調用前執行
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 執行controller方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 執行HandlerInterceptor.postHandle,在controller的方法被調用後執行
mappedHandler.applyPostHandle(processedRequest, response, mv);
// 渲染結果到視圖
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
複制代碼
- HandlerMapping是request與handler object之間的映射,它能根據request找到對應的handler。handler object可以是任意類型,比如@Controller注解的類,或者實作了Controller接口的類,或者實作了HttpRequestHandler接口的類等。
- HandlerExecutionChain是handler執行鍊,它包裝了handler object和一組HandlerInterceptor。HandlerInterceptor是攔截器,它可以在handler執行前後進行一些額外的操作,比如權限檢查,日志記錄等。
- HandlerAdapter是handler的擴充卡,它能處理不同類型的handler object,并調用其對應的方法,傳回ModelAndView對象。HandlerAdapter可以根據handler object的類型,進行參數綁定,傳回值處理等操作。
HandlerInterceptor使用
- 定義一個攔截器類,實作HandlerInterceptor接口或者繼承HandlerInterceptorAdapter類,重寫preHandle,postHandle和afterCompletion三個方法。
- 在preHandle方法中,可以擷取請求和響應對象,進行預處理,比如檢查請求頭中的token,或者判斷請求的url是否有權限通路等。如果傳回true,則繼續執行後續的攔截器或者處理器;如果傳回false,則中斷請求,不再執行後續的攔截器或者處理器。
- 在postHandle方法中,可以擷取請求和響應對象,以及處理器傳回的ModelAndView對象,進行後處理,比如修改模型資料或者視圖資訊等。這個方法隻有在preHandle傳回true且處理器成功執行後才會調用。
- 在afterCompletion方法中,可以擷取請求和響應對象,以及處理器抛出的異常對象(如果有的話),進行清理資源或者異常處理等。這個方法隻有在preHandle傳回true後才會調用,無論處理器是否成功執行。
- 在SpringMVC的配置檔案中,注冊攔截器類,并指定攔截的url模式。可以注冊多個攔截器,并指定順序。攔截器會按照順序執行preHandle方法,然後按照逆序執行postHandle和afterCompletion方法。
HandlerInterceptor和Filter的差別
- HandlerInterceptor是基于Java反射機制的,而Filter是基于函數回調的。HandlerInterceptor可以利用Spring的AOP技術,實作更靈活的攔截邏輯,而Filter隻能在請求前後進行簡單的處理。
- HandlerInterceptor不依賴于Servlet容器,而Filter依賴于Servlet容器。HandlerInterceptor是SpringMVC架構提供的,可以在任何情況下使用,而Filter是Servlet規範的一部分,隻能在Web應用中使用。
- HandlerInterceptor的執行由SpringMVC架構控制,而Filter的執行由Servlet容器控制。HandlerInterceptor可以通過IoC容器來管理,可以注入其他的Bean,而Filter則需要在web.xml中配置,或者使用@WebFilter注解,并且需要@ServletComponentScan掃描。
- HandlerInterceptor隻能攔截DispatcherServlet處理的請求,而Filter可以攔截任何請求。HandlerInterceptor隻能對Controller方法進行攔截,而Filter可以對靜态資源、JSP頁面等進行攔截。
- HandlerInterceptor有三個方法:preHandle,postHandle和afterCompletion,分别在請求處理前後和視圖渲染前後執行,而Filter隻有一個方法:doFilter,在請求處理前後執行。
處理controller傳回結果
對于被controller方法,使用的擴充卡是RequestMappingHandlerAdapter,在handlerAdapter.handle方法執行時,會去執行對應的controller方法,處理controller方法傳回的結果。
invocableMethod.invokeAndHandle(webRequest, mavContainer);
複制代碼
ServletInvocableHandlerMethod.invokeAndHandle
// 執行controller方法
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
...
// 處理傳回資料,會判斷是不是有@ResponseBody注解,如果有,會使用RequestResponseBodyMethodProcessor來處理傳回值
// 然後會解析請求頭等等,判斷應該傳回什麼類型的資料,然後使用對應的HttpMessageConverter寫入輸出流
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
複制代碼
SpringBoot Jar啟動
SpringBoot使用嵌入式Servlet容器啟動應用,有Tomcat,Jetty,Undertow。
選擇Servlet容器
SpringBoot預設使用Tomcat,可以在配置檔案中看出。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
複制代碼
web子產品自動引入了tomcat
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
複制代碼
如果不使用Tomcat可以排除,引入其他伺服器。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 剔除Tomcat -->
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 使用jetty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
複制代碼
如果沒有排除Tomcat,直接引入其他伺服器,比如下面。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 沒有排除Tomcat -->
</dependency>
<!-- 引入jetty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
複制代碼
如果項目中同時引入了Tomcat和其他伺服器的依賴,那麼SpringBoot會按照以下順序來選擇啟動的伺服器。
Tomcat > Jetty > Undertow
也就是說,如果有Tomcat,就優先使用Tomcat,如果沒有Tomcat,就看有沒有Jetty,如果有Jetty,就使用Jetty,以此類推。這個順序是在SpringBoot的ServletWebServerFactoryConfiguration類中定義的。
// 隻展示必要代碼
class ServletWebServerFactoryConfiguration {
// 當Servlet、Tomcat、UpgradeProtocol類在類路徑存在時
// 并且ServletWebServerFactory類存在,則會建立tomcatServletWebServerFactory bean。
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
static class EmbeddedTomcat {
@Bean
TomcatServletWebServerFactory tomcatServletWebServerFactory(
... 代碼省略
}
}
// 當Servlet、Server、WebAppContext類在類路徑存在時
// 并且ServletWebServerFactory類型的Bean不存在時,則會建立JettyServletWebServerFactory bean。
// ServletWebServerFactory是TomcatServletWebServerFactory、JettyServletWebServerFactory、
// UndertowServletWebServerFactory的父類
// 是以如果Tomcat被引入,上面的tomcatServletWebServerFactory就會被建立,這裡的條件就不滿足,不會被建立。
@ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
static class EmbeddedJetty {
@Bean
JettyServletWebServerFactory JettyServletWebServerFactory(
... 代碼省略
}
}
// 分析同上
@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
static class EmbeddedUndertow {
@Bean
UndertowServletWebServerFactory undertowServletWebServerFactory(
... 代碼省略
}
}
複制代碼
下面繼續以Tomcat為例
Tomcat配置、啟動
Tomcat是在Spring容器啟動的時候啟動的
SpringApplication.run方法
- 首先建立一個ConfigurableApplicationContext對象,并調用其refresh()方法,這個對象一般是AnnotationConfigServletWebServerApplicationContext。
- context = createApplicationContext(); -> refreshContext(context); -> refresh(context); -> applicationContext.refresh(); 複制代碼
- refresh()方法會調用其父類ServletWebServerApplicationContext的refresh()方法,在父類的refresh()中再次調用父類AbstractApplicationContext的refresh()方法,主要在onRefresh階段,會進行伺服器的配置。
- ... refresh()代碼簡略 // 這裡會初始化Tomcat配置 onRefresh(); // 這裡會啟動Tomcat finishRefresh(); ... 複制代碼
- 回到ServletWebServerApplicationContext類的onRefresh()方法,會調用createWebServer()方法,建立web伺服器。
- protected void onRefresh() { super.onRefresh(); try { // 建立伺服器 createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); } } 複制代碼 private void createWebServer() { ... 代碼簡略 // 擷取工廠類,這裡擷取的就是在配置類中生效的那一個,這裡為TomcatServletWebServerFactory ServletWebServerFactory factory = getWebServerFactory(); createWebServer.tag("factory", factory.getClass().toString()); // 擷取伺服器 this.webServer = factory.getWebServer(getSelfInitializer()); } 複制代碼
- TomcatServletWebServerFactory.getWebServer
- public WebServer getWebServer(ServletContextInitializer... initializers) { if (this.disableMBeanRegistry) { Registry.disableRegistry(); } Tomcat tomcat = new Tomcat(); File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat"); tomcat.setBaseDir(baseDir.getAbsolutePath()); for (LifecycleListener listener : this.serverLifecycleListeners) { tomcat.getServer().addLifecycleListener(listener); } // 設定Connector,對應與Tomcat Server.xml 中的<Connector></Connector> Connector connector = new Connector(this.protocol); connector.setThrowOnFailure(true); // 對應于Server.xml 中 // <Service name="Catalina"> // <Connector port="8080" protocol="HTTP/1.1" // connectionTimeout="20000" // redirectPort="8443" relaxedQueryChars="[|]"/> // </Service> tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false); configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); } // 準備好Context元件 prepareContext(tomcat.getHost(), initializers); return getTomcatWebServer(tomcat); } 複制代碼 // 建立Tomcat伺服器 protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown()); } 複制代碼
- 至此,Tomcat配置已經初始化完成,準備啟動。
- 在finishRefresh()方法中,會啟動Tomcat
- getLifecycleProcessor().onRefresh(); > DefaultLifecycleProcessor.startBeans(true); > LifecycleGroup::start > doStart(this.lifecycleBeans, member.name, this.autoStartupOnly); > bean.start(); > WebServerStartStopLifecycle.start > TomcatWebServer.start(); 複制代碼 private void startBeans(boolean autoStartupOnly) { Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans(); Map<Integer, LifecycleGroup> phases = new TreeMap<>(); lifecycleBeans.forEach((beanName, bean) -> { if (!autoStartupOnly || (bean instanceof SmartLifecycle smartLifecycle && smartLifecycle.isAutoStartup())) { int phase = getPhase(bean); phases.computeIfAbsent( phase, p -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly) ).add(beanName, bean); } }); if (!phases.isEmpty()) { phases.values().forEach(LifecycleGroup::start); } } 複制代碼 public void start() { this.webServer.start(); this.running = true; this.applicationContext .publishEvent(new ServletWebServerInitializedEvent(this.webServer, this.applicationContext)); } 複制代碼
DispatchServlet配置
ServletContextInitializer
在prepareContext方法中,有一個方法configureContext
configureContext(context, initializersToUse);
複制代碼
configureContext方法,在這裡面建立了一個TomcatStarter對象,這個類實作了ServletContainerInitializer接口,是以在容器啟動過程中會被調用。
TomcatStarter starter = new TomcatStarter(initializers);
context.addServletContainerInitializer(starter, NO_CLASSES);
複制代碼
initializers是Spring自己定義的初始化接口ServletContextInitializer,傳入TomcatStarter之後,在onStartup方法中循環調用onStartup方法。
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
try {
for (ServletContextInitializer initializer : this.initializers) {
initializer.onStartup(servletContext);
}
}
...
}
複制代碼
需要注意的是,這裡的initializers有些傳過來的時候是一個函數式接口,在上面的factory.getWebServer(getSelfInitializer());這裡傳進來的,就是一個函數式接口
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
return this::selfInitialize;
}
複制代碼
實際調用在下面這個方法
private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}
複制代碼
這裡周遊所有的ServletContextInitializer,然後調用它的onStartup方法。
其中有一個DispatcherServletRegistrationBean,這個類實作了ServletContextInitializer接口,主要是用來添加DispatchServlet。
DispatcherServletAutoConfiguration配置類中有DispatcherServlet,DispatcherServletRegistrationBean兩個Bean。
protected static class DispatcherServletRegistrationConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
// 建立DispatcherServletRegistrationBean,并把dispatcherServlet傳進去
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
}
protected static class DispatcherServletConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
// 建立DispatcherServlet
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
return dispatcherServlet;
}
}
複制代碼
ServletContextInitializer.onStartup方法由子類RegistrationBean實作
public final void onStartup(ServletContext servletContext) throws ServletException {
String description = getDescription();
if (!isEnabled()) {
logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");
return;
}
// register是一個抽象方法,由子類DynamicRegistrationBean實作
register(description, servletContext);
}
protected abstract void register(String description, ServletContext servletContext);
複制代碼
DynamicRegistrationBean.register
protected final void register(String description, ServletContext servletContext) {
// addRegistration是一個抽象方法,由子類ServletRegistrationBean實作
D registration = addRegistration(description, servletContext);
if (registration == null) {
logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)");
return;
}
// Servlet被添加到Context後,這裡對Servlet進行配置,如攔截路徑
configure(registration);
}
protected abstract D addRegistration(String description, ServletContext servletContext);
複制代碼
ServletRegistrationBean.addRegistration,作用類似下面
<servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet> 複制代碼
protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {
String name = getServletName();
// 添加Servlet到Context中,這裡的servlet就是DispatchServlet。
return servletContext.addServlet(name, this.servlet);
}
複制代碼
ServletRegistrationBean.configure,作用類似下面
<servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> 複制代碼
protected void configure(ServletRegistration.Dynamic registration) {
super.configure(registration);
String[] urlMapping = StringUtils.toStringArray(this.urlMappings);
if (urlMapping.length == 0 && this.alwaysMapUrl) {
// DEFAULT_MAPPINGS默是“/”
urlMapping = DEFAULT_MAPPINGS;
}
if (!ObjectUtils.isEmpty(urlMapping)) {
// 設定mapping
registration.addMapping(urlMapping);
}
registration.setLoadOnStartup(this.loadOnStartup);
if (this.multipartConfig != null) {
registration.setMultipartConfig(this.multipartConfig);
}
}
複制代碼
至此,DispatchServlet已配置好,後續流程和web.xml配置調用流程基本相同。
FrameworkServlet.initWebApplicationContext()
protected WebApplicationContext initWebApplicationContext() {
// 此處擷取根容器,就是Spring初始化的XmlWebApplicationContext,
// 在上面把它添加到了ServletContext的屬性中,标記根容器,這裡把它擷取出來
// String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
// servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
// ===========上面為使用web.xml時的分析,下面為SpringBoot嵌入式Tomcat分析============
// 同樣是擷取根容器,不過一般為AnnotationConfigServletWebServerApplicationContext
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
// 此時webApplicationContext還是null,因為DispatchServlet是被tomcat建立的,需要無參構造器
// 構造器中沒有設定webApplicationContext的代碼,是以此時webApplicationContext還是null
// ===========上面為使用web.xml時的分析,下面為SpringBoot嵌入式Tomcat分析============
// 注意:在SpringBoot使用嵌入式Tomcat時,這個webApplicationContext不為null,因為FrameworkServlet還
// 實作了ApplicationContextAware接口,是以當SpringBoot的上下文準備好之後,會回調setApplicationContext方法
// 注入ApplicationContext,後面在細說
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext cwac && !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
// 此處主要是擷取web.xml配置的WebApplicationContext
// 可以通過設定參數contextAttribute來設定加載SpringMVC的ApplicationContext
// 比如下面這樣。除非項目中有多個WebApplicationContext,需要使用其他WebApplicationContext才會用到
// 一般都是null
// <context-param>
// <param-name>contextAttribute</param-name>
// <param-value>myWebApplicationContext</param-value>
// </context-param>
// ===========上面為使用web.xml時的分析,下面為SpringBoot嵌入式Tomcat分析
// 因為wac此時不為null,這裡不會進入
wac = findWebApplicationContext();
}
if (wac == null) {
// 現在進入到建立SpringMVC的ApplicationContext流程
// 也就是加載contextConfigLocation定義的xml檔案
// ===========上面為使用web.xml時的分析,下面為SpringBoot嵌入式Tomcat分析
// 因為wac此時不為null,這裡不會進入,是以沒有SpringMVC的容器,也就是沒有父子容器之分,SpringBoot項目中隻有一個容器
// 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) {
// 初始化政策對象
// 比如:HandlerMapping,HandlerAdapter,ViewResolver等等
onRefresh(wac);
}
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
作者:一隻小小的Bug
連結:https://juejin.cn/post/7217086166418456633
來源:稀土掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。