还是和前面一样分析HttpServletBean、FrameworkServlet和DispatcherServlet
书籍《看透Spring MVC》
文章目录
1. HttpServletBean
HttpServletBean主要参与了创建工作,并没有涉及请求的处理。
2. FrameworkServlet
如果了解,Servlet与Tomcat,流程处理的起始是从Servlet接口的Service方法开始,然后在HttpServlet的service方法中根据请求的类型不同将请求路由到了doGet、doHead、doPost、doPut、doDelete、doOptions和doTrace七个方法,并且做了doHead、doOptions和doTrace的默认实现,其中doHead调用doGet,然后返回只有header没有body的response。
而我们知道,部署到Tomcat上的Servlet其实只有DispatcherServlet,然后由其分发请求。
看类图DispatcherServlet继承自FrameworkServlet,所以我们从FrameworkServlet说起。
看类图,我们得知FrameworkServlet 继承了HttpServletBean,并且也重写了service()方法。
/**
* 控制父类实现以拦截补丁请求。
*/
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
if (httpMethod == HttpMethod.PATCH || httpMethod == null) { //请求全部交给processRequest处理
processRequest(request, response);
}
else {
super.service(request, response);
}
}
重点关注processRequest内的doService,其在DispatcherServlet中具体实现:是一个模板方法。
/ * *
* 处理这个请求,发布一个事件,不管结果如何。
* 实际的事件处理是由抽象来执行的
* {@link #doService}模板方法
* /
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
//获取了LocaleContextHolder和RequestContextHolder中原来保存的LocaleContext和RequestAttributes,
//设置到previousLocaleContext和previousAttributes临时属性
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
//调用buildLocaleContext和buildRequestAttributes方法获取到当前请求的LocaleContext和RequestAttributes
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
//使用request拿到异步处理管理器并设置了拦截器
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
//通过initContextHolders方法将它们设置到LocaleContextHolder和Request-ContextHolder中(处理完请求后再恢复到原来的值)
initContextHolders(request, localeContext, requestAttributes);
try {
//重点关注doService,在DispatcherServlet中具体实现
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
//(finally中)通过resetContextHolders方法将原来的previousLocaleContext和previousAttributes恢复到Locale-ContextHolder和RequestContextHolder中,
//并调用publishRequestHandledEvent方法发布了一个ServletRequestHandledEvent类型的消息。
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
在doService前后还做了一些事情(装饰模式);
LocaleContext里面存放着Locale(也就是本地化信息,如zh-cn等)
RequestAttributes是spring的一个接口,通过它可以get/set/removeAttribute,根据scope参数判断操作request还是session。
2.1 LocaleContextHolder
/ * *
与LocaleContext实例关联的简单holder类,当前线程。LocaleContext将被继承
由当前线程派生的任何子线程
*在Spring中用作当前语言环境的中心holder,
*必要时:例如,在MessageSourceAccessor中。
* DispatcherServlet在这里自动公开它的当前语言环境。
*其他应用程序也可以公开他们的类,使类喜欢
*MessageSourceAccessor自动使用该地区。
*/
public final class LocaleContextHolder {
LocaleContextHolder类里面封装了两个属性localeContextHolder和inheritableLocaleContextHolder,它们都是LocaleContext,其中第二个可以被子线程继承。LocaleContextHolder还提供了get/set方法,可以获取和设置LocaleContext,另外还提供了get/setLocale方法,可以直接操作Locale,当然都是static的。
2.2 RequestContextHolder
/**
Holder类以线程绑定的形式公开web请求
* {@link RequestAttributes}对象。请求将被继承
由当前线程派生的任何子线程
* {@code inheritable} flag is set to {@code true}.
*
* <p>Use {@link RequestContextListener} or
* {@link org.springframework.web.filter.RequestContextFilter} to expose
* the current web request. Note that
* {@link org.springframework.web.servlet.DispatcherServlet}
* already exposes the current request by default.
*
*/
public abstract class RequestContextHolder {
/ * *
*访问与请求相关的属性对象的抽象。
*支持访问请求范围的属性和会话范围的属性
*属性,带有可选的“全局会话”概念。
*
*
可以实现任何类型的请求/会话机制,
*特别是servlet请求。
* /
public interface RequestAttributes {
/ * *
基于servlet的{@link RequestAttributes}接口实现。
*
*
从servlet请求和HTTP会话范围访问对象,
*没有“会话”和“全局会话”的区别。
*
*与javax . servlet . servletrequest @see # getAttribute
* @see javax.servlet.http.HttpSession # getAttribute
* /
public class ServletRequestAttributes extends AbstractRequestAttributes {
里面封装了RequestAttributes,可以get/set/removeAttribute,而且因为实际封装的是ServletRequestAttributes,所以还可以getRequest、getResponse、getSession。
故这也是为什么我们可以随意取出当前线程的request等引用的原因。
在方法最后的finally中调用resetContextHolders方法将原来的LocaleContext和Request-Attributes又恢复了。这是因为在Sevlet外面可能还有别的操作,如Filter(Spring-MVC自己的HandlerInterceptor是在doService内部的)等,为了不影响那些操作,所以需要进行恢复。
2.2 publishRequestHandledEvent
当publishEvents设置为true时,请求处理结束后就会发出这个消息,无论请求处理成功与否都会发布。
publishEvents可以在web.xml文件中配置Spring MVC的Servlet时配置,默认为true。
总结一下:
- 在service方法里添加了对PATCH的处理
- 将所有需要自己处理的请求都集中到了processRequest方法进行统一处理
- 在processRequest里面主要的处理逻辑交给了doService,这是一个模板方法
- 对使用当前request获取到的LocaleContext和RequestAttributes进行了保存,以及处理完之后的恢复
- 发布了ServletRequest-HandledEvent事件
3. DispatcherServlet
最核心的来了。
那么我们直接进入到doService():
/**
* 将特定于dispatcherservlet的请求属性和委托暴露给{@link #doDispatch}
*用于实际调度。
*/
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);
//保存请求属性的快照,以备包括,
//能够恢复原始属性后,include。
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}
// 使框架对象对处理程序和视图对象可用。
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
try {
//核心代码
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
//在include的情况下恢复原始属性快照。
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}
通过注释我们也知道,请求其实是交给了doDispatch进行具体的处理。在交给doDispatch进行具体的处理之前,如果是include的请求,则进行一次快照备份。
之后对request进行了一些属性设置,后面三个属性都和flashMap相关,主要用于Redirect转发时参数的传递。
我们知道重定向是没有传递参数的功能的,按普通的模式如果想传递参数,就只能将其写入url中,但是url有长度限制,另外有些场景中我们想传递的参数还不想暴露在url里,这时就可以用flashMap来进行传递了。
举例就是model,attr。
一般有三种:
- 使用前面讲过的RequestContextHolder获取到request,并从其属性中拿到output-FlashMap,然后将属性放进去,RequestContextHolder获取request的方法。
- 通过传入的attr参数的addFlashAttribute方法设置,这样也可以保存到outputFlashMap中,和第1种方法效果一样。
- 通过传入的attr参数的addAttribute方法设置,这样设置的参数不会保存到FlashMap,而是会拼接到url中。
@RequestMapping("/model") public String modelex(Model model) { model.addAttribute("hello"); return "redirect:success"; }
接下来看doDispatch。
doDispatch任务很简单:
①根据request找到Handler;
②根据Handler找到对应的HandlerAdapter;
③用HandlerAdapter处理Handler;
④调用processDispatchResult方法处理上面处理之后的结果(包含找到View并渲染输出给用户)
对应如下:
这里出现了很多的Handler,而Handler对SpringMVC有很重要的作用。
Handler:也就是处理器,它直接对应着MVC中的C也就是Controller层,它的具体表现形式有很多,可以是类,也可以是方法,如果你能想到别的表现形式也可以使用,它的类型是Object。我们前面例子中标注了@RequestMapping的所有方法都可以看成一个Handler。只要可以实际处理请求就可以是Handler。
HandlerMapping:是用来查找Handler的,在Spring MVC中会处理很多请求,每个请求都需要一个Handler来处理,具体接收到一个请求后使用哪个Handler来处理呢?这就是HandlerMapping要做的事情。
HandlerAdapter:也就是适配器。因为Spring MVC中的Handler可以是任意的形式,只要能处理请求就OK,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法(如doService方法)。怎么让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。
通过以上四句代码可以得出结论,使用HandlerMapping找到干活的Handler,找到使用Handler的HandlerAdapter,让HandlerAdapter使用Handler作用,最后将结果通过view展示。
3.1 doDispatch全解
注释如下:
将实际的分派处理程序处理。
通过依次应用servlet的处理程序映射来获得处理程序。
HandlerAdapter将通过查询servlet安装的HandlerAdapter获得
找到第一个支持处理程序类的。
所有的HTTP方法都由此方法处理。这取决于处理适配器或处理程序
他们自己决定哪些方法是可以接受的。
代码如下:
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);
// 根据request确定当前请求的处理程序。
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 确定当前请求的处理程序适配器。
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 (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 实际调用处理程序。
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
//在4。3版本中,我们也在处理处理器方法抛出的错误,
//使它们可用于@ExceptionHandler方法和其他场景。
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
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()) {
// 而不是完成后和完成后
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// 清除由多部分请求使用的任何资源。
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
其实,我们不看代码已经可以猜到dodispatch该做一些什么工作了:处理请求与结果处理。
在方法刚开始时,声明了几个变量:
- HttpServletRequest processedRequest:实际处理时所用的request,如果不是上传请求则直接使用接收到的request,否则封装为上传类型的request。
- HandlerExecutionChain mappedHandler:处理请求的处理器链(包含处理器和对应的Interceptor)。
- boolean multipartRequestParsed:是不是上传请求的标志。
- ModelAndView mv:封装Model和View的容器,此变量在整个Spring MVC处理的过程中承担着非常重要角色,如果使用过Spring MVC就不会对ModelAndView陌生。
- Exception dispatchException:处理请求过程中抛出的异常。需要注意的是它并不包含渲染过程抛出的异常。
接着,就检查请求是否时上传请求。
如果是,将request转换为Multi-partHttpServletRequest,并将multipartRequestParsed标志设置为true。
/ * *
*将请求转换为多部分请求,并提供多部分解析器。
*如果没有设置多部分解析器,只需使用现有的请求。
* @param请求当前HTTP请求
* @返回处理后的请求(如果需要,可以使用多部分包装器)
* @see MultipartResolver # resolveMultipart
* /
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
接着,
通过getHandler方法获取Handler处理器链,其中使用到了HandlerMapping,返回值为HandlerExecutionChain类型,其中包含着与当前request相匹配的Interceptor和Handler。
/ * *
*返回此请求的HandlerExecutionChain。
*
依次尝试所有处理程序映射。
* @param请求当前HTTP请求
* @返回HandlerExecutionChain,如果没有找到处理程序,则返回{@code null}
* /
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
接下来处理处理GET、HEAD请求的Last-Modified。
当浏览器第一次跟服务器请求资源(GET、Head请求)时,服务器在返回的请求头里面会包含一个Last-Modified的属性,代表本资源最后是什么时候修改的。在浏览器以后发送请求时会同时发送之前接收到的Last-Modified,服务器接收到带Last-Modified的请求后会用其值和自己实际资源的最后修改时间做对比,如果资源过期了则返回新的资源(同时返回新的Last-Modified),否则直接返回304状态码表示资源未过期,浏览器直接使用之前缓存的结果。
接着调用调用相应Interceptor的preHandle。也就是HandlerAdapter使用Handler处理请求。
prehandler处理
再接着,判断是否需要异步处理:
如果不需要异步处理,然后执行相应Interceptor的postHandle。
接下来使用processDispatchResult方法处理前面返回的结果,其中包括处理异常、渲染页面、触发Interceptor的afterCompletion方法三部分内容。
至此, doDispatch就差不多结束了。