文章目录
- 【README】
- 【1】文件上传与MultipartResolver
- 【2】Handler与HandlerAdaptor(处理器与处理器适配器)
- 【3】web请求处理拦截与HandlerInterceptor拦截器
-
- 【3.1】springmvc提供的HandlerInterceptor实现类
- [【3.2】自定义 HandlerInterceptor(统计执行耗时)](#【3.2】自定义 HandlerInterceptor(统计执行耗时))
- 【3.3】过滤器Filter
- 【4】springmvc异常处理与HandlerExceptionResolver(处理器异常解析器)
【README】
本文总结自《spring揭秘》,作者王福强,非常棒的一本书,墙裂推荐;
代码详情参见: springmvcDiscoverFirstDemo【github】
1)springmvc其他组件如下:
- MultipartResolver(多部件解析器): 在 HandlerMapping之前执行, 处理文件上传请求;
- HandlerInterceptor(处理器拦截器): 对处理流程进行拦截;
- HandlerAdapter(处理器适配器): 帮助我们使用其他类型的Handler;(而不仅仅只使用Controller这一种Handler)
- HandlerExceptionResolver(处理器异常解析器): 处理器异常解析器; 提供处理器异常处理的标准框架;
2)web.xml (web应用部署描述符,servlet容器加载时读取的xml文件)
xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns = "https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version = "5.0"
metadata-complete = "false"
>
<display-name>springmvcDiscover</display-name>
<!-- 指定ContextLoaderListener加载web容器时使用的多个xml配置文件(默认使用/WEB-INF/applicationContext.xml) -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml,/WEB-INF/applicationContext-module1.xml</param-value>
</context-param>
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 注册过滤器代理 -->
<filter>
<filter-name>customFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>customFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 配置监听器ContextLoaderListener,其加载顶层WebApplicationContext web容器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 注册一级控制器 DispatcherServlet,用于拦截所有请求(匹配url-pattern) -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- DispatcherServlet启动读取xml配置文件加载组件,构建web容器(子),通过contextConfigLocation为其配置多个xml文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dispatcher-servlet.xml,/WEB-INF/dispatcher-servlet2.xml,/WEB-INF/dispatcher-servlet3-upload.xml</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
<!-- 新增multipart-config 子元素,该servlet才启用文件上传功能(必须) -->
<multipart-config>
<!-- 当上传文件被处理或文件超过fileSizeThreshold,文件的保存路径;默认为空串 -->
<location>D:\temp\springmvcUploadDir</location>
<!-- 上传文件字节最大值,若超过则抛出异常;默认无限;我们这里设置为20M -->
<max-file-size>20971520</max-file-size>
<!-- 请求报文字节最大值,若超过则抛出异常;默认无限;我们这里设置为1000M -->
<max-request-size>1048576000</max-request-size>
<!-- 临时保存到磁盘的文件大小最小值(超过该值就保存);默认0 -->
<file-size-threshold>-1</file-size-threshold>
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
3)配置文件目录结构:
- springmvc顶级web容器配置文件: applicationContext.xml , applicationContext-module1.xml
- DispatcherServlet次顶级web容器配置文件:dispatcher-servlet.xml, dispatcher-servlet2.xml, dispatcher-servlet3-upload.xml
【1】文件上传与MultipartResolver
1)RFC1867: 为html表单新增了一种MIME类型(multipart/formdata),表示表单的文件上传 ;
2)**MIME(Multipurpose Internet Mail Extensions)定义:**多用途互联网邮件扩展类型。媒体类型(也称为多用途互联网邮件扩展或 MIME 类型)表示文档、文件或字节组合的性质和格式。简单理解:MIME定义了文档,文件或字节组合的格式;MIME 类型在 IETF 的 RFC 6838 中定义并标准化, 参见 https://datatracker.ietf.org/doc/html/rfc6838
- 常见的MIME类型(通用型):
- 超文本标记语言文本 .html text/html
- xml文档 .xml text/xml
- XHTML文档 .xhtml application/xhtml+xml
- 普通文本 .txt text/plain
- RTF文本 .rtf application/rtf
- PDF文档 .pdf application/pdf
- Microsoft Word文件 .word application/msword
- PNG图像 .png image/png
- GIF图形 .gif image/gif
- JPEG图形 .jpeg,.jpg image/jpeg
- au声音文件 .au audio/basic
- MIDI音乐文件 mid,.midi audio/midi,audio/x-midi
- RealAudio音乐文件 .ra, .ram audio/x-pn-realaudio
- MPEG文件 .mpg,.mpeg video/mpeg
- AVI文件 .avi video/x-msvideo
- GZIP文件 .gz application/x-gzip
- TAR文件 .tar application/x-tar
- 任意的二进制数据 application/octet-stream
- MIME作用: 显然,MIME是定义文档,文件或字节组合格式的一种标准;
- 有了标准,客户端(如浏览器)根据MIME某种标准格式封装请求报文;
- 有了标准, 服务器根据MIME格式解析请求报文(字节流),并做处理;
2)声明文件上传的html表单元素:
html
<!-- html文件上传表单元素 -->
<form method="post" action="busiFileUpload.do" enctype="multipart/form-data">
<table>
<tr>
<td>选择上传文件: <input name="inputFile" type="file" /></td>
</tr>
<tr>
<td><input type="submit" value="提交" /></td>
</tr>
</table>
</form>
3)文件上传请求报文封装与解析:
- 客户端浏览器根据RFC1867定义的格式或标准,对文件上传表单内容进行编码;而服务器根据RFC1867对请求报文解码,就可以获取表单提交的数据,包括上传的文件流;
- 服务器端对multipart/form-data类型的报文解析,没必要自定义实现;可以复用已有的文件上传类库,如 CommonsFileUpload
4)springmvc提供了几种文件上传类库,通过MultipartResolver接口的抽象,我们可以自行选择使用哪种文件上传类库;
【1.1】使用MultipartResolver进行文件上传
1)web.xml 配置文件上传,参见 https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#a-basic-example (搜索multipart-config)
2)java的servlet规范能够处理multipart请求,并使得mime类型(多用途互联网邮件扩展)附件可用;但需要对servlet(springmvc中的DispatcherServlet)新增配置 ,使得该servlet启用处理Multipart请求功能,包括但不限于文件上传;
- 把 <multipart-config> 子元素添加到 DispatcherServlet 配置中;( 或使用注解MultipartConfig 标注某servlet,表明该servlet启用处理multipart请求 ) 参见 https://docs.oracle.com/javaee/7/tutorial/servlets011.htm
- 注册MultipartResolver(StandardServletMultipartResolver )到springmvc容器中,bean名称一定是 multipartResolver ; 参见 https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet/multipart.html
【1.2】springmvc处理multipart多部件请求流程
1)springmvc处理multipart多部件请求流程(multipart请求包括但不限于文件上传):
- 浏览器提交Multipart请求到springmvc应用;
- DispatcherServlet接收到请求后,从自身的spring web容器WebApplicationContext中找到名为multipartResolver的多组件解析器实例;(本文用的是StandardServletMultipartResolver)
- 通过multipartResolver.isMultipart(request)判断该请求是否为 multipart 请求(请求报文的mime类型是否 multipart开头);
- 若不是,则直接返回原始HttpServletRequest;
- 若是,则通过multipartResolver.resolveMultipart(request) 把request封装为StandardMultipartHttpServletRequest , (HttpServletRequest子类) ;后续所有请求都使用 StandardMultipartHttpServletRequest 进行业务逻辑处理;
- multipart请求处理完成后,DispatcherServlet会调用multipartResolver的cleanupMultipart()方法释放文件上传处理时的系统资源;
【1.3】使用springmvc上传文件代码实现(springmvc6.10版本):
【fileUpload.jsp】文件上传页面
jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.List" import="java.util.ArrayList" isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>文件上传列表</title>
</head>
<body>
<form method="post" action="busiFileUpload.do" enctype="multipart/form-data">
<table>
<tr>
<td>选择上传文件: <input name="inputFile" type="file" /></td>
</tr>
<tr>
<td><input type="submit" value="提交" /></td>
</tr>
</table>
</form>
</body>
</html>
【web.xml】
xml
<!-- 注册一级控制器 DispatcherServlet,用于拦截所有请求(匹配url-pattern) -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- DispatcherServlet启动读取xml配置文件加载组件,构建web容器(子),通过contextConfigLocation为其配置多个xml文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dispatcher-servlet.xml,/WEB-INF/dispatcher-servlet2.xml,/WEB-INF/dispatcher-servlet3-upload.xml</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
<!-- 新增multipart-config 子元素,该servlet才启用处理Multipart请求,包括文件上传(必须) -->
<multipart-config>
<!-- 当上传文件被处理或文件超过fileSizeThreshold,文件的保存路径;默认为空串 -->
<location>D:\temp\springmvcUploadDir</location>
<!-- 上传文件字节最大值,若超过则抛出异常;默认无限;我们这里设置为20M -->
<max-file-size>20971520</max-file-size>
<!-- 请求报文字节最大值,若超过则抛出异常;默认无限;我们这里设置为1000M -->
<max-request-size>1048576000</max-request-size>
<!-- 临时保存到磁盘的文件字节最小阈值;默认0 -->
<file-size-threshold>0</file-size-threshold>
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
【文件临时保存】通过调试我们发现, 文件在处理过程中会被临时保存(因为保存的阈值为0,即所有文件都被临时暂存;当然可以调整为其他值); 如下;
【dispatcher-servlet3-upload.xml】DispatcherServlet的spring容器配置文件:注册Multipart解析器到spring容器(StandardServletMultipartResolver)
xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 注册多部件请求解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver" />
<bean id="/busiFileUpload.do" class="com.tom.springmvc.controller.upload.BusiFileUploadController" />
<bean id="/fileUploadPage.do" class="com.tom.springmvc.controller.upload.BusiFileUploadPageController"/>
</beans>
【BusiFileUploadController】文件上传控制器
java
public class BusiFileUploadController extends AbstractController {
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (!(request instanceof MultipartHttpServletRequest multipartRequest)) {
return new ModelAndView("error");
}
// 类型转换
MultipartFile multipartFile = multipartRequest.getFile("inputFile");
if (Objects.isNull(multipartFile)) {
return new ModelAndView("error");
}
// 保存文件流到本地
String fileName = multipartFile.getOriginalFilename();
String path = request.getServletContext().getRealPath("/") + fileName;
BusiIOUtils.saveToDiskFile(multipartFile, path);
// 返回视图
ModelAndView uploadSuccMv = new ModelAndView("fileUploadSucc");
uploadSuccMv.addObject("fileName", multipartFile.getOriginalFilename());
uploadSuccMv.addObject("path", path);
return uploadSuccMv;
}
}
【BusiIOUtils.java】
java
/**
* @description 保存到本地磁盘文件
* @author admin
*/
public static void saveToDiskFile(MultipartFile multipartFile, String path) throws IOException {
BufferedOutputStream targetBufferedOutputStream = new BufferedOutputStream(new FileOutputStream(path));
BufferedInputStream bufferedInputStream = new BufferedInputStream(multipartFile.getInputStream());
byte[] bufferArr = new byte[1024];
while (bufferedInputStream.read(bufferArr, 0, bufferArr.length) != -1) {
targetBufferedOutputStream.write(bufferArr);
}
targetBufferedOutputStream.flush();
targetBufferedOutputStream.close();
bufferedInputStream.close();
}
【上传效果】
【2】Handler与HandlerAdaptor(处理器与处理器适配器)
【2.1】概述
1)springmvc中:任何用于web请求处理的处理对象统称为Handler处理器; Controller是处理器的一种;
2)对于DispatcherServlet来说,有个问题:DispatcherServlet应该使用什么样的Handler, 又如何调用Handler的哪个方法来处理请求?
- DispatcherServlet把Handler调用职责转交给HandlerAdapter(为什么DispatcherServlet调用HandlerAdapter,再由HandlerAdapter调用具体Handler,而不是DispatcherServlet直接调用Handler;因为Handler可以有多种,如servlet,controller;他们要适配DispatcherServlet的调用,就需要拥有相同的方法名;而因为历史原因,servlet先于controller被发明,即Handler间没有相同的方法名;即对于没有相同方法的Handler需要适配DispatcherServlet的调用(即便通过实现新增接口,可能对存量Handler有侵入性,强耦合),就需要使用适配器模式;这是适配器模式的又一应用; );
- DispatcherServlet从 HandlerMapping获取Handler后,通过HandlerAdapter#supports(handler)方法判断当前HandlerAdapter是否支持对该handler的调用;
- 返回true,则表示支持,则把handler作为参数传给 HandlerAdapter#handle()方法进行请求处理;
- 若遍历所有HandlerAdapter,所有HandlerAdapter都不支持该handler的调用,则抛出异常;
- DispatcherServlet从 HandlerMapping获取Handler后,通过HandlerAdapter#supports(handler)方法判断当前HandlerAdapter是否支持对该handler的调用;
- 想让DispatcherServlet支持新的Handler类型, 只需要提供对应的新的HandlerAdapter实现类 ;
- 如 Controller这种处理器对应的HandlerAdapter是SimpleControllerHandlerAdapter;
java
public interface HandlerAdapter {
boolean supports(Object handler);
@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
/** @deprecated */
@Deprecated
long getLastModified(HttpServletRequest request, Object handler);
}
【DispatcherServlet#doDispatch】 查找HandlerAdapter及调用handle()方法的过程
java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
try {
ModelAndView mv = null;
Exception dispatchException = null;
// ...
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 传入请求到HanderMapping获取二级控制器(或处理器)如Controller(DispatcherServlet是一级控制器)
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
// 传入处理器获取HandlerAdapter处理器适配器 (底层调用 adapter.supports()方法,若为true,则返回adapter )
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 调用处理器适配器的handle() 方法,并获取处理结果ModelAndView
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
// ......
}
【getHandlerAdapter()】
java
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
Iterator var2 = this.handlerAdapters.iterator();
while(var2.hasNext()) {
HandlerAdapter adapter = (HandlerAdapter)var2.next();
// 判断当前处理器适配器是否支持对该handler的调用
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
【2.1.1】Handler及HandlerAdapter处理web请求过程
1)我们以Controller这种handler为例,对Handler及HandlerAdapter的工作细节进行说明;(注意: Controller是二级控制器或二级处理器,DispatcherServlet是一级处理器 )
2)Controller对应的HandlerAdapter是SimpleControllerHandlerAdapter; 定义如下;
【SimpleControllerHandlerAdapter】
我想这个HandlerAdapter的代码非常简单了,不再展开赘述;逻辑是:判断当前handler是否为二级控制器类型Controller,若是,则通过SimpleControllerHandlerAdapter本身的handle方法调用handler.handleRequest()方法处理请求;
java
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
public SimpleControllerHandlerAdapter() {
}
public boolean supports(Object handler) {
return handler instanceof Controller;
}
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return ((Controller)handler).handleRequest(request, response); // 调用具体的处理器的handleRequest()方法
}
public long getLastModified(HttpServletRequest request, Object handler) {
if (handler instanceof LastModified lastModified) {
return lastModified.getLastModified(request);
} else {
return -1L;
}
}
}
【3】web请求处理拦截与HandlerInterceptor拦截器
1)拦截器: 在web请求处理过程中,新增业务拦截逻辑,如拦截切面;应用场景如前置参数校验, 报文解析与参数类型转换, 收集日志等;
2)springmvc中使用HandlerInterceptor抽象拦截器,有3个方法(preHandle, postHandle, afterCompletion) ;
- preHandle:在调用HandlerAdapter#handle()之前执行; (应用场景,如前置参数校验)
- 返回true,则继续执行后续步骤;
- 返回false, 不允许执行后续步骤; 包括HandlerInterceptor链中其他 HandlerInterceptor以及之后的Handler;
- postHandle: 在调用HandlerAdapter#handle()之后,但在视图渲染之前执行; (应用场景,如统计处理耗时)
- afterCompletion:无论是否抛出异常,该方法在请求被处理完成后都被执行;
java
public interface HandlerInterceptor {
// 前置处理
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
// 后置处理
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
// 无论是否抛出异常,该方法在请求被处理完成后都被执行
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
【3.1】springmvc提供的HandlerInterceptor实现类
【3.2】自定义 HandlerInterceptor(统计执行耗时)
【TimeCostHandlerInterceptor】 执行耗时统计拦截器
java
public class TimeCostHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
request.setAttribute("startTime" , System.currentTimeMillis());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
Long startTime = (Long) request.getAttribute("startTime");
System.out.println("执行耗时统计(单位秒)=" + (System.currentTimeMillis() - startTime) / 1000);
}
}
【dispatcher-servlet3-upload.xml】注册拦截器bean-timeCostHandlerInterceptor到spring容器
xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 注册多部件请求解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver" />
<bean id="/busiFileUpload.do" class="com.tom.springmvc.controller.upload.BusiFileUploadController" />
<bean id="/fileUploadPage.do" class="com.tom.springmvc.controller.upload.BusiFileUploadPageController"/>
<!-- 注册自定义处理器拦截器 -->
<bean id="timeCostHandlerInterceptor" class="com.tom.springmvc.handlerinterceptor.TimeCostHandlerInterceptor"/>
</beans>
【把拦截器装配到 HandlerMapping】 dispatcher-servlet.xml 中的HandlerMapping中装配拦截器timeCostHandlerInterceptor
xml
<!-- 注册HandllerMapping bean到springweb容器, BeanNameUrlHandlerMapping使用URL与Controller的bean名称进行匹配 -->
<bean id="beanNameUrlHandlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="timeCostHandlerInterceptor" />
</list>
</property>
</bean>
【3.2.1】HandlerInterceptor装配
1)为什么HandlerInterceptor要在HandlerMapping装配,而不是其他组件?
【DispatcherServlet#doDispatch】
java
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);
// Determine handler for the current request.
// 根据请求processedRequest, 获取映射后的处理器mappedHandler,类型为HandlerExecutionChain,HandlerExecutionChain是一个Handler包装类, 包装了具体处理器(如Controller), HandlerInterceptor列表 </font>
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 根据Handler处理器获取 HandlerAdapter处理器适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
// ......
// 调用拦截器前置处理方法(拦截器)
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
// 调用handle处理业务逻辑,处理完后返回ModelAndView对象
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// ...
applyDefaultViewName(processedRequest, mv);
// 调用拦截器后置处理方法(拦截器)
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
dispatchException = new ServletException("Handler dispatch failed: " + err, err);
}
// 视图渲染,且渲染完成后调用拦截器执行完成后的处理方法 (拦截器) -- 不抛异常也会调用
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
// 调用拦截器执行完成后的处理方法(拦截器) -- 异常时调用
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new ServletException("Handler processing failed: " + err, 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);
}
}
}
}
// 根据请求processedRequest, 获取映射后的处理器,类型为HandlerExecutionChain
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
【HandlerExecutionChain】处理器执行链属性定义
显然, HandlerExecutionChain是一个Handler包装类, 包装了具体处理器(如Controller), HandlerInterceptor列表 ;
java
public class HandlerExecutionChain {
private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class);
private final Object handler;
private final List<HandlerInterceptor> interceptorList = new ArrayList<>();
private int interceptorIndex = -1;
// ......
}
【DispatcherServlet#getHandler()调试时的内存信息】
1)getHandler():遍历所有HandlerMapping列表,通过request找出处理器(二级控制器, 如Controller);
- 返回类型是HandlerExecutionChain,而HandlerExecutionChain包装了具体处理器和拦截器列表;
【补充】
- AbstractHandlerMapping#getHandler()方法, 调用getHandlerExecutionChain()获取HandlerExecutionChain;
- 而 getHandlerExecutionChain()方法新建HandlerExecutionChain对象,并把AbstractHandlerMapping中adaptedInterceptors收集到HandlerExecutionChain中;
- 而adaptedInterceptor是由initInterceptors()方法遍历this.interceptors 并执行适配方法收集得到的;
- 这就是为什么要在BeanNameUrlHandlerMapping的bean注册配置信息中,装配interceptors属性,并引用timeCostHandlerInterceptor的原因 ;
【dispatcher-servlet.xml】装配拦截器到BeanNameUrlHandlerMapping
xml
<!-- 注册HandllerMapping bean到springweb容器, BeanNameUrlHandlerMapping使用URL与Controller的bean名称进行匹配 -->
<bean id="beanNameUrlHandlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="timeCostHandlerInterceptor" />
</list>
</property>
</bean>
【3.2.2】拦截器作用范围(HandlerMapping)
1)由配置可知,拦截器的作用范围是HandlerMapping ; 如本文配置了2个HandlerMapping,包括 BeanNameUrlHandlerMapping, SimpleUrlHandlerMapping ;而只有BeanNameUrlHandlerMapping装配了timeCostHandlerInterceptor拦截器,而SimpleUrlHandlerMapping 没有;
- 所以:通过BeanNameUrlHandlerMapping找到的二级处理器,并调用该处理器时,才会有timeCostHandlerInterceptor拦截功能;而SimpleUrlHandlerMapping 没有;
【3.3】过滤器Filter
1)对web请求进行拦截,除了使用 HandlerInterceptor之外,还可以使用 Filter;
2)HandlerInterceptor与Filter过滤器区别:
- Filter是Servlet规范的标准组件, Filter在DispatcherServlet之前对servlet进行拦截(过滤);是servlet级别的拦截(过滤);
- HandlerInterceptor是在DispatcherServlet内部对handler做拦截(细粒度),包括请求处理前,请求处理后及完成后拦截;
3)Filter过滤器是一个接口,如下:
java
package jakarta.servlet;
import java.io.IOException;
public interface Filter {
default public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
default public void destroy() {
}
}
过滤器执行逻辑:
- 若请求通过拦截条件,则在 doFilter()方法中执行 chain.doFilter(request, response); 把请求透传给下一个处理步骤;
- 若不通过,则不调用 chain.doFilter,即请求处理流程终止(当然,终止请求处理时,需要封装响应报文,以提示错误信息);
【3.3.1】springmvc配置过滤器代码实践
【web.xml】注册DelegatingFilterProxy到servlet容器 【servlet容器】
xml
<!-- 注册过滤器代理 -->
<filter>
<filter-name>customFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>customFilter</filter-name>
<url-pattern>/*</url-pattern> <!-- 对路径以/开头的所有servlet都进行过滤 -->
</filter-mapping>
【applicationContext.xml】注册名为customFilter的过滤器到spring容器【 spring 容器】, filter名称(customFilter)需要与DelegatingFilterProxy的filterName保持一致;
xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="userAppService" class="com.tom.springmvc.model.UserAppService" />
<bean id="bankCardAppService" class="com.tom.springmvc.model.bankcard.BankCardAppService" />
<!-- 注册自定义过滤器 -->
<bean id="customFilter" class="com.tom.springmvc.filter.CustomFilter"/>
</beans>
【CustomFilter】过滤器定义
java
public class CustomFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println(request.getServletContext().getContextPath() + "CustomFilter 过滤器执行");
chain.doFilter(request, response);
}
}
【执行效果】
c++
/springmvcDiscoverFirstDemo CustomFilter 过滤器执行
【3.3.2】底层原理
1)问题: 为什么要在web.xml中注册DelegatingFilterProxy ;
- DelegatingFilterProxy的作用:作为Filter的代理对象;当对请求拦截时,把拦截逻辑委派给具体的Filter(如本文中的CustomFilter);
- 物理结构上DelegatingFilterProxy在web.xml中注册,在servlet容器中;
- 而 CustomFilter 在 applicationContext.xml 中注册,在springmvc顶级WebApplicationContext容器中;
- 当然,我们讲,在web.xml中肯定可以注册CustomFilter来执行拦截逻辑;但无法装配spring容器的bean;
- 简单理解: 要把spring容器的bean装配到CustomFilter,则CustomerFilter必须注册到spring容器; 所以CustomerFilter在applicationContext.xml中注册(applicationContext.xml是springmvc顶级web容器加载的配置文件)
- 又引入新问题:把CustomFilter注册到spring的顶级web容器中,servlet容器是无法识别的;由上文可知,filter是servlet级别的拦截,又servlet容器无法识别spring容器中的CustomFilter,所以如果没有中介,servlet容器是无法调用spring容器中的CustomFilter执行过滤逻辑 ;
- 解决方法: DelegatingFilterProxy 就是连接servlet容器与spring容器的中介;DelegatingFilterProxy在web.xml中配置,注册到servlet容器,servlet容器执行DelegatingFilterProxy的doFilter()方法,doFilter方法内部根据filter名称从spring容器中取出目标filter并执行目标filter的过滤逻辑;
【注意】上述过程,在DelegatingFilterProxy#initFilterBean()方法中设置断点并调试,即可明了 ;
【4】springmvc异常处理与HandlerExceptionResolver(处理器异常解析器)
【4.1】HandlerExceptionResolver-处理器异常解析器
1)HandlerExceptionResolver定义: Handler处理器接口能够设计得如此灵活(如Handler的实现可以是servlet,也可以是Controller),除了HandlerAdapter适配器之外,还因为HandlerExceptionResolver提供的框架内统一的异常处理方式 ;
- 若handler处理请求没有异常,则handler返回ModelAndView,封装了后续处理流程要用的视图和模型数据信息;
- 若handler处理有异常,则由HandlerExceptionResolver接手处理异常 ,封装异常视图与异常提示信息到ModelAndView并返回;
java
public interface HandlerExceptionResolver {
// 处理异常,并把处理结果封装到ModelAndView并返回
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
2)HandlerExceptionResolver子类:
本文使用SimpleMappingExceptionResolver为例介绍HandlerExceptionResolver;
【4.2】SimpleMappingExceptionResolver
1)SimpleMappingExceptionResolver使用 Properties管理具体异常类型与所要转向的错误页面之间的映射关系;
- SimpleMappingExceptionResolver内部遍历exceptionMappings的所有元素,找出与当前抛出异常类型最接近的映射值,并将其映射值作为错误信息页面的逻辑视图名,然后封装到ModelAndView返回以供后续处理流程使用;
2)SimpleMappingExceptionResolver属性定义:
java
public class SimpleMappingExceptionResolver extends AbstractHandlerExceptionResolver {
/** The default name of the exception attribute: "exception". */
public static final String DEFAULT_EXCEPTION_ATTRIBUTE = "exception";
@Nullable
private Properties exceptionMappings;
@Nullable
private Class<?>[] excludedExceptions;
@Nullable
private String defaultErrorView;
@Nullable
private Integer defaultStatusCode;
private final Map<String, Integer> statusCodes = new HashMap<>();
@Nullable
private String exceptionAttribute = DEFAULT_EXCEPTION_ATTRIBUTE;
//...
}
3)SimpleMappingExceptionResolver属性:
- exceptionMappings: 异常类型与异常信息视图属性映射;
- defaultErrorView: 默认异常信息视图逻辑名;
- defaultStatusCode:默认状态码;
- exceptionAttribute:异常属性(前端可以通过该属性获取异常信息);
【4.2.1】SimpleMappingExceptionResolver处理异常代码实践
【applicationContext.xml】配置SimpleMappingExceptionResolver-异常处理器
xml
<!-- 注册SimpleMappingExceptionResolver-处理器异常解析器 -->
<bean name="simpleMappingExceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="defaultErrorView" value="/error/defaultErrorPage" />
<property name="exceptionAttribute" value="exceptionInfo" />
<property name="exceptionMappings">
<props>
<prop key="com.tom.springmvc.exception.TomWebException">/error/tomWebErrorPage</prop>
<prop key="java.lang.Exception">/error/exceptionBaseErrorPage</prop>
</props>
</property>
</bean>
【tomWebErrorPage.jsp】异常信息展示视图页面
jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.List" import="java.util.ArrayList" isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>tomWebErrorPage</title>
</head>
<body>
tomWebErrorPage
<p>异常信息: ${exceptionInfo}</p>
</body>
</html>
【TomWebException】自定义web异常
java
public class TomWebException extends RuntimeException {
public TomWebException() {
super();
}
public TomWebException(String message) {
super("TomWebException-" + message);
}
}
【TomWebThrowExceptionController】抛出异常控制器
java
public class TomWebThrowExceptionController extends AbstractController {
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (Objects.isNull(request.getParameter("testParamKey"))) {
throw new TomWebException("testParamKey查无记录");
}
return new ModelAndView("index");
}
}
【异常处理效果】
【4.3】HandlerExceptionResolver异常处理代码调试
1)对于HandlerExceptionResolver处理器异常解析器提供的统一处理异常细节,还是需要从DispatcherServlet#doDispatch(HttpServletRequest request, HttpServletResponse response)说起;
【DispatcherServlet#doDispatch()】web请求处理入口
java
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 {
// 校验是否multipart请求(包括但不限于文件上传请求)
processedRequest = checkMultipart(request);
// 获取异常处理器,类型为HandlerExecutionChain,它是一个包装器,封装了实际的二级处理器(如Controller)与拦截器列表
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// 根据实际的二级处理器获取处理器适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// ...
// 拦截器前置处理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 实际调用二级处理器的处理方法,二级处理器也就是本文定义的TomWebThrowExceptionController
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 拦击器后置处理
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
// 若二级处理器在处理过程中抛出异常,则在这里被捕获,赋值给dispatchException
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new ServletException("Handler dispatch failed: " + err, err);
}
// 无论是否抛出异常,都执行processDispatchResult()进行后续处理
// 若处理逻辑成功,则dispatchException=null;若抛出异常,则dispatchException就是实际的业务异常
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
// 请求处理完成后触发拦截器afterCompletion方法
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
// 请求处理完成后触发拦截器afterCompletion方法
triggerAfterCompletion(processedRequest, response, mappedHandler,
new ServletException("Handler processing failed: " + err, 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);
}
}
}
}
由上文可知, 二级控制器(或二级处理器,Controller)抛出异常,被捕获后,把异常对象作为入参,调用processDispatchResult方法;
【DispatcherServlet#processDispatchResult()】加工二级控制器的请求处理结果(主要包括处理异常,视图渲染,再执行处理结束的拦截器方法)
java
// 若二级控制器抛出异常,则exception不为空;若处理流程成功,则exception为null
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) { // 异常则进入这个分支
if (exception instanceof ModelAndViewDefiningException mavDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = mavDefiningException.getModelAndView();
}
else { // 有异常,类型为TomWebException,非ModelAndViewDefiningException类型,进入这个分支
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
// 视图渲染(本文不展开)
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
// ...
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null); // 触发执行拦截器的处理结束方法
}
}
由上文可知,本文抛出类型为TomWebException的异常,非ModelAndViewDefiningException类型,执行processHandlerException(request, response, handler, exception);
【DispatcherServlet#processHandlerException()】加工处理器异常(调用处理器异常解析器)
java
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// ...
// Check registered HandlerExceptionResolvers...
// 遍历注册的HandlerExceptionResolver (本文在applicationContext.xml注册的SimpleMappingExceptionResolver)
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
// 遍历处理器异常解析器列表,并调用其resolveException方法,获取处理器异常解析器根据异常封装的ModelAndView;
// 异常解析器按照Ordered语义排序(值越小,优先级越高)
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
// 若没有视图,则使用默认视图
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
// ...
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}
throw ex;
}
由上文可知;异常处理的传播链如下:
- DispatcherServlet#doDispatch():web请求处理入口 ;
- handlerAdapter.handle(processedRequest, response, mappedHandler.getHandler()): DispatcherServlet调用处理器适配器的handle方法执行实际处理器的业务处理逻辑(业务处理逻辑抛出异常);
- DispatcherServlet#processDispatchResult():加工二级控制器的请求处理结果(主要包括处理异常,视图渲染,再执行处理结束的拦截器方法);
- DispatcherServlet#processHandlerException():加工处理器异常(调用处理器异常解析器);
- HandlerExceptionResolver#resolveException(request, response, handler, ex): 处理器异常解析器解析异常,返回解析后的封装了异常信息的ModelAndView对象 ; (因SimpleMappingExceptionResolver继承自AbstractHandlerExceptionResolver,实际调用的是AbstractHandlerExceptionResolver#resolveException)
【AbstractHandlerExceptionResolver#resolveException】处理器异常解析器解析异常方法
java
public ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
if (shouldApplyTo(request, handler)) {
prepareResponse(ex, response);
// 调用doResolveException() 方法解析异常; 调用子类的调用SimpleMappingExceptionResolver#doResolveException()
ModelAndView result = doResolveException(request, response, handler, ex);
if (result != null) {
// ...
}
return result;
}
else {
return null;
}
}
【SimpleMappingExceptionResolver#doResolveException()】处理器异常解析器解析异常方法
java
protected ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
// Expose ModelAndView for chosen error view.
String viewName = determineViewName(ex, request);
if (viewName != null) {
// Apply HTTP status code for error views, if specified.
// Only apply it if we're processing a top-level request.
Integer statusCode = determineStatusCode(request, viewName); // 获取响应码
if (statusCode != null) {
applyStatusCodeIfPossible(request, response, statusCode);
}
// 获取ModelAndView对象
return getModelAndView(viewName, ex, request);
}
else {
return null;
}
}
protected ModelAndView getModelAndView(String viewName, Exception ex, HttpServletRequest request) {
return getModelAndView(viewName, ex);
}
// 封装视图名与异常对象到ModelAndView,并返回
protected ModelAndView getModelAndView(String viewName, Exception ex) {
ModelAndView mv = new ModelAndView(viewName);
if (this.exceptionAttribute != null) {
mv.addObject(this.exceptionAttribute, ex);
}
return mv;
}
【SimpleMappingExceptionResolver#getModelAndView()】