spring MVC是嚴(yán)格遵守java servlet規(guī)范的一種web框架筷弦,可以打包成war包交由web容器(Tomcat或者jetty)運(yùn)行坐漏。接下來(lái)就來(lái)學(xué)習(xí)下spring mvc的運(yùn)行過(guò)程以及其中的細(xì)節(jié),如何和Tomcat無(wú)縫合作薄货,如何和spring 本身的核心功能IOC、AOP合作。
MVC 實(shí)例
接下來(lái)就搭建一個(gè)基于maven的spring mvc的實(shí)例蝇狼。
項(xiàng)目結(jié)構(gòu)
StudentController 類(lèi)
@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>
然后通過(guò)mvn tomcat7:run
就可以正常啟動(dòng)了
Tomcat 基礎(chǔ)
在介紹spring mvc的工作原理之前,有必要介紹下web容器的一種Tomcat倡怎。Tomcat是一個(gè)開(kāi)源的web容器迅耘,嚴(yán)格準(zhǔn)守servlet規(guī)范,在Tomcat中包含了各種各樣的組件诈胜,層層嵌套依賴(lài)豹障,如下圖所示。
catalina是最核心的組件焦匈,也可以認(rèn)為T(mén)omcat是從它開(kāi)始啟動(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è)閥門(mén)valve,閥門(mén)可以為request和response添加任何外置的功能。
Tomcat啟動(dòng)是由各自容器的監(jiān)聽(tīng)器調(diào)用啟動(dòng)的分蓖,按照上面所說(shuō)的順序依次執(zhí)行啟動(dòng)的尔艇。
那我們常用的servlet是在哪里的么?他是被wrapper包裝的么鹤,每一個(gè)wrapper持有一個(gè)servlet,所以在xml中配置了幾個(gè)servlet终娃,則就會(huì)存在多少個(gè)wrapper。并且servlet是通過(guò)ServletContext傳遞上下文的蒸甜。在具體的URL映射的時(shí)候棠耕,會(huì)先根據(jù)各自的servlet的URL配置在Tomcat的mappingdata中體現(xiàn),經(jīng)過(guò)host柠新、context窍荧、再到選擇不同類(lèi)型的wrapper(包含了wildcardWrappers、extensionWrappers登颓、defaultWrapper搅荞、exactWrappers四種wrapper類(lèi)型)最后才具體到某一個(gè)servlet請(qǐng)求上。
DispatcherServlet init 過(guò)程
DispatcherServlet類(lèi)是spring mvc中實(shí)現(xiàn)的了servlet規(guī)范的實(shí)體類(lèi)框咙,實(shí)現(xiàn)了HttpServlet類(lèi)咕痛,如下圖
首先需要明確一點(diǎn)的是,DispatcherServlet也只是一個(gè)HttpServlet類(lèi)喇嘱,他是被wrapper調(diào)用的init(ServletConfig config)方法進(jìn)入的茉贡,設(shè)置好config之后,進(jìn)入到init()方法者铜。
PS:在當(dāng)前環(huán)境中腔丧,ServletConfig是StandardWrapperFacade類(lèi),可以從中獲取到在xml配置的例如contextConfigLocation數(shù)據(jù)
HttpServletBean 類(lèi)
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沒(méi)有任何配置的時(shí)候,就會(huì)拋出異常拿撩,說(shuō)缺少必備的配置屬性
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;
}
}
// 開(kāi)始真正的初始化DispatcherServlet類(lèi)了
initServletBean();
if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}
FrameworkServlet 類(lèi)
protected final void initServletBean() throws ServletException {
try {
this.webApplicationContext = initWebApplicationContext();
// 這個(gè)是初始化當(dāng)前dispatchservlet的webApplicationContext參數(shù)
// 然后該參數(shù)中會(huì)持有spring的IOC容器等信息衣厘,通過(guò)這個(gè)參數(shù)可以獲取bean數(shù)據(jù)
// 具體細(xì)節(jié)可看下面的代碼
initFrameworkServlet();
// 未做任何事情,由子類(lèi)實(shí)現(xiàn)压恒,不過(guò)暫時(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)前的上下文中查找是否存在跟上下文信息(通過(guò)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類(lèi)型宙,也就是XmlWebApplicationContext實(shí)體類(lèi),并設(shè)置好其環(huán)境伦吠、配置屬性妆兑、父類(lèi)等信息
// 2魂拦、調(diào)用configureAndRefreshWebApplicationContext,同樣的設(shè)置好其關(guān)于servlet的上下文的屬性信息箭跳,最后調(diào)用wac.refresh()
// PS: refresh()就開(kāi)始了spring IOC的解析存儲(chǔ)操作了
wac = createWebApplicationContext(rootContext);
// 不過(guò)這里有一步需要注意到晨另,在refresh()中,最后會(huì)有onApplicationEvent()的操作谱姓,他會(huì)調(diào)用在DispatcherServlet類(lèi)的initStrategies方法,完成URL映射刨晴、Template等操作
// 并且設(shè)置this.refreshEventReceived為true
}
if (!this.refreshEventReceived) {
// 完成了上面的刷新操作就不要再刷新了
// 這里的onRefresh還是會(huì)調(diào)用initStrategies方法
// 殊途同歸罷了
onRefresh(wac);
}
if (this.publishContext) {
// 把當(dāng)前的上下文信息也保存到ServletContext中
// 如果注意到的函數(shù)開(kāi)頭的rootContext的獲取方法會(huì)發(fā)現(xiàn)也是通過(guò)這樣的方式獲取的
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
這樣就完成了DispatcherServlet的初始化操作了屉来,接下來(lái)就可以具體處理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)求處理 過(guò)程
Tomcat會(huì)通過(guò)的DispatcherServlet的servlet-mapping的屬性匹配到合適的wrapper脐雪,再關(guān)聯(lián)到具體的DispatcherServlet,也就意味著在web.xml確實(shí)可以配置多個(gè)servlet恢共,只是在spring mvc中常用的就這一個(gè)而已战秋。
HTTP請(qǐng)求,最后都會(huì)打到DispatcherServlet類(lèi)的doService方法中讨韭,設(shè)置一些屬性之后脂信,又來(lái)到了doDispatch方法中,這個(gè)方法是核心也是最重要的http請(qǐng)求處理的方法,通過(guò)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);
// 通過(guò)URL找到合適的controller,并存儲(chǔ)在HandlerExecutionChain對(duì)著中
// 還會(huì)檢測(cè)是否進(jìn)行cors跨域操作,如果存在跨域就會(huì)按照跨域的要求去處理
// @CrossOrigin(origins="http://test.com") 可以直接放在controller的注解上
if (mappedHandler == null || mappedHandler.getHandler() == null) {
// 沒(méi)有找到合適的處理handle濒生,就是404了
// 這就可以自定義配置404頁(yè)面以及跳轉(zhuǎn)等信息
// response.sendError(HttpServletResponse.SC_NOT_FOUND)
// 這里又可以引出一個(gè)問(wèn)題了埋泵,如何配置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類(lèi)中執(zhí)行handle方法秋泄,返回的mv是ModelAndView
// 解析當(dāng)前handler中包含了所有的方法,匹配其中合適的方法之后规阀,invoke調(diào)用
// 不過(guò)這里需要注意到恒序,類(lèi)似于返回json的請(qǐng)求,是不需要模板渲染的谁撼,此時(shí)mv返回的是null歧胁,不過(guò)具體的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)過(guò)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)求的處理全過(guò)程,不過(guò)還是存在諸多問(wèn)題沒(méi)有去分析崭参,如圖
- cors跨域是什么搭伤,如何使用
- URL映射規(guī)則是如何完成的,以及和Tomcat的URL映射有什么關(guān)聯(lián)么退盯?
- 模板是如何被渲染的婆赠,在xml中如何設(shè)置不同的模板的?
還有個(gè)問(wèn)題一直疏忽了海洼,在spring mvc中存在大量的if(***.debug)這種操作跨新,那么該如何配置使用日志系統(tǒng)呢?
接下來(lái)會(huì)再寫(xiě)筆記去分析上述提到的各種問(wèn)題~