spring MVC是嚴(yán)格遵守java servlet規(guī)范的一種web框架,可以打包成war包交由web容器(Tomcat或者jetty)運(yùn)行。接下來就來學(xué)習(xí)下spring mvc的運(yùn)行過程以及其中的細(xì)節(jié),如何和Tomcat無縫合作,如何和spring 本身的核心功能IOC、AOP合作。
MVC 實(shí)例
接下來就搭建一個(gè)基于maven的spring mvc的實(shí)例。
項(xiàng)目結(jié)構(gòu)
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
就可以正常啟動(dòng)了
Tomcat 基礎(chǔ)
在介紹spring mvc的工作原理之前,有必要介紹下web容器的一種Tomcat。Tomcat是一個(gè)開源的web容器,嚴(yán)格準(zhǔn)守servlet規(guī)范,在Tomcat中包含了各種各樣的組件,層層嵌套依賴,如下圖所示。
catalina是最核心的組件,也可以認(rèn)為Tomcat是從它開始啟動(dòng)的,他持有1個(gè)server容器,server容器可以包含了多個(gè)service容器,每個(gè)service容器都持有了一個(gè)connector連接器以及一個(gè)engine,engine有層層包含了host、context、wrapper等。
其中engine、host、context、wrapper又分別存在各自的一個(gè)管道pipeline以及至少一個(gè)閥門valve,閥門可以為request和response添加任何外置的功能。
Tomcat啟動(dòng)是由各自容器的監(jiān)聽器調(diào)用啟動(dòng)的,按照上面所說的順序依次執(zhí)行啟動(dòng)的。
那我們常用的servlet是在哪里的么?他是被wrapper包裝的,每一個(gè)wrapper持有一個(gè)servlet,所以在xml中配置了幾個(gè)servlet,則就會(huì)存在多少個(gè)wrapper。并且servlet是通過ServletContext傳遞上下文的。在具體的URL映射的時(shí)候,會(huì)先根據(jù)各自的servlet的URL配置在Tomcat的mappingdata中體現(xiàn),經(jīng)過host、context、再到選擇不同類型的wrapper(包含了wildcardWrappers、extensionWrappers、defaultWrapper、exactWrappers四種wrapper類型)最后才具體到某一個(gè)servlet請(qǐng)求上。
DispatcherServlet init 過程
DispatcherServlet類是spring mvc中實(shí)現(xiàn)的了servlet規(guī)范的實(shí)體類,實(shí)現(xiàn)了HttpServlet類,如下圖
首先需要明確一點(diǎn)的是,DispatcherServlet也只是一個(gè)HttpServlet類,他是被wrapper調(diào)用的init(ServletConfig config)方法進(jìn)入的,設(shè)置好config之后,進(jìn)入到init()方法。
PS:在當(dāng)前環(huán)境中,ServletConfig是StandardWrapperFacade類,可以從中獲取到在xml配置的例如contextConfigLocation數(shù)據(jù)
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配置的屬性,在下面貼的圖片中可以看到,當(dāng)DispatcherServlet沒有任何配置的時(shí)候,就會(huì)拋出異常,說缺少必備的配置屬性
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);
// 這一步就是設(shè)置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();
// 這個(gè)是初始化當(dāng)前dispatchservlet的webApplicationContext參數(shù)
// 然后該參數(shù)中會(huì)持有spring的IOC容器等信息,通過這個(gè)參數(shù)可以獲取bean數(shù)據(jù)
// 具體細(xì)節(jié)可看下面的代碼
initFrameworkServlet();
// 未做任何事情,由子類實(shí)現(xiàn),不過暫時(shí)為空
}
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());
// 從當(dāng)前的上下文中查找是否存在跟上下文信息(通過ContextLoaderListener加載的都會(huì)存在的)
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持有的內(nèi)容信息
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
wac = findWebApplicationContext();
}
if (wac == null) {
// 創(chuàng)建一個(gè)新的WebApplicationContext信息,并設(shè)置好其parent
// 這里需要好好看看,同樣的拆分為兩部分執(zhí)行
// 1、創(chuàng)建WebApplicationContext類,也就是XmlWebApplicationContext實(shí)體類,并設(shè)置好其環(huán)境、配置屬性、父類等信息
// 2、調(diào)用configureAndRefreshWebApplicationContext,同樣的設(shè)置好其關(guān)于servlet的上下文的屬性信息,最后調(diào)用wac.refresh()
// PS: refresh()就開始了spring IOC的解析存儲(chǔ)操作了
wac = createWebApplicationContext(rootContext);
// 不過這里有一步需要注意到,在refresh()中,最后會(huì)有onApplicationEvent()的操作,他會(huì)調(diào)用在DispatcherServlet類的initStrategies方法,完成URL映射、Template等操作
// 并且設(shè)置this.refreshEventReceived為true
}
if (!this.refreshEventReceived) {
// 完成了上面的刷新操作就不要再刷新了
// 這里的onRefresh還是會(huì)調(diào)用initStrategies方法
// 殊途同歸罷了
onRefresh(wac);
}
if (this.publishContext) {
// 把當(dāng)前的上下文信息也保存到ServletContext中
// 如果注意到的函數(shù)開頭的rootContext的獲取方法會(huì)發(fā)現(xiàn)也是通過這樣的方式獲取的
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
這樣就完成了DispatcherServlet的初始化操作了,接下來就可以具體處理http請(qǐng)求了,當(dāng)前這其中遺漏了很多重要的的點(diǎn),例如
- rootContext 這是怎么一回事,是必須的么,和applicationContext.xml又有什么關(guān)系呢?
- xml配置的context-param和servlet的init-param有什么區(qū)別?
這幾個(gè)點(diǎn)會(huì)在后續(xù)的學(xué)習(xí)筆記中再了解其原理,當(dāng)前主要是介紹DispatcherServlet以及相關(guān)的東西。
DispatcherServlet URL映射以及請(qǐng)求處理 過程
Tomcat會(huì)通過的DispatcherServlet的servlet-mapping的屬性匹配到合適的wrapper,再關(guān)聯(lián)到具體的DispatcherServlet,也就意味著在web.xml確實(shí)可以配置多個(gè)servlet,只是在spring mvc中常用的就這一個(gè)而已。
HTTP請(qǐng)求,最后都會(huì)打到DispatcherServlet類的doService方法中,設(shè)置一些屬性之后,又來到了doDispatch方法中,這個(gè)方法是核心也是最重要的http請(qǐng)求處理的方法,通過URL選擇合適的controller,選擇具體的modelandview,再渲染生成數(shù)據(jù)回調(diào)等等操作
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,并存儲(chǔ)在HandlerExecutionChain對(duì)著中
// 還會(huì)檢測(cè)是否進(jìn)行cors跨域操作,如果存在跨域就會(huì)按照跨域的要求去處理
// @CrossOrigin(origins="http://test.com") 可以直接放在controller的注解上
if (mappedHandler == null || mappedHandler.getHandler() == null) {
// 沒有找到合適的處理handle,就是404了
// 這就可以自定義配置404頁(yè)面以及跳轉(zhuǎn)等信息
// response.sendError(HttpServletResponse.SC_NOT_FOUND)
// 這里又可以引出一個(gè)問題了,如何配置404頁(yè)面
noHandlerFound(processedRequest, response);
return;
}
// 檢測(cè)當(dāng)前獲取的controller是否合適,并且得到合適的HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 如果符合可以使用緩存機(jī)制,減少不必要的請(qǐng)求
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());
// 真正的處理請(qǐng)求,在本demo中會(huì)調(diào)到AnnotationMethodHandlerAdapter類中執(zhí)行handle方法,返回的mv是ModelAndView
// 解析當(dāng)前handler中包含了所有的方法,匹配其中合適的方法之后,invoke調(diào)用
// 不過這里需要注意到,類似于返回json的請(qǐng)求,是不需要模板渲染的,此時(shí)mv返回的是null,不過具體的json數(shù)據(jù)已經(jīng)填入到了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);
}
// 處理結(jié)果了,如果存在異常會(huì)把exception帶上,例如500錯(cuò)誤等,按照異常處理
// 如果存在了模板,需要經(jīng)過render處理
// 否則就直接把得到的數(shù)據(jù)當(dāng)做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);
}
}
}
}
到這里就完成了對(duì)一個(gè)普通的http請(qǐng)求的處理全過程,不過還是存在諸多問題沒有去分析,如圖
- cors跨域是什么,如何使用
- URL映射規(guī)則是如何完成的,以及和Tomcat的URL映射有什么關(guān)聯(lián)么?
- 模板是如何被渲染的,在xml中如何設(shè)置不同的模板的?
還有個(gè)問題一直疏忽了,在spring mvc中存在大量的if(***.debug)這種操作,那么該如何配置使用日志系統(tǒng)呢?
接下來會(huì)再寫筆記去分析上述提到的各種問題~