spring MVC是嚴格遵守java servlet規范的一種web框架,可以打包成war包交由web容器(Tomcat或者jetty)運行。接下來就來學習下spring mvc的運行過程以及其中的細節,如何和Tomcat無縫合作,如何和spring 本身的核心功能IOC、AOP合作。
MVC 實例
接下來就搭建一個基于maven的spring mvc的實例。
項目結構
StudentController 類
@Controller
@RequestMapping
public class StudentController {
@RequestMapping(value = "/h")
@ResponseBody
public String getStudentInfo() {
return "hello world";
}
}
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_3_1.xsd"
version="3.1">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/applicationContext.xml</param-value>
</context-param>
<servlet>
<servlet-name>demo</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>demo</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.demo.web" />
</beans>
pom.xml
// 插件部分
<build>
<finalName>demo</finalName>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/</path>
<uriEncoding>${file_encoding}</uriEncoding>
<port>9912</port>
<server>tomcat7</server>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.5</version>
</plugin>
<!-- Java Compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>${java_source_version}</source>
<target>${java_target_version}</target>
<encoding>${file_encoding}</encoding>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
然后通過mvn tomcat7:run
就可以正常啟動了
Tomcat 基礎
在介紹spring mvc的工作原理之前,有必要介紹下web容器的一種Tomcat。Tomcat是一個開源的web容器,嚴格準守servlet規范,在Tomcat中包含了各種各樣的組件,層層嵌套依賴,如下圖所示。
catalina是最核心的組件,也可以認為Tomcat是從它開始啟動的,他持有1個server容器,server容器可以包含了多個service容器,每個service容器都持有了一個connector連接器以及一個engine,engine有層層包含了host、context、wrapper等。
其中engine、host、context、wrapper又分別存在各自的一個管道pipeline以及至少一個閥門valve,閥門可以為request和response添加任何外置的功能。
Tomcat啟動是由各自容器的監聽器調用啟動的,按照上面所說的順序依次執行啟動的。
那我們常用的servlet是在哪里的么?他是被wrapper包裝的,每一個wrapper持有一個servlet,所以在xml中配置了幾個servlet,則就會存在多少個wrapper。并且servlet是通過ServletContext傳遞上下文的。在具體的URL映射的時候,會先根據各自的servlet的URL配置在Tomcat的mappingdata中體現,經過host、context、再到選擇不同類型的wrapper(包含了wildcardWrappers、extensionWrappers、defaultWrapper、exactWrappers四種wrapper類型)最后才具體到某一個servlet請求上。
DispatcherServlet init 過程
DispatcherServlet類是spring mvc中實現的了servlet規范的實體類,實現了HttpServlet類,如下圖
首先需要明確一點的是,DispatcherServlet也只是一個HttpServlet類,他是被wrapper調用的init(ServletConfig config)方法進入的,設置好config之后,進入到init()方法。
PS:在當前環境中,ServletConfig是StandardWrapperFacade類,可以從中獲取到在xml配置的例如contextConfigLocation數據
HttpServletBean 類
public final void init() throws ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet '" + getServletName() + "'");
}
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
// 獲取xml配置的屬性,在下面貼的圖片中可以看到,當DispatcherServlet沒有任何配置的時候,就會拋出異常,說缺少必備的配置屬性
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);
// 這一步就是設置DispatcherServlet的屬性值的操作
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// 開始真正的初始化DispatcherServlet類了
initServletBean();
if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}
FrameworkServlet 類
protected final void initServletBean() throws ServletException {
try {
this.webApplicationContext = initWebApplicationContext();
// 這個是初始化當前dispatchservlet的webApplicationContext參數
// 然后該參數中會持有spring的IOC容器等信息,通過這個參數可以獲取bean數據
// 具體細節可看下面的代碼
initFrameworkServlet();
// 未做任何事情,由子類實現,不過暫時為空
}
catch (ServletException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
catch (RuntimeException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
}
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
// 從當前的上下文中查找是否存在跟上下文信息(通過ContextLoaderListener加載的都會存在的)
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
// 重新再刷新一次WebApplicationContext持有的內容信息
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
wac = findWebApplicationContext();
}
if (wac == null) {
// 創建一個新的WebApplicationContext信息,并設置好其parent
// 這里需要好好看看,同樣的拆分為兩部分執行
// 1、創建WebApplicationContext類,也就是XmlWebApplicationContext實體類,并設置好其環境、配置屬性、父類等信息
// 2、調用configureAndRefreshWebApplicationContext,同樣的設置好其關于servlet的上下文的屬性信息,最后調用wac.refresh()
// PS: refresh()就開始了spring IOC的解析存儲操作了
wac = createWebApplicationContext(rootContext);
// 不過這里有一步需要注意到,在refresh()中,最后會有onApplicationEvent()的操作,他會調用在DispatcherServlet類的initStrategies方法,完成URL映射、Template等操作
// 并且設置this.refreshEventReceived為true
}
if (!this.refreshEventReceived) {
// 完成了上面的刷新操作就不要再刷新了
// 這里的onRefresh還是會調用initStrategies方法
// 殊途同歸罷了
onRefresh(wac);
}
if (this.publishContext) {
// 把當前的上下文信息也保存到ServletContext中
// 如果注意到的函數開頭的rootContext的獲取方法會發現也是通過這樣的方式獲取的
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
這樣就完成了DispatcherServlet的初始化操作了,接下來就可以具體處理http請求了,當前這其中遺漏了很多重要的的點,例如
- rootContext 這是怎么一回事,是必須的么,和applicationContext.xml又有什么關系呢?
- xml配置的context-param和servlet的init-param有什么區別?
這幾個點會在后續的學習筆記中再了解其原理,當前主要是介紹DispatcherServlet以及相關的東西。
DispatcherServlet URL映射以及請求處理 過程
Tomcat會通過的DispatcherServlet的servlet-mapping的屬性匹配到合適的wrapper,再關聯到具體的DispatcherServlet,也就意味著在web.xml確實可以配置多個servlet,只是在spring mvc中常用的就這一個而已。
HTTP請求,最后都會打到DispatcherServlet類的doService方法中,設置一些屬性之后,又來到了doDispatch方法中,這個方法是核心也是最重要的http請求處理的方法,通過URL選擇合適的controller,選擇具體的modelandview,再渲染生成數據回調等等操作
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);
mappedHandler = getHandler(processedRequest);
// 通過URL找到合適的controller,并存儲在HandlerExecutionChain對著中
// 還會檢測是否進行cors跨域操作,如果存在跨域就會按照跨域的要求去處理
// @CrossOrigin(origins="http://test.com") 可以直接放在controller的注解上
if (mappedHandler == null || mappedHandler.getHandler() == null) {
// 沒有找到合適的處理handle,就是404了
// 這就可以自定義配置404頁面以及跳轉等信息
// response.sendError(HttpServletResponse.SC_NOT_FOUND)
// 這里又可以引出一個問題了,如何配置404頁面
noHandlerFound(processedRequest, response);
return;
}
// 檢測當前獲取的controller是否合適,并且得到合適的HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 如果符合可以使用緩存機制,減少不必要的請求
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
// 使用spring本身的攔截器前置處理
return;
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 真正的處理請求,在本demo中會調到AnnotationMethodHandlerAdapter類中執行handle方法,返回的mv是ModelAndView
// 解析當前handler中包含了所有的方法,匹配其中合適的方法之后,invoke調用
// 不過這里需要注意到,類似于返回json的請求,是不需要模板渲染的,此時mv返回的是null,不過具體的json數據已經填入到了responseBody中
applyDefaultViewName(processedRequest, mv);
// 如果mv
mappedHandler.applyPostHandle(processedRequest, response, mv);
// spring 攔截器的后置處理
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 處理結果了,如果存在異常會把exception帶上,例如500錯誤等,按照異常處理
// 如果存在了模板,需要經過render處理
// 否則就直接把得到的數據當做body返回
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
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);
}
}
}
}
到這里就完成了對一個普通的http請求的處理全過程,不過還是存在諸多問題沒有去分析,如圖
- cors跨域是什么,如何使用
- URL映射規則是如何完成的,以及和Tomcat的URL映射有什么關聯么?
- 模板是如何被渲染的,在xml中如何設置不同的模板的?
還有個問題一直疏忽了,在spring mvc中存在大量的if(***.debug)這種操作,那么該如何配置使用日志系統呢?
接下來會再寫筆記去分析上述提到的各種問題~