SpringMVC 原理 設計原理、啟動過程、請求處理詳細解讀 目錄 "一、 設計原理" "二、 啟動過程" "三、 請求處理" 一、 設計原理 Servlet 規範 SpringMVC 是基於 Servlet 的。 Servlet 是運行在 web 伺服器上的程式,它接收並響應來自 web 客戶端 ...
SpringMVC 原理 - 設計原理、啟動過程、請求處理詳細解讀
目錄
一、 設計原理
Servlet 規範
SpringMVC 是基於 Servlet 的。
Servlet 是運行在 web 伺服器上的程式,它接收並響應來自 web 客戶端的請求(通常是 HTTP 請求)。
Servlet 規範有三個主要的技術點: Servlet, Filter, Listener
1. Servlet
Servlet 是實現 Servlet 介面的程式。對於 HTTP, 通常繼承 javax.servlet.http.HttpServlet, 可以為不同的 URL 配置不同的 Servlet。Servlet 是"單例"的,所有請求共用一個 Servlet, 因此對於共用變數(比如實例變數),需要自己保證其線程安全性。DispatcherServlet 便是一個 Servlet。
Servlet 生命周期
Servlet 實例化後,Servlet 容器會調用
init
方法初始化。init 只會被調用一次,且必須成功執行才能提供服務。客戶端每次發出請求,Servlet 容器調用
service
方法處理請求。Servlet 被銷毀前,Servlet 容器調用
destroy
方法。通常用來清理資源,比如記憶體,文件,線程等。
2. Filter
Filter 是過濾器,用於在客戶端的請求訪問後端資源之前,攔截這些請求;或者在伺服器的響應發送回客戶端之前,處理這些響應。只有通過 Filter 的請求才能被 Servlet 處理。
Filter 可以通過配置(xml 或 java-based)攔截特定的請求,在 Servlet 執行前後(由 chain.doFilter 劃分)處理特定的邏輯,如許可權過濾,字元編碼,日誌列印,Session 處理,圖片轉換等
Filter 生命周期
Filter 實例化後,Servlet 容器會調用
init
方法初始化。init 方法只會被調用一次,且成功執行(不拋出錯誤且沒有超時)才能提供過濾功能客戶端每次發出請求,Servlet 容器調用
doFilter
方法攔截請求。public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, ServletException { // 客戶端的請求訪問後端資源之前的處理邏輯 System.out.println("我在 Servlet 前執行"); // 把請求傳回過濾鏈,即傳給下一個 Filter, 或者交給 Servlet 處理 chain.doFilter(request,response); // 伺服器的響應發送回客戶端之前的處理邏輯 System.out.println("我在 Servlet 後執行"); }
Filter 生命周期結束時調用
destroy
方法,通常用來清理它所持有的資源,比如記憶體,文件,線程等。
3. Listener
Listener 是監聽某個對象的狀態變化的組件,是一種觀察者模式。
被監聽的對象可以是域對象 ServletContext, Session, Request
監聽的內容可以是域對象的創建與銷毀,域對象屬性的變化
ServletContext | HttpSession | ServletRequest | |
---|---|---|---|
對象的創建與銷毀 | ServletContextListener | HttpSessionListener | ServletRequestListener |
對象的屬性的變化 | ServletContextAttributeListener | HttpSessionAttributeListener | ServletRequestAttributeListener |
ContextLoaderListener 是一個 ServletContextListener, 它會監聽 ServletContext 創建與銷毀事件
public interface ServletContextListener extends EventListener {
// 通知 ServletContext 已經實例化完成了,這個方法會在所有的 servlet 和 filter 實例化之前調用
public void contextInitialized(ServletContextEvent sce);
// 通知 ServletContext 將要被銷毀了,這個方法會在所有的 servlet 和 filter 調用 destroy 之後調用
public void contextDestroyed(ServletContextEvent sce);
}
Servlet 的配置與 SpringMVC 的實現
通過 web.xml
這個是以前常用的配置方式。Servlet 容器會在啟動時載入根路徑下 /WEB-INF/web.xml 配置文件。根據其中的配置載入 Servlet, Listener, Filter 等,然後根據 Servlet 規範調用相應的方法。下麵是 SpringMVC 的常見配置:
<listener>
<!--監聽器,用來管理 root WebApplicationContext 的生命周期:載入、初始化、銷毀-->
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<!--root web application context, 通過 ContextLoaderListener 載入-->
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<context-param>
<!--可以不配置,預設為 XmlWebApplicationContext-->
<param-name>contextClass</param-name>
<!--WebApplicationContext 實現類-->
<param-value>org.springframework.web.context.support.XmlWebApplicationContext</param-value>
<context-param>
<servlet>
<servlet-name>dispatcher</servlet>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<!--DispatchServlet 持有的 WebApplicationContext-->
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
<load-on-startup>1</load-on-startup>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatch</servlet-name>
<servlet-pattern>/*</servlet-pattern>
</servlet-mapping>
通過 ServletContainerInitializer
這個是 Servlet 3.0 的規範,新的 code-based 的配置方式。簡單來說就是容器會去載入文件JAR包下 META-INF/services/javax.servlet.ServletContainerInitalizer 文件中聲明的 ServletContainerInitalizer(SCI) 實現類,並調用他的 onStartup 方法,可以通過 @HandlesTypes 註解將特定的 class 添加到 SCI。
在 spring-web 模塊下有個文件 META-INF/services/javax.servlet.ServletContainerInitializer, 其內容為
org.springframework.web.SpringServletContainerInitializer
SpringServletContainerInitializer
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
它會探測並載入 ClassPath 下 WebApplicationContextInitializer 的實現類,調用它的 onStartUp 方法。
簡單來說,只要 ClassPath 下存在 WebApplicationContextInitializer 的實現類,SpringMVC 會自動發現它,並且調用他的 onStartUp 方法
下麵是一個 SpringMVC 的 java-based 配置方式:
public class MyWebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
// 創建根容器
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
rootContext.register(AppConfig.class);
// 創建 ContextLoaderListener
// 用來管理 root WebApplicationContext 的生命周期:載入、初始化、銷毀
container.addListener(new ContextLoaderListener(rootContext));
// 創建 dispatcher 持有的上下文容器
AnnotationConfigWebApplicationContext dispatcherContext = new AnnotationConfigWebApplicationContext();
dispatcherContext.register(DispatcherConfig.class);
// 註冊、配置 dispatcher servlet
ServletRegistration.Dynamic dispatcher = container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}
}
可見,它和上面 web.xml 配置方式基本一致,也配置了 ContextLoaderListener 和 DispatcherServlet 以及其持有的 application context,不過通過代碼實現,邏輯更加清晰。
如果每次都需要創建 ContextLoaderListener 和 DispatcherServlet,顯然很麻煩,不符合 KISS 原則(keep it simple and stupid)。
SpringMVC 為 WebApplicationInitializer 提供了基本的抽象實現類
代碼實現這裡不再贅述,總之就是利用模版方法,調用鉤子方法。子類只需提供少量的配置即可完成整個邏輯的創建。
所以,更簡單的方法是繼承 AbstractAnnotationConfigDispatcherServletInitializer
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
WebApplicationContext
SpringMVC 用 Spring 化的方式來管理 web 請求中的各種對象。
什麼是 Spring 化? IOC 和 AOP, 這不是本文的重點,具體自行查閱。
SpringMVC 會通過 WebApplicationContext 來管理伺服器請求中涉及到的各種對象和他們之間的依賴關係。我們不需要花費大量的精力去理清各種對象之間的複雜關係,而是以離散的形式專註於單獨的功能點。
WebApplicationContext 繼承自 ApplicationContext, 它定義了一些新的作用域,提供了 getServletContext 介面。
public interface WebApplicationContext extends ApplicationContext {
// 根容器名,作為 key 存儲在 ServletContext 中; ServletContext 持有的 WebApplicationContext
String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
/**
* 這三個是 WebApplicationContext 特有的作用域
* 通過 WebApplicationContextUtils.registerWebApplicationScopes 註冊相應的處理器
*/
String SCOPE_REQUEST = "request";
String SCOPE_SESSION = "session";
String SCOPE_APPLICATION = "application";
/**
* ServletContext 在 WebApplicationContext 中的名字
* 通過 WebApplicationContextUtils.registerEnvironmentBeans 註冊到 WebApplicationContext 中
*/
String SERVLET_CONTEXT_BEAN_NAME = "servletContext";
/**
* ServletContext 和 ServletConfig 配置參數在 WebApplicationContext 中的名字
* ServletConfig 的參數具有更高的優先順序,會覆蓋掉 ServletContext 中的參數
* 值為 Map<String, String> 結構
*/
String CONTEXT_PARAMETERS_BEAN_NAME = "contextParameters";
/**
* ServletContext 屬性信息在 WebApplicationContext 中的名字
* 值為 Map<String, String> 結構
* 屬性是用來描述 ServletContext 本身的一些信息的
*/
String CONTEXT_ATTRIBUTES_BEAN_NAME = "contextAttributes";
/**
* 獲取 ServletContext 介面
*/
@Nullable
ServletContext getServletContext();
}
WebApplicationContext 類圖
ApplicationContext 有一個抽象實現類 AbstractApplicationContext, 模板方法的設計模式。它有一個 refresh 方法,它定義了載入或初始化 bean 配置的基本流程。後面的實現類提供了不同的讀取配置的方式,可以是 xml, annotation, web 等,並且可以通過模板方法定製自己的需求。
AbstractApplicationContext 有兩個實現體系, 他們的區別是每次 refresh 時是否會創建一個新的 DefaultListableBeanFactory。DefaultListableBeanFactory 是實際存放 bean 的容器, 提供 bean 註冊功能。
AbstractRefreshableApplicationContext 這個 refreshable 並不是指 refresh 這個方法,而是指 refreshBeanFactory 這個方法。他會在每次 refresh 時創建一個新的 BeanFactory(DefaultListableBeanFactory)用於存放 bean,然後調用 loadBeanDefinitions 將 bean 載入到新創建的 BeanFactory。
GenericApplicationContext 內部持有一個 DefaultListableBeanFactory, 所以可以提前將 Bean 載入到 DefaultListableBeanFactory, 它也有 refreshBeanFactory 方法,但是這個方法啥也不做。
根據讀取配置的方式,可以分成 3 類,基於 xml 的配置, 基於 annotation 的配置和基於 java-based 的配置
基於 xml 的配置使用 xml 作為配置方式, 此類的名字都含有 Xml, 比如從文件系統路徑讀取配置的 FilePathXmlApplicationContext, 從 ClassPath 讀取配置的 ClassPathXmlApplicationContext, 基於 Web 的 XmlWebApplicationContext 等
基於註解的配置通過掃描指定包下麵具有某個註解的類,將其註冊到 bean 容器,相關註解有 @Component, @Service, @Controller, @Repository,@Named 等
java-based 的配置方式目前是大勢所趨,結合註解的方式使用簡單方便易懂,主要是 @Configuration 和 @Bean
上面幾個類是基礎類,下麵是 SpringMVC 相關的 WebApplicationContext
XmlWebApplicationContext 和 AnnotationConfigWebApplicationContext 繼承自 AbstractRefreshableApplicationContext,表示它們會在 refresh 時新創建一個 DefaultListableBeanFactory, 然後 loadBeanDefinitions。 它們分別從 xml 和 註解類(通常是 @Configuration 註解的配置類)中讀取配置信息。
XmlEmbeddedWebApplicationContext 和 AnnotationConfigEmbeddedWebApplicationContext 繼承自 GenericApplicationContext,表示他們內部持有一個 DefaultListableBeanFactory, 從名字可以看出它們是用於 "Embedded" 方面的,即 SpringBoot 嵌入容器所使用的 WebApplicationContext
SpringMVC 應用中幾乎所有的類都交由 WebApplicationContext 管理,包括業務方面的 @Controller, @Service, @Repository 註解的類, ServletContext, 文件處理 multipartResolver, 視圖解析器 ViewResolver, 處理器映射器 HandleMapping 等。
refresh 流程
AbstractApplicationContext#refresh
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
prepareRefresh();
// XmlWebApplicationContext 和 AnnotationConfigWebApplicationContext 會在這裡執行 refreshBeanFactory
// 創建一個新的 DefaultListableBeanFactory 然後從 xml 或 java-config 配置中 loadBeanDefinitions
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
prepareBeanFactory(beanFactory);
try {
// 在這裡會配置一些 web 相關的東西,註冊 web 相關的 scope
postProcessBeanFactory(beanFactory);
// 下麵步驟和初始化其他 ApplicationContext 基本一致,忽略
invokeBeanFactoryPostProcessors(beanFactory);
registerBeanPostProcessors(beanFactory);
initMessageSource();
initApplicationEventMulticaster();
onRefresh();
registerListeners();
finishBeanFactoryInitialization(beanFactory);
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
destroyBeans();
cancelRefresh(ex);
throw ex;
}
finally {
resetCommonCaches();
}
}
}
AbstractRefreshableWebApplicationContext 重寫了 postProcessBeanFactory 方法
AbstractRefreshableWebApplicationContext#postProcessBeanFactory
@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// servlet-context aware 後處理器
beanFactory.addBeanPostProcessor(new ServletContextAwareProcessor(this.servletContext, this.servletConfig));
beanFactory.ignoreDependencyInterface(ServletContextAware.class);
beanFactory.ignoreDependencyInterface(ServletConfigAware.class);
// 註冊 scope: request, session, application;
// bean 依賴: ServletRequest, ServletResponse, HttpSession, WebRequest, beanFactory
WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, this.servletContext);
// 註冊 servletContext, servletConfig, contextParameters, contextAttributes
WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, this.servletContext, this.servletConfig);
}
這些方法都比較簡單,不再展開
SpringMVC 通過兩種方式創建 WebApplicationContext
一種是通過 ContextLoaderListener, 它創建的 WebApplicationContext 稱為 root application context,或者說根容器。一個 ServletContext 中只能有一個根容器,而一個 web application 中只能有一個 ServletContext,因此一個 web 應用程式中只能有一個根容器,根容器的作用和 ServletContext 類似,提供了一個全局的訪問點,可以用於註冊多個 servlet 共用的業務 bean。 根容器不是必要的。
另一種是通過 DispatcherServlet, 它創建的 WebApplicationContext,稱為上下文容器,上下文容器只在 DispatcherServlet 範圍內有效。DispatcherServlet 本質上是一個 Servlet,因此可以有多個 DispatcherServlet,也就可以有多個上下文容器。
如果上下文容器的 parent 為 null, 並且當前 ServletContext 中存在根容器,則把根容器設為他的父容器。
二、 啟動過程
ContextLoaderListener
一般我們會配置(web.xml 或 java-based)一個 ContextLoaderListener, 它實現了 ServletContextListener 介面, 主要用來載入根容器。
根據 Servelet 規範,這個 Listener 會在 ServletContext 創建時執行 ServletContextListener#contextInitialized 方法。
相關代碼如下:
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
ContextLoader#initWebApplicationContext
/**
* Initialize Spring's web application context for the given servlet context,
* using the application context provided at construction time, or creating a new one
* according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and
* "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.
* @param servletContext current servlet context
* @return the new WebApplicationContext
* @see #ContextLoader(WebApplicationContext)
* @see #CONTEXT_CLASS_PARAM
* @see #CONFIG_LOCATION_PARAM
*/
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 當前 ServletContext 中是否已經存在 root web applicationContext
// 一個 ServletContext 中只能有一個根容器
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();
try {
// context 可以通過構造方法傳入(這個在 java config 方式會用到)
if (this.context == null) {
// 若 web application 為空,創建一個, 這個一般是 web.xml 方式配置的
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
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 ->
// determine parent for root web application context, if any.
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// 設置 ID, ServletContext, contextConfigLocation
// 執行 refresh 操作
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 將 web application context 放進 servlet context 中
// 因此可以調用 servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) 拿到這個 WebApplicationContext
// 更簡單的方法是通過 SpringMVC 提供的工具類 WebApplicationContextUtils.getWebApplicationContext(servletContext)
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
}
return this.context;
}
catch (RuntimeException | Error ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}
如果 context 為 null, 則創建一個
ContextLoader#createWebApplicationContext
// web.xml 配置方式需要調用此方法創建 WebApplicationContext
// java-config 一般通過構造方法傳入,不需要再創建,上面有示例
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
// 決定使用哪個 WebApplicationContext 的實現類
Class<?> contextClass = determineContextClass(sc);
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
}
// 調用工具類實例化一個 WebApplicationContext
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
ContextLoader#determineContextClass 根據 ContextLoader.CONTEXT_CLASS_PARAM
確定使用哪個 WebApplicationContext 的實現類。
protected Class<?> determineContextClass(ServletContext servletContext) {
// CONTEXT_CLASS_PARAM = "contextClass", 即在 web.xml 中配置的初始化參數 contextClass
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
if (contextClassName != null) {
try {
return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load custom context class [" + contextClassName + "]", ex);
}
}
else {
// 如果未配置 contextClass, 從 defaultStrategies 屬性文件中獲取,下麵會說到
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load default context class [" + contextClassName + "]", ex);
}
}
}
若 contextClass 未指定,則從 defaultStrategies 這個 Properties 中獲取,他預設載入 ClassPath 路徑下, ContextLoader.properties 文件中配置的類,預設為 XmlWebApplicationContext。
// 屬性文件中的類為 XmlWebApplicationContext。
private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
private static final Properties defaultStrategies;
// 靜態載入 XmlWebApplicationContext 到 defaultStrategies 中
static {
// Load default strategy implementations from properties file.
// This is currently strictly internal and not meant to be customized
// by application developers.
try {
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
}
}
確定 class 後,反射實例化一個 WebApplicationContext 的實現類,一個"裸"的根容器創建出來了。
想一想,平時為啥創建 ApplicationContext?
作為 Bean 容器,當然是用來管理 Bean 了。
既然用來管理 Bean,是不是應該先把 Bean 放進去? 通過 xml? 註解? 或者乾脆直接調用 register 方法註冊? 然後是不是應該 refresh 一下?配置一些 post-processer,設置一些參數,提前創建 Singleton?
ContextLoader#configureAndRefreshWebApplicationContext
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
else {
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
// WebApplication 會持有當前 ServletContext
wac.setServletContext(sc);
// CONFIG_LOCATION_PARAM = "contextConfigLocation", web.xml 裡面配置參數
// root web application context 的 Bean 配置文件
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
// 初始化屬性資源,占位符等
// 在這裡調用確保 servlet 屬性資源在 post-processing 和 initialization 階段是可用的
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
// ApplicationContextInitializer 回調介面,在 refresh 之前定製一些信息
customizeContext(sc, wac);
// 所有的 ApplicationContext 調用 refresh 之後才可用,此方法位於
// AbstractApplication,它統一了 ApplicationContext 初始化的基本
// 流程,子類(包括 WebApplicationContext 的實現類)通過鉤子方法
//(模版方法)定製一些自己的需求
// web refresh 流程上面以已經說過
wac.refresh();
}
小結: ContextLoaderListener 的初始化流程可以用下麵的代碼表示
def initWebApplicationContext():
if context == null:
context = createWebApplicationContext()
configureAndRefreshWebApplicationContext(context)
DispatcherServlet 初始化流程
SpringMVC 將前端的所有請求都交給 DispatcherServlet 處理,他本質上是一個 Servlet,可以通過 web.xml 或者 java config 方式配置。
DispatcherServlet 類圖
SpringMVC 將 DispatcherServlet 也當做一個 bean 來處理,所以對於一些 bean 的操作同樣可以作用於 DispatcherServlet, 比如相關 *Aware 介面。
Servlet 容器會在啟動時調用 init 方法。完成一些初始化操作,其調用流程如下:
HttpServletBean#init -> FrameworkServlet#initServletBean -> FrameworkServlet#initWebApplicationContext
前面兩個方法比較簡單,主要是 initWebApplicationContext
FrameworkServlet#initWebApplicationContext
/**
* Initialize and publish the WebApplicationContext for this servlet.
* <p>Delegates to {@link #createWebApplicationContext} for actual creation
* of the context. Can be overridden in subclasses.
* @return the WebApplicationContext instance
* @see #FrameworkServlet(WebApplicationContext)
* @see #setContextClass
* @see #setContextConfigLocation
*/
protected WebApplicationContext initWebApplicationContext() {
// 獲取根容器,ServletContext 持有的 WebApplicationContext
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
// 上下文容器,當前 DispatcherServlet 持有的 WebApplicationContext
WebApplicationContext wac = null;
// web application 可以通過構造方法傳入, java-config 方式會用到
if (this.webApplicationContext != null) {
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);
}
// 配置刷新 web application context, 下麵會說到
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) {
// 如果通過 web.xml 方式配置,此時 wac 為空,創建一個,預設 XmlWebApplicationContext
// 配置文件位置 contextConfigLocation 在這裡載入
// 這個方法比較簡單,不再贅述
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
// 初始化 SpringMVC 處理過程中面向不同功能的策略對象
// 比如 MultipartResolver, HandlerMappings, ViewResolvers 等
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
if (this.publishContext) {
// 將 DispatcherServlet 持有的 web application context 放進 ServletContext
// 命名規則為 SERVLET_CONTEXT_PREFIX + dispatcherServlet 名字
// SERVLET_CONTEXT_PREFIX = FrameWorkServlet.class.getName() + ".CONTEXT."
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
DispatcherServlet 持有的 WebApplicationContext 可以通構造方法傳入,或者 createWebApplicationContext 方法創建
創建容器步驟和 ContextLoader#createWebApplicationContext 有所不同
FrameworkServlet#createWebApplicationContext
// web.xml 配置方式需要調用此方法創建 WebApplicationContext
// java-config 一般通過構造方法傳入,不需要再創建,上面有示例
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
// 返回 WebApplicationContext 的實現類,預設為 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");
}
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(getEnvironment());
// parent 為 rootContext
wac.setParent(parent);
// 獲 bean 取配置文件位置
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
// 配置,刷新容器,下麵會說到
configureAndRefreshWebApplicationContext(wac);
return wac;
}
FrameworkServlet#configureAndRefreshWebApplicationContext
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
if (this.contextId != null) {
wac.setId(this.contextId);
}
else {
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
}
}
// 配置 Servlet 相關信息
wac.setServletContext(getServletContext());
wac.setServletConfig(getServletConfig());
wac.setNamespace(getNamespace());
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
// 初始化屬性資源,占位符等
// 在這裡調用確保 servlet 屬性資源在 post-processing 和 initialization 階段是可用的
((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
}
// 空方法,可以在 refresh 之前配置一些信息
postProcessWebApplicationContext(wac);
// ApplicationContextInitializer 回調介面
applyInitializers(wac);
// 所有的 ApplicationContext 調用 refresh 之後才可用,此方法位於
// AbstractApplication,它統一了 ApplicationContext 初始化的基本
// 流程,子類(包括 WebApplicationContext 的實現類)通過鉤子方法
//(模版方法)定製一些自己的需求
// web refresh 流程上面以已經說過
wac.refresh();
}
DispatcherServlet#onRefresh
/**
* This implementation calls {@link #initStrategies}.
*/
@Override
protected void onRefresh(ApplicationContext context) {
// 初始化面向不同功能的策略對象
initStrategies(context);
}
DispatcherServlet#initStrategies
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
這些策略方法的執行流程都是相似的,即從當前 context 中查找相應類型、相應名字的 bean,將他設為當前 DispatcherServlet 的成員變數。對於必須存在的 bean, 通過 DispatcherServlet.properties 文件提供。下麵以 initHandlerMappings 為例說明
DispatcherServlet#initHandlerMappings
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
// 預設為 true, 表示查找所有的 HandlerMappings 實現類
if (this.detectAllHandlerMappings) {
// 從 ApplicationContext(包括父容器)中查找所有的 HandlerMappings
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
// 只載入名字為 handlerMapping 的 HandlerMapping
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
// 確保至少有一個 HandlerMapping
if (this.handlerMappings == null) {
// 載入預設的 HandlerMapping, 下麵會說到
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
DispatcherServlet#getDefaultStrategies
protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
String key = strategyInterface.getName();
// 從 defaultStrategies 這個 Properties 中獲取
String value = defaultStrategies.getProperty(key);
// 後面反射創建 value 類,省略
...
}
下麵位於 DispatcherServlet
// 靜態載入 DispatcherServlet.properties 文件中的類到 defaultStrategies
static {
try {
// DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
}
}
因此可以根據需求,在 DispatcherServlet#onRefresh 之前將需要的策略類註冊進 context, 它們會在 onRefresh 之後生效。
小結: DispatcherServlet 的初始化流程可以表示為
def initWebApplicationContext():
rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext())
if context == null:
context = createWebApplicationContext(rootContext)
context.setParent(rootContext)
configureAndRefreshWebApplicationContext(context)
onRefresh(context)
三、 請求處理
DispatcherServlet 處理流程
DispatcherServlet 中主要組件的簡析
Handler
處理器。請求對應的處理方法,@Controller 註解的類的方法
HandlerInterceptor
攔截器。在 handler 執行前後及視圖渲染後執行攔截,可以註冊不同的 interceptor 定製工作流程
public interface HandlerInterceptor {
// 在 handler 執行前攔截,返回 true 才能繼續調用下一個 interceptor 或者 handler
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
// 在 handler 執行後,視圖渲染前進行攔截處理
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
// 視圖渲染後,請求完成後進行處理,可以用來清理資源
// 除非 preHandle 放回 false,否則一定會執行,即使發生錯誤
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
HandlerExecutionChain
處理器執行鏈。裡面包含 handler 和 interceptors
HandlerMapping
處理器映射器。request -> handler 的映射。主要有 BeanNameUrlHandlerMapping 和 RequestMappingHandlerMapping 兩個實現類
BeanNameUrlHandlerMapping 將 bean 名字作為 url 映射到相應的 handler, 也就是說 bean 名字必須是這種形式的: "/foo", "/bar",這個應該是比較老的東西了
RequestMappingHandlerMapping 使用 @RequestMapping 註解將 url 和 handler 相關聯。
HandlerAdapter
處理器適配器。適配器模式,通過他來調用具體的 handler
ViewResolver
視圖解析器。其中的 resolveViewName 方法可以根據視圖名字,解析出對應的 View 對象。可以配置不同的 viewResolver 來解析不同的 view, 常見的如 Jsp, Xml, Freemarker, Velocity 等
View
視圖。不同的 viewResolver 對應不同 View 對象,調用 view.render 方法渲染視圖
SpringMVC 處理請求流程圖
- 客戶端發出請求,會先經過 filter 過濾,通過的請求才能到達 DispatcherServlet。
- DispatcherServlet 通過 handlerMapping 找到請求對應的 handler,返回一個 HandlerExecutionChain 裡面包含 interceptors 和 handler
- DispatcherServlet 通過 handlerAdapter 調用實際的 handler 處理業務邏輯, 返回 ModelAndView。裡面會包含邏輯視圖名和 model 數據。註意,在此之前和之後,會分別調用 interceptors 攔截處理
- 調用 viewResolver 將邏輯視圖名解析成 view 返回
- 調用 view.render 渲染視圖,寫進 response。然後 interceptors 和 filter 依次攔截處理,最後返回給客戶端
下麵結合源碼看一看
DispatcherServlet 請求處理源碼解析
DispatcherServlet 是一個 servlet,他的調用流程大致如下
HttpServlet#service -> FrameworkServlet#processRequest -> DispatcherServlet#doService -> DispatcherServlet#doDispatch
DispatcherServlet#doDispatch
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 檢查是否為文件上傳請求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 通過 handlerMapping 查到請求對應的 handler
// 返回 HandlerExecutionChain 裡面包含 handler 和 interceptors
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 根據 handler 匹配對應的 handlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 攔截器前置處理,調用 HandlerInterceptor#preHandle
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 調用 handler, 就是 @Controller 註解的類對應的方法
// 如果是一個 rest 請求,mv 為 null,後面不會再調用 render 方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
// 設置 viewName, 後面會根據 viewName 找到對應的 view
applyDefaultViewName(processedRequest, mv);
// 攔截器後置處理,調用 HandlerInterceptor#postHandle
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 結果處理,錯誤,視圖等
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
// 攔截器結束處理, 調用 HandlerInterceptor#afterCompletion
// 即使發生錯誤也會執行
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
processDispatchResult 會進行異常處理(如果存在的話),然後調用 render 方法渲染視圖
DispatcherServlet#render
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
View view;
// 這個是 @Controller 方法返回的名字
String viewName = mv.getViewName();
if (viewName != null) {
// 調用 viewResolver 解析視圖,返回一個視圖對象
// 會遍歷 viewResolvers 找到第一個匹配的處理, 返回 View 對象
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// 已經有視圖對象
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
if (logger.isTraceEnabled()) {
logger.trace("Rendering view [" + view + "] ");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
// 渲染,不同的 viewResolver 會有不同的邏輯實現
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex);
}
throw ex;
}
}
總結
SpringMVC 是基於 Servlet 的, 因此 SpringMVC 的啟動流程基於 Servlet 的啟動流程
ServletContext 持有的 WebApplicationContext 稱為根容器; 根容器在一個 web 應用中都可以訪問到,因此可以用於註冊共用的 bean;如果不需要可以不創建,根容器不是必要的
根容器是指在 ServletContext 中以 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 為 key 的 WebApplicationContext。根容器並不一定要由 ContextLoaderListener 創建。
DispatcherServlet 持有的 WebApplicationContext 稱為它的上下文容器;每個 DispatcherServlet 都會持有一個上下文容器(自己創建或者構造傳入)。
SpringMVC 的處理流程並不一定按上面的順序執行,比如如果是 json 請求,HandlerAdapter 調用 handler 處理後返回的 mv 可能是 null, 後面就不會進行視圖渲染
請求如果沒有到達 DispatcherServlet 可能是被過濾器過濾了(許可權?異常?);一定不是被攔截器攔截的,因為攔截器在 DispatcherServlet 內部執行。
除非請求被 interceptor#preHandle 攔截,否則 interceptor#afterCompletion 一定會執行,即使發生錯誤。
獲取 WebApplicationContext, 除了相關 Aware 介面,還可以通過 WebApplicationContextUtils.getWebApplicationContext 獲取根容器,相關原理在這裡, 或者通過 RequestContextUtils.findWebApplicationContext 獲取當前 DispatcherServlet 對應的上下文容器,相關代碼在 DispatcherServlet#doService
SpringBoot 應用部署到外部容器的時候為啥要繼承 SpringBootServletInitializer, 因為它是一個 WebApplicationContextInitializer, 具體見這裡
備註
以上相關代碼基於 SpringBoot 2.1.6.RELEASE, SpringMVC 5.1.6.RELEASE, Servlet 3.0
結語
寫了不少,算是對 SpringMVC 的一次複習了。能力有限,如有不正確的地方,歡迎拍磚!
參考: