這節介紹SpringMVC,SpringMVC是一種基于JAVA的實現MVC設計模式的請求驅動類型的輕量級Web框架。本章會介紹相關概念,流程,再從源碼進行講解。
1. MVC
MVC(Model View Controller)是一種軟件設計的框架模式,它采用模型(Model)-視圖(View)-控制器(controller)的方法把業務邏輯、數據與界面顯示分離。MVC框架模式是一種復合模式,MVC的三個核心部件分別是
- Model(模型):所有的用戶數據、狀態以及程序邏輯,獨立于視圖和控制器
- View(視圖):呈現模型,類似于Web程序中的界面,視圖會從模型中拿到需要展現的狀態以及數據,對于相同的數據可以有多種不同的顯示形式(視圖)
- Controller(控制器):負責獲取用戶的輸入信息,進行解析并反饋給模型,通常情況下一個視圖具有一個控制器
2. SpringMVC流程
基本上大家都會在網上看到這張圖:
這個圖描述了SpringMVC處理一個Http請求的基本流程,對應的流程為:
- 用戶發送請求至前端控制器DispatcherServlet。
- DispatcherServlet收到請求調用HandlerMApping處理器映射器。
- 處理器映射器找到具體的處理器(可以根據xml配置、注解進行查找),生成處理器對象及處理器攔截器(如果有則生成)一并返回給DispatcherServlet。
- DispatcherServlet調用HandlerAdapter處理器適配器。
- HandlerAdapter經過適配調用具體的處理器(Controller,也叫后端控制器)。
- Controller執行完成返回ModelAndView。
- HandlerAdapter將controller執行結果ModelAndView返回給DispatcherServlet。
- DispatcherServlet將ModelAndView傳給ViewReslover視圖解析器。
- ViewReslover解析后返回具體View.
- DispatcherServlet根據View進行渲染視圖(即將模型數據填充至視圖中)。
- DispatcherServlet響應用戶。
還有大家都會接觸到的demo:
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
在applicationContext.xml,指定包的掃描訪問并添加標簽
<context:component-scan base-package="xxx" />
<mvc:annotation-driven />
添加Controller
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/hello")
public String excute() {
return "hello";
}
}
上面表示的意思為:
- 開啟ContextLoaderListener加載Spring根Context,對應的配置文件為applicationContext.xml
- 開啟DispatcherServlet監聽/*下的所有請求,加載WebContext,對應的配置文件為dispatcher-servlet.xml
- 指定掃描包路徑,并開啟mvc注解支持
- 添加對應的Controller,使用@RestController標記為Controller對象并使用@RequestMapping標記處理的請求路徑
3. SpringMVC加載流程
SpringMVC的加載是依賴Servlet切入的,主要依賴兩個技術點:Listener和Servlet。
3.1. ContextLoaderListener的加載
從web.xml中可以知道,ContextLoaderListener依賴于Servlet的Listener技術。Listener是在servlet2.3中加入的,主要用于對session、request、context等進行監控。使用Listener需要實現相應的接口。觸發Listener事件的時候,Tomcat會自動調用相應的Listener的方法。常用的監聽接口包括:
- HttpSessionListener:監聽session的創建和銷毀。
- ServletRequestListener:監聽request的創建和銷毀
- ServletContextListener:監聽context的創建和銷毀。
這里主要使用了ServletContextListener,用于在Servlet初始化前執行自定義動作。
ContextLoaderListener的定義如下:
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
該類繼承了ContextLoader,并通過contextInitialized方法執行了初始化(傳入ServletContext),通過contextDestroyed方法進行資源銷毀回收。重點看ContextLoader方法。
ContextLoader在初始化時,會先執行內部的一個靜態代碼塊:
private static final Properties defaultStrategies;
static {
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());
}
}
這一步會加載classpath下的配置文件ContextLoader.properties,該文件將作為默認配置用于初始化Properties對象defaultStrategies。
3.1.1. contextInitialized
contextInitialized方法的主要內容如下:
過程為:
(1) 判斷當前Context是否已經初始化過
通過判斷ServletContext中是否存在key為org.springframework.web.context.ROOT的值
- 初始化WebApplicationContext:從ContextLoader.properties中查找WebApplicationContext的具體實現,如下:
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
即XmlWebApplicationContext,然后初始化該類
(2) 配置并刷新該XMLWebApplicationContext
XMLWebApplicationContext繼承簡圖如下:
層級比較明顯,本身也是一個RefreshableConfigApplicationContext(具體內容可以看往期內容)。其父類保存了ServletContext和ServletConfig兩個Web Context相關的對象,其本身也維持了一些默認屬性,如
DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";
這個屬性就是默認的Spring配置文件的路徑。
需要指出的是XMLWebApplicationContext重寫了父類的loadBeanDefinitions方法
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// Create a new XmlBeanDefinitionReader for the given BeanFactory.
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
// Configure the bean definition reader with this context's
// resource loading environment.
beanDefinitionReader.setEnvironment(getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
// Allow a subclass to provide custom initialization of the reader,
// then proceed with actually loading the bean definitions.
initBeanDefinitionReader(beanDefinitionReader);
loadBeanDefinitions(beanDefinitionReader);
}
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
String[] configLocations = getConfigLocations();
if (configLocations != null) {
for (String configLocation : configLocations) {
reader.loadBeanDefinitions(configLocation);
}
}
}
@Override
protected String[] getDefaultConfigLocations() {//Tip:返回配置文件路徑
if (getNamespace() != null) {
return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX};
}
else {
return new String[] {DEFAULT_CONFIG_LOCATION};
}
}
這里用了XmlBeanDefinitionReader來解析Bean定義,且指定了配置文件的加載邏輯,getConfigLocations方法:如果父類的configLocations不為空,則返回該值,否則返回getDefaultConfigLocations的值。而getDefaultConfigLocations方法邏輯為:如果存在命名空間,則返回/WEB_INF/namespace.xml作為配置文件,否則返回/WEB-INF/applicationContext.xml。對應上面的demo,將返回配置中的文件(同默認值相同)。
XMLWebApplicationContext的初始化步驟為:
- 讀取contextId配置,進行設置
- 讀取contextConfigLocation配置,使用指定的配置文件,若沒有則使用上面提到的默認配置文件DEFAULTCONFIGLOCATION
- 加載contextInitializerClasses指定的class,用于在context刷新前執行自定義處理
- 調用XMLWebApplicationContext的refresh方法
(3)標記已經初始化
通過將該根Context存在ServletContext中,并設置值為org.springframework.web.context.ROOT,用于第(1)步的判斷
3.1.2. contextDestroyed
銷毀過程比較簡單,首先調用WebApplicationContext的close方法銷毀該Context,然后移除ServletContex中的org.springframework.web.context.ROOT屬性值,最后清除ServletContext中所有org.springframework.開頭的屬性值。
3.2. DispatcherServlet的加載
同ContextLoaderListener類似,DispatcherServlet依賴于Servlet進行擴展。DispatcherServlet的結構如下:
如上,DispatcherServlet繼承自HttpServlet,并重寫了doService方法,用于處理http請求,其中:
3.2.1. HttpServletBean
在HttpServlet的繼承上增加了ConfigurableEnvironment屬性,用于存放Servlet的配置項。通過重寫init方法,在初始化時將servlet配置項添加到上下文環境變量中,并在該方法中開放了initBeanWrapper和initServletBean方法給子類。
3.2.2. FrameworkServlet
基于Servlet實現的Web框架,每個Servlet內部都對應一個XmlWebApplicationContext對象,且namespace格式為ServletName-servlet。上面說了在沒顯示設定配置文件路徑的情況下,且存在namespace時,會使用/WEB-INF/namespace.xml作為Spring配置文件,對應到demo即為/WEB-INF/dispatcher-servlet.xml。FrameworkServlet重寫了父類的intServletBean方法,對XmlWebApplicationContext的初始化工作。Servlet在初始化XmlWebApplicationContext時,會嘗試從ServletContext中獲取根Context(上面提到的,會將根Ccontext放到ServletContext中以標記已經初始化過)并設置為當前Context的父Context,然后再按照雷士根Contextde 的初始化過程對其進行初始化。不同的是,會在refresh前開放口子進行擴展,包括:
- 對內通過重寫子類的postProcessWebApplicationContext方法
- 對外通過加載并執行globalInitializerClasses中配置的ApplicationContextInitializer類
FrameworkServlet還重寫了父類的各doXXX方法,都交給processService方法,以處理Http請求。processService最終委托給了doService方法。
3.2.3. DispatchdrServlet
是SpringMVC處理Http請求的主要實現,主要完成了兩件事:
3.1. 重寫了onRefresh方法
初始化時設置了眾多默認處理策略,包括:文件處理策略、HandlerMapping處理策略、HandlerAdapter處理策略、HandlerException處理策略、View解析策略等。SpringMVC在處理Http的每個步驟上,都提供了類似filter的機制,每個步驟都能夠注冊多個策略處理器,按照順序選擇出能夠處理當前請求的策略并交給其處理。而大部分的默認策略來至于spring-mvc模塊下的org/springframework/web/servlet/DispatcherServlet.properties文件,如下:
下面為本人demo(SpringBoot)運行時DispatcherServlet各屬性以及注冊的各策略的情況
主要關注handlerMappings中的RequestMappingHandlerMapping和handlerAdapters中的RequestMappingHandlerAdapter。這兩個都不是在DispatcherServlet.properties文件中指定的,而是在開啟 后自動注冊的,這個后面會介紹。
3.1.1 RequestMappingHandlerMapping初始化
RequestMappingHandlerMapping主要用于查找@RequestMapping注解的handler,其繼承關系如下:
- AbstractHandlerMapping:實現了HandlerMapping接口,提供了獲取handler的主要實現。getHandler方法的實現為,將具體handler的查找委托給了子類的getHandlerInternal方法,然后跟當前請求路徑相關的interceptor一起包裝為一個HandlerExecutionChain返回。interceptor為所有實現了MappedInterceptor接口的bean,會在AbstractHandlerMapping初始化的時候遍歷上下文進行查找。
- AbstractHandlerMethodMapping:在AbstractHandlerMapping的基礎上,主要提供了根據請求查找對應handler method的實現,即getHandlerInternal方法。該類會在初始化時遍歷上下文中所有的Bean,然后符合條件的Bean(通過isHandler方法),遍歷當前Bean符合條件的方法(通過getMappingForMethod方法),每個方法都有一個對應的path,稱為lookUpPath。getHandlerInternal實現上也是通過請求的HttpServletRequest得到對應的lookUpPath,然后從內存緩存中獲取對應的handler。
- RequestMappingHandlerMapping:@RequestMapping的實現,主要實現了 isHandler和getMappingForMethod。 isHandler:判斷是否出現@Controller注解或者@RequestMapping注解 getMappingForMethod:根據@RequestMapping注解返回RequestMappingInfo實例。
3.1.2 RequestMappingHandlerAdapter初始化
RequestMappingHandlerAdapter主要完成HandlerMethod的執行,,其繼承關系如下:
- AbstractHandlerMethodAdapter:用于判斷是否支持Handler的執行,需要傳入的handler是否為HandlerMethod實例,同時將handler的執行委托給子類的handleInternal方法。
- RequestMappingHandlerAdapter:真正執行handler對應的Method對象,會調用各種resolvers解析參數,用于在反射時作為入參傳入;調用各種converter用于對結果進行加工等操作。
3.2. 重寫doService方法
實現了Http請求的處理過程,具體流程如下圖,即開頭提及的SpringMVC處理Http請求的過程,前面已經介紹過流程,這里不再贅述。
3.3. mvc:annotation-driven
按照之前說的,先看resource/META-INF/spring.handlers文件,這個配置在spring-webmvc模塊下,內容為:
http://www.springframework.org/schema/mvc=org.springframework.web.servlet.config.MvcNamespaceHandler
支持的標簽如下:
annotation-driven的解析類為:AnnotationDrivenBeanDefinitionParser,該類主要自動做了如下動作:
- 注入了RequestMappingHandlerMapping和BeanNameUrlHandlerMapping兩個HandlerMapping實現
- 注入了RequestMappingHandlerAdapter、HttpRequestHandlerAdapter和SimpleControllerHandlerAdapter三個HandlerAdapter實現。需要指出的是對于RequestMappingHandlerAdapter,如果沒有配置message-converters標簽指定消息處理器的話,會根據classpath中存在的包自動注入處理器,包括: ByteArrayHttpMessageConverter StringHttpMessageConverter ResourceHttpMessageConverter SourceHttpMessageConverter AllEncompassingFormHttpMessageConverter 如果存在com.rometools.rome.feed.WireFeed類,則增加AtomFeedHttpMessageConverter、RssChannelHttpMessageConverter 如果存在com.fasterxml.jackson.dataformat.xml.XmlMapper類,則增加MappingJackson2XmlHttpMessageConverter 如果存在javax.xml.bind.Binder類,則增加Jaxb2RootElementHttpMessageConverter 如果存在com.fasterxml.jackson.databind.ObjectMapper和com.fasterxml.jackson.core.JsonGenerator,則增加MappingJackson2HttpMessageConverter 如果存在com.google.gson.Gson,則增加GsonHttpMessageConverter
- 注入了ExceptionHandlerExceptionResolver用于實現@ExceptionHandler注解、注入了ResponseStatusExceptionResolver用于實現@ResponseStatus和DefaultHandlerExceptionResolver
- 注入了AntPathMatcher和UrlPathHelper用于路徑解析
上面介紹了SpringMVC大體流程的實現,當然還有很多細節沒有進行說明,如@Param,HttpServletRequest等各種參數的解析和注入,響應結果轉為json等各種結果的加工,詳細內容可以根據上面介紹再進行深入。
4. WebApplicationInitializer
Servlet3.0+提供了ServletContainerInitializer接口,用于在web容器啟動時為提供給第三方組件機會做一些初始化的工作,例如注冊servlet或者filtes等。
每個框架要使用ServletContainerInitializer就必須在對應的jar包的META-INF/services 目錄創建一個名為javax.servlet.ServletContainerInitializer的文件,文件內容指定具體的ServletContainerInitializer實現類。spring-web模塊下便存在該配置:
內容為:
org.springframework.web.SpringServletContainerInitializer
SpringServletContainerInitializer的主要功能是加載classpath下的所有WebApplicationInitializer實現類(非接口、非抽象類),按照@Order進行排序后依次執行WebApplicationInitializer的onStartup方法。
spring-web模塊提供的抽象類實現AbstractContextLoaderInitializer能夠不用web.xml配置增加RootContext;提供的抽象類實現AbstractDispatcherServletInitializer能夠不用web.xml配置增加DispatcherServlet。當然更重要的實現是SpringBoot中的實現,這個后續介紹SpringBoot時再提。
更多原創內容請搜索微信公眾號:啊駝(doubaotaizi)