Spring MVC请求原理介绍
结合最近做过的项目和阅读《Spring 实战》对Spring MVC 请求原理做些粗浅的总结,不对之处请多多指正。
本文主要介绍两方面的内容
第一,spring MVC请求的流程;
第二,请求完成后Web视图的渲染;
Spring MVC 请求流程
用户在浏览器中输入一个URL、点击一个连接或者提交一个表单时,请求就开始了。Spring MVC请求会经历多个节点,如下1-1展示请求的节点

(1)请求从浏览器发出时,携带了用户所请求内容的信息,至少包含请求的URL,一般还会带有其他的信息,例如用户的属性信息、提交的表单内容等。
请求流程的第一个节点是Spring的DispatcherServlet,它是Spring MVC的前端控制器,它是一个单例模式的Servlet,由它将前端的请求委托给应用程序的其他组件来执行实际的处理。一般在web.xml中配置它的初始化

(2)DispatcherServlet的任务是将请求发送给Spring MVC 控制器(Controller)。控制器是一个用户处理请求的Spring组件,一般一个应用程序中会有多个控制器。所以DispatchcerServlet需要知道某次请求应该发送给哪个控制器。因此DispatcherServlet会调用一个或多个处理器映射器(handler Mapping)方法来确定请求流程的下一个节点是哪里,处理器映射器会根据请求中携带的URL信息进行决策。
处理器映射器在应用程序启动时会根据配置、注解完成初始化。
贴出DispatcherServlet#doDispatch源码有助于理解,代码清单1-1:
1 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
2 HttpServletRequest processedRequest = request;
3 HandlerExecutionChain mappedHandler = null;
4 boolean multipartRequestParsed = false;
5 WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
6
7 try {
8 ModelAndView err = null;
9 Exception dispatchException = null;
10
11 try {
12 processedRequest = this.checkMultipart(request);
13 multipartRequestParsed = processedRequest != request;
14 mappedHandler = this.getHandler(processedRequest);//取Handler
15 if(mappedHandler == null || mappedHandler.getHandler() == null) {
16 this.noHandlerFound(processedRequest, response);
17 return;
18 }
19
20 HandlerAdapter ex = this.getHandlerAdapter(mappedHandler.getHandler());//合适的适配器
21 String method = request.getMethod();
22 boolean isGet = "GET".equals(method);
23 if(isGet || "HEAD".equals(method)) {
24 long lastModified = ex.getLastModified(request, mappedHandler.getHandler());
25 if(this.logger.isDebugEnabled()) {
26 this.logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
27 }
28
29 if((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
30 return;
31 }
32 }
33
34 if(!mappedHandler.applyPreHandle(processedRequest, response)) {
35 return;
36 }
37
38 err = ex.handle(processedRequest, response, mappedHandler.getHandler());
39 if(asyncManager.isConcurrentHandlingStarted()) {
40 return;
41 }
42
43 this.applyDefaultViewName(request, err);
44 mappedHandler.applyPostHandle(processedRequest, response, err);
45 } catch (Exception var19) {
46 dispatchException = var19;
47 }
48
49 this.processDispatchResult(processedRequest, response, mappedHandler, err, dispatchException);
50 } catch (Exception var20) {
51 this.triggerAfterCompletion(processedRequest, response, mappedHandler, var20);
52 } catch (Error var21) {
53 this.triggerAfterCompletionWithError(processedRequest, response, mappedHandler, var21);
54 } finally {
55 if(asyncManager.isConcurrentHandlingStarted()) {
56 if(mappedHandler != null) {
57 mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
58 }
59 } else if(multipartRequestParsed) {
60 this.cleanupMultipart(processedRequest);
61 }
62
63 }
64
65 }
(3)一但通过映射器找到了合适的控制器之后,会向前端控制器(DispatcherServlet)会将请求发送给找到的控制器,此时DispatcherServlet会卸下负载(即用户请求的信息内容),并耐心的等待控制器处理交付的信息。一般情况,设计良好的控制器只是处理少量的信息甚至不做信息处理的工作,而是将信息建单的封装后交给业务层(service)去处理,此处不做赘述。
(4)控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户,并且可能需要在浏览器上显示。这些信息被称为模型(model)。不过仅仅给用返回原始的信息是不够的(这些信息需要以用户友好的方式进行格式化,一般以HTML方式)。所以,信息需要发送一个视图(View),比如常规的JSP,也可能是htm、表示velocity框架的vm等。
除了以上工作,控制器最后还要将模型数据打包,标识出用于渲染输出的视图名称。然后它将请求连同模型和视图名称返回给前端控制器DispatcherServlet。
以上这种方式的好处是控制器不会与特定的视图耦合在一起,处理控制器传给前端控制器(DispatcherServlet)的并不直接绑定特定的视图,它只是传回了视图的逻辑名称,这个逻辑名称用来查找具体的视图实现,这个视图可能是JSP,也可能不是。
(5)前端控制器DispatcherServlet 收到处理控制器返回的模型数据包后,使用视图解析器(view resolve)将视图名称匹配一个特定视图实现页,它可能是JSP,也可能不是。
具体是什么形式还要看我们使用的视图框架,第二部分再做介绍。
DispatcherServlet#processDispatchResult 源码清单1-2
1 private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
2 boolean errorView = false;
3 if(exception != null) {
4 if(exception instanceof ModelAndViewDefiningException) {
5 this.logger.debug("ModelAndViewDefiningException encountered", exception);
6 mv = ((ModelAndViewDefiningException)exception).getModelAndView();
7 } else {
8 Object handler = mappedHandler != null?mappedHandler.getHandler():null;
9 mv = this.processHandlerException(request, response, handler, exception);
10 errorView = mv != null;
11 }
12 }
13
14 if(mv != null && !mv.wasCleared()) {
15 this.render(mv, request, response);
16 if(errorView) {
17 WebUtils.clearErrorRequestAttributes(request);
18 }
19 } else if(this.logger.isDebugEnabled()) {
20 this.logger.debug("Null ModelAndView returned to DispatcherServlet with name \'" + this.getServletName() + "\': assuming HandlerAdapter completed request handling");
21 }
22
23 if(!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
24 if(mappedHandler != null) {
25 mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
26 }
27
28 }
29 }
(6)既然DispatcherServlet已经通过模型数据包知道了有哪个视图渲染返回的结果,那么请求任务基本上完成了,它的最后一个节点就是视图的实现。
(7)在输出视图阶段,它交付模型数据,完成请求任务,视图使用模型数据渲染输出,该输出内容通过相应对象(Response)传递给用户客户端。
Spring MVC 实现了处理控制器中请求处理逻辑和视图渲染的解耦,这一特性使得请求处理逻辑与视图维护和更新互不影响,只需保证控制器方法和视图的实现在模型内容上达成一致即可,其他方面不应该再有关联。对于模型内容来说视图更关注的是信息展示的布局,而处理控制器更倾向于信息数据。以上讲述了从请求发出到返回模型数据包给视图的过程,那么视图极端是如何对输出数据做渲染的呢?
视图渲染
如果控制器只是通过逻辑视图名称来指向视图的话,那么Spring如何确定使用哪个特定视图来渲染模型数据呢?这就是视图解析器的任务了。
Spring视图解析器的基础类,Spring MVC中定义了名为ViewResolver的接口和View接口来完成视图定位和视图渲染输出工作
ViewResolver 接口代码清单1-3
package org.springframework.web.servlet;
import java.util.Locale;
import org.springframework.web.servlet.View;
public interface ViewResolver {
View resolveViewName(String var1, Locale var2) throws Exception;
}
当给resolverViewName传入一个视图名称和一个Locale对象时,它会返回一个View实例。
View接口代码清单1-4
package org.springframework.web.servlet;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface View {
String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
String PATH_VARIABLES = View.class.getName() + ".pathVariables";
String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";
String getContentType();
void render(Map<String, ?> var1, HttpServletRequest var2, HttpServletResponse var3) throws Exception;
}
View的工作职责就是接收模型以及Servlet的Request和Response对象,并将输出结果渲染到response对象中,以便输出给用户浏览器。
由此看我们好像还要实现ViewResolver接口和View接口,然而 Spring已经提供了多个内置的解析器的实现,基本适用于绝大多数的场景
以下是Spring自带的12个视图解析器,列表清单1-1
|
视图解析器 |
描述 |
|
BeanNameResolver |
将视图解析未Spring上下文中Bean,其中Bean的ID与视图名称相同 |
|
ContentNegotiatingViewResolver |
通过考虑客户端需要的内容类型来解析视图,委托给另外一个能够产生对应内容类型的视图解析器 |
|
FreeMarkerViewResolver |
将视图接卸FreeMarker模板 |
|
InternalResourceViewResolver |
将视图解析为Web应用的内部资源(一般为JSP) |
|
JasperReportViewResolver |
将视图解析为JasperReport定义 |
|
ResourceBundleViewResolver |
将视图解析为资源Bundle(一般为属性文件) |
|
TilesViewResolver |
将视图解析为Apache Tile定义,其中tile ID与视图名称相同。注意有两个不同的TilesViewResolver实现,分别对应于Tiles 2.0 和Tiles 3.0 |
|
UrlBasedViewResolver |
直接根据视图的名称解析视图,视图的名称会匹配一个物理视图的定义 |
|
VelocityLayoutViewResolver |
将视图解析为Velocity布局,从不同Velocity模板中组合页面 |
|
VelocityViewResolver |
将视图解析为Velocity模板 |
|
XMLViewResolver |
将视图解析为特定XML 文件中的bean 定义。类似于BeanNameViewResolver |
|
XsltViewResolver |
将视图解析为XSLT转换后的结果 |
除以上列举的12个视图解析器之外,还有个替代JSP的视图解析技术,就是Thymeleaf。需要单独引入thymeleaf包。
本文只是介绍主要特点,详细使用方式请查阅相关资料(推荐文章:https://blog.csdn.net/u014507083/article/details/56480732)
1.Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持 html 原型,然后在 html 标签里增加额外的属性来达到模板+数据的展示方式。浏览器解释 html 时会忽略未定义的标签属性,所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示。
2.Thymeleaf 开箱即用的特性。它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、该jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。
3.Thymeleaf 提供spring标准方言和一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。

浙公网安备 33010602011771号