文章目录
- 一、拦截器
- 1.1 简介
- 1.2 拦截器的使用
- 1.2.1 创建
- 1.2.2 配置
- 1.2.3 测试
- 1.3 多个拦截器的执行顺序
- 1.3.1 preHandle()方法返回true
- 1.3.2 preHandle()方法返回false
- 二、异常处理器
- 2.1 简介
- 2.2 配置
- 2.2.1 springmvc.xml中配置
- 2.2.2 注解配置
学习视频🎥:https://www.bilibili.com/video/BV1Ry4y1574R
一、拦截器
1.1 简介
💬概述:Spring MVC中提供了拦截器HandlerInterceptor
,类似于过滤器,可以在执行当前请求对应控制器方法之前对请求进行拦截,同时可以对当前请求进行加工,可以同时设置多个拦截器
🔑三个重要方法
方法名 | 执行时间点 | 说明 |
---|---|---|
boolean preHandle() | 控制器方法执行前 | 前置处理方法 --> 方法返回值表示拦截和放行(true表示放行,即正常执行控制器方法;返回false表示拦截,即不执行控制器方法) |
void postHandle() | 控制器方法执行后 | 后置处理方法 --> 在控制器方法执行之后执行,如果请求被拦截,即控制器方法不执行,则该方法也不执行 |
void afterCompletion() | 处理完视图和模型数据并对视图进行渲染之后 | 完成处理方法 --> 在完成视图和模型数据处理并对视图进行渲染之后执行,如果请求被拦截,即控制器方法不执行,后面的数据处理和视图渲染也不会执行,所以该方法也不执行 |
🔑从DispatcherServlet源码观察三个方法的执行位置
① 打开DispatcherServlet
源码(按两下shift键),找到doDisptch()
方法
② 在doDispatch()
方法中,找到ModelAndView
对象mv
,以及ha.handle()
方法,由之前的分析可知,ha.handle()
方法的执行间接或直接调用了对应的控制器方法,所以handle()
方法相当于控制器方法,可以看到该方法前面由一个mappedHandler.applyPreHandle()
方法,该方法中调用了preHandle()
前置处理方法(可点击方法进入查看),所以applyPreHandle()
相当于preHandle()
。同理可得,在handle()
方法后面有一个mappedHandler.applyPostHandle()
方法,而applyPostHandle()
也相当于postHandle()
③ 由之前的分析可知,doDispatch()
方法中会调用processDispatchResult()
方法对请求结果进行处理,然后processDispatchResult()
方法中又会调用render()
方法对视图进行渲染,进入processDispatchResult()
方法中可以看到,在render()
方法执行之后,有一个mappedHandler.triggerAfterCompletion()
方法,triggerAfterCompletion()
方法中同样调用了afterCompletion()
方法,所以该方法也就是完成处理方法
④ 从源码中可以得出拦截器中三个方法的执行顺序为:Ⅰ.preHanlde()
--放行–> Ⅱ.控制器方法 --> Ⅲ.postHandle()
--完成视图和模型数据处理和渲染视图–> Ⅳ.afterCompletion()
📸拦截器的大致执行过程图
🔑拦截器 🆚 过滤器
参考博文:Spring中的拦截器
-
相同点
① 都有优先处理请求的权利,都可以决定是否将请求转移到请求的实际处理的控制器处
② 都可以对请求或者会话当中的数据进行加工 -
不同点
① 过滤器只负责前面的过滤行为;而拦截器可以做前置处理也可以做后置处理,还可以进行完成处理,控制得更加细致
② 过滤器优先执行,还是拦截器优先呢? --> 过滤器优先
③ 过滤器是servlet规范里面的组件;拦截器都是框架自己而外添加的组件
1.2 拦截器的使用
1.2.1 创建
🔑创建方式:添加interceptor包,在该包下创建一个类TestInterceptor
,在类上添加@Component
注解(直接交给Spring IOC容器创建对象),然后实现HandleInterceptor
接口,并重写实现接口的方法,接口的方法就是拦截器中的三个重要方法——preHandle()
、postHandle()
和afterCompletion()
@Component
public class TestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
return false;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
}
1.2.2 配置
🔑配置方式:在springmvc.xml文件添加<mvc:interceptors>
标签,用于配置拦截器,在<mvc:interceptors>
标签中需要创建拦截器对象,创建方式有三种
<!-- 配置拦截器 -->
<mvc:interceptors>
<!-- 创建拦截器类对象,并设置拦截路径 -->
</mvc:interceptors>
① <bean>
标签
- 使用:在
<bean>
的class
属性直接写上拦截器类的全类名 - 设置拦截路径:无法设置拦截路径,默认为拦截全部请求路径
<bean class="com.key.mvc.interceptor.TestInterceptor"></bean>
💡 如果使用
<bean>
标签创建拦截器对象,则不用在类上添加@Component
注解,因为<bean>
本身就是用于创建对象的标签
② <ref>
标签
- 使用:在
<ref>
的bean
属性直接写上拦截器类对应的bean-id值 - 设置拦截路径:无法设置具体的拦截路径,默认为拦截全部请求路径
<ref bean="testInterceptor"></ref>
💡 因为
<ref>
标签本身就是用来导入外部Bean,所以使用<ref>
的前提是拦截器类已经创建好对象,因此必须要在拦截器类上添加@Component
注解(当然也可以在xml文件配置),此时拦截器类的bean-id值就是第一个字母改成小写后的类名
③ <mvc:interceptor>
标签:在<mvc:interceptors>
标签中又有三个标签,用于配置具体的拦截路径和创建拦截器对象
-
设置拦截路径
Ⅰ.
<mvc:mapping path="拦截路径"/>
:在path
中设置具体的拦截路径💡 如果需要拦截全部请求路径,则设置为
/**
,而不是/*
❓ 为什么是
/**
而不是/*
:因为/*
中的*
是指0个或多个字符,类似于Ant风格中的通配符,所以/*
仅表示一层路径,如果请求路径为多层,如/a/testInterceptor
就不会被拦截;而/**
中的有两个**
,就表示0或多层路径,所以/**
能匹配到所有请求路径Ⅱ.
<mvc:exclude-mapping path="请求路径"/>
:在path
是属性中设置不进行拦截的请求路径 -
创建拦截器对象:使用
<bean>
或<ref>
标签即可<mvc:interceptor> <!-- 拦截所有请求资源,除了请求路径为 / 的请求(首页) --> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/"/> <!-- 创建拦截器对象 --> <ref bean="testInterceptor"></ref> </mvc:interceptor>
1.2.3 测试
① 将拦截器类TesInterceptor
的preHandle()
方法返回值设置为true,并在三个方法中打印测试信息
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
System.out.println("①执行前后处理方法 --> preHandle()");
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
System.out.println("③执行后置处理方法 --> postHandle()");
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("④执行完成处理方法 --> afterCompletion()");
}
② 配置拦截器,采用第三种方式标识拦截器类,并设置拦截路径为全部路径,除了首页(/
)
③ 创建对应的测试控制器,控制器中创建测试控制器方法,对应请求路径设置为/**/testInterceptor
(Ant风格),方便测试多个不同的路径,同时在方法中打印测试信息
@RequestMapping("/**/testInterceptor")
public String showTestInterceptor() {
System.out.println("②执行控制器方法 --> showTestInterceptor()");
return "success";
}
④ 在首页添加测试超链接
<a th:href="@{/testInterceptor}">测试拦截器</a>
⑤ 测试不同请求路径,观察控制器打印情况
1.3 多个拦截器的执行顺序
1.3.1 preHandle()方法返回true
💬概述:如果配置的每一个拦截器中的preHandle()
方法都返回true,即每一个拦截器都采取放行的操作,此时拦截器中的三个方法的执行顺序只与拦截器在springmvc.xml文件中配置的顺序有关
① preHandle()
方法:每一个拦截器中的preHandle()
都能被执行,且执行顺序与拦截器的在springmvc.xml中配置的顺序一致
② postHandle()
方法:每一个拦截器中的postHandle()
都能被执行,且执行顺序与拦截器的在springmvc.xml中配置的顺序相反
③ afterComplaetion()
方法:每一个拦截器中的afterCompletion()
都能被执行,且执行顺序与拦截器的在springmvc.xml中配置的顺序相反
🔑测试
① 创建两个拦截器,在springmvc.xml文件中的配置顺序为——Ⅰ.FirstInterceptor
、Ⅱ.SecondInterceptor
,两个拦截器中的preHandle()
方法返回值都为true
<mvc:interceptors>
<!-- 拦截器① -->
<ref bean="firstInterceptor"></ref>
<!-- 拦截器② -->
<ref bean="secondInterceptor"></ref>
</mvc:interceptors>
② 运行服务器后,点击测试链接,查看控制台打印情况
🔑从源码分析拦截器的执行顺序
❓ applyPreHandle()、applyPostHandle()和triggerAfterCompletion()三个方法关系:三个方法都是
HandlerExecutionChain
类中的方法,而在doDispatch()
方法中的mappedHandler
对象就是HandlerExecutionChain
类型的对象
① preHandle()
方法
-
进入
DispatcherServlet
前端控制器的doDispatch()
方法中,找到mappedHandler.applyPreHandle()
方法,之前已经介绍过,该方法内部调用了前置处理方法——preHandle()
-
进入
applyPreHandle()
方法中,可以看到方法中有一个interceptorList
——拦截器集合,该集合封装了全部拦截器(集合的元素对应每一个拦截器) -
通过debug模式启动,并在
applyPreHanlde()
方法上打上断点,查看拦截器集合的详情,可以看到集合中有三个元素,分别为ConversionServiceExposingInterceptor
、FirstInterceptor
和SecondInterceptor
,索引分别为0、1、2,由此可见,越先配置的拦截器,在集合中的索引值越小,即位置越靠前❓
interceptorList
中三个拦截器的来源Ⅰ.
ConversionServiceExposingInterceptor
:Spring MVC中创建的拦截器,无需我们创建和配置,所以它肯定是最先被配置的,因此它在拦截器集合中肯定是第一个元素(索引值为0)
Ⅱ.FirstInterceptor
和SecondInterceptor
:后面两个拦截器都是我在测试中创建和配置的,配置的顺序为同上,因此它们在集合中的索引值分别为1、2 -
继续看到
applyPreHandle()
方法,方法中采用for循环正序遍历InterceptorList
集合(从第一个元素遍历到最后一个元素),循环中获取集合中每一个拦截器对象,然后调用每一个拦截器的preHandle()
方法,因为是正序循环遍历,因此每一个拦截器的preHandle()
方法的执行顺序就跟集合中拦截器的顺序一致,即跟每一个拦截器的配置顺序一致 -
在for循环中可以看到,当前获取的拦截器对象的
preHandle()
返回值为true时,for循环才会继续执行,而且applyPreHandle()
方法才会返回true;如果有一个拦截器的preHandle()
返回false,则直接结束for循环,并返回false(下面会讨论返回false的情况),因此只有每一个拦截器的preHandle()
方法都返回true时,每一个preHandle()
才能都被执行,applyPreHandle()
才能返回true💡 由上面测试中的打印结果可知,测试的两个拦截器的
preHandle()
方法都被执行了,因此可以得出Spring MVC创建的ConversionServiceExposingInterceptor
拦截器中的preHandle()
返回值肯定也是true -
在循环还可以看到一个
interceptorIndex
变量,即拦截器索引,初始值为-1,当执行完本次循环获取的拦截器对象中的preHandle()
方法后,如果循环还能继续(preHandle()
返回true),则interceptorIndex
加一,所以该变量是用来记录拦截器集合中第一个preHandle()
返回false的拦截器的前一个拦截器在集合中的索引值,如果集合中没有preHandle()
返回false的拦截器,则interceptorIndex
记录的是最后一个拦截器在集合中的索引值,interceptorIndex
会在triggerAfterCompletion()
方法中使用到
② postHandle()
方法
-
继续回到前端控制器的
doDispatch()
方法中,找到mappedHandler.applyPostHandle()
方法,该方法内部调用了后置处理方法postHandle()
-
进入
applyPostHandle()
方法中,可以看到方法中和applyPreHandle()
同样有拦截器集合interceptorList
,也同样采用for循环遍历拦截器集合,for循环中也同样获取了每一个拦截器对象并调用其postHandle()
方法,不一样的是,applyPostHandle()
中的for循环是反序遍历(从集合的最后一个元素遍历到第一个元素),因此在循环中,是先调用集合索引值较大(即位置靠后)的拦截器中的postHandle()
方法,所以每一个拦截器的postHandle()
方法的执行顺序是跟集合中拦截器的顺序相反,即跟拦截器的配置顺序相反,也是跟preHandle()
的执行顺序相反
③ afterComplaetion()
方法
-
回到前端控制器的
doDispatch()
方法中,找到processDispatchResult()
,进入该方法可以找到mappedHandler.triggerAfterCompletion()
方法,该方法中就调用了完成处理方法afterCompletion()
-
进入
triggerAfterCompletion()
中,可以看到interceptorIndex
变量,该变量就是applyPreHandle()
方法中的拦截器索引变量,因为applyPreHandle()
在triggerAfterCompletion()
之前执行,所以在triggerAfterCompletion()
方法中的interceptorIndex
的值为拦截器集合中第一个preHandle()
返回false的拦截器的前一个拦截器在集合中的索引值 -
triggerAfterCompletion()
方法中与前两个方法类似,同样有一个for循环,同样是遍历拦截器集合,同样在循环中获取每一个拦截器对象并调用器afterCompletion()
方法,不一样的是,triggerAfterCompletion()
中的for循环的是反序遍历且起始索引为interceptorIndex
,而所有拦截器的preHandle()
方法都返回true的情况下,interceptorIndex
的值就是最后一个拦截器在集合中的索引,因此在triggerAfterCompletion()
方法中,所有拦截器的afterCompletion()
方法都会被执行
1.3.2 preHandle()方法返回false
💬概述:如果配置的多个拦截器中,有一个或者多个拦截器的preHandle()
方法返回值为false,即采取不放行的操作,此时拦截器中的三个方法的执行顺序不仅与拦截器的配置顺序有关,也与第一个preHandle()
返回false的拦截器在拦截器集合中的位置有关
① preHandle()
方法:在拦截器集合中,第一个preHandle
返回false的拦截器以及它之前的拦截器中的preHandle()
都能被执行(包括preHandle()
返回false的拦截器),且执行顺序与拦截器的在springmvc.xml中配置的顺序一致,之后的都不会被执行
② postHandle()
方法:所有拦截器中的postHandle()
都不会被执行
③ afterComplaetion()
方法:在拦截器集合中,第一个preHandle
返回false的拦截器之前的拦截器中的afterCompletion()
都能被执行(不包括preHandle()
返回false的拦截器),且执行顺序与拦截器的在springmvc.xml中配置的顺序相反,之后的都不会被执行
🔑测试
① 创建并配置三个测试拦截器——firstInterceptor
、secondInterceptor
和ThirdInterceptor
,配置顺序为Ⅰ.FirstInterceptor
、Ⅱ.SecondInterceptor
、Ⅲ.ThirdInterceptor
,secondInterceptor
的preHandle()
方法返回值设置为false,其他两个拦截器的preHandle()
方法返回值都是true
<mvc:interceptors>
<!-- 拦截器① -->
<ref bean="firstInterceptor"></ref>
<!-- 拦截器② -->
<ref bean="secondInterceptor"></ref>
<!-- 拦截器③ -->
<ref bean="thirdInterceptor"></ref>
</mvc:interceptors>
② 运行服务器后,点击测试链接,查看控制台打印情况
③ 查看浏览器页面的显示结果
🔑从源码分析拦截器的执行顺序
① preHandle()
方法
-
进入
doDispatch()
方法中,同样打开applyPreHandle()
方法,打上断点,debug模式启动,查看此时的拦截器集合 -
在
applyPreHandle()
方法中可以看到如果for循环中调用的某个拦截器中的preHandle()
方法返回值为false时,就会直接执行triggerAfterCompletion()
方法,执行完后就返回false,此时for循环结束,applyPreHandle()
方法也执行结束,返回值就是false💡 因为
preHandle()
和triggerAfterCompletion()
在同一个类中,所以在preHandle()
可以直接调用this.triggerAfterCompletion()
,无需创建对象来调用 -
但在第一个
preHandle
返回false的拦截器之前的拦截器,还是能正常遍历到,而且preHandle()
方法也正常执行,并且interceptorIndex
也正常的加一,直到遇到第一个preHandle
返回false的拦截器时,interceptorIndex
就不会在加一(因为for循环已经结束),因此它记录就是第一个preHandle()
返回false的拦截器的前一个拦截器在集合中的索引值 -
回到
doDispatch()
中可以看到,当applyPreHandle()
方法返回false时,直接就return
,即结束doDispatch()
的执行,mappedHandler.applyPreHandle()
后面的代码就不会再执行,因此控制器方法以及拦截器中的applyPostHandle()
方法也就不会再执行,而applyPostHandle()
方法不执行就会导致所有拦截器的postHandle()
都不会被执行
② afterComplaetion()
方法
-
由上述分析可知,在
applyPreHandle()
方法中,如果遇到preHandle()
返回false的拦截器,就直接调用triggerAfterCompletion()
方法,由此可得,即使有拦截器的preHandle()
返回false,拦截器中的afterCompletion()
方法还是能正常执行 -
进入
triggerAfterCompletion()
方法中,由于applyPreHandle()
方法同样执行在triggerAfterCompletion()
之前,所以interceptorIndex
的值同样是拦截器集合中第一个preHandle()
返回false的拦截器的前一个拦截器在集合中的索引值,因此在循环中,是先调用索引值为interceptorIndex
的拦截器中的afterCompletion()
方法,然后往索引值减小的方向遍历,因此在interceptorIndex
索引值之前(包括interceptorIndex
)的拦截器中的afterCompletion()
方法都能被执行,且执行顺序与集合中拦截器的顺序相反,而interceptorIndex
索引值之后的拦截器中的afterCompletion()
方法都不会被执行
二、异常处理器
2.1 简介
💬概述:SpringMVC提供了一个异常处理器接口——HandlerExceptionResolver
,该接口是Spring MVC中最底层的接口
🔑作用:异常处理器可以处理控制器方法在执行过程中遇到的所有异常,在遇到异常时也可以自定义跳转的视图页面
🔑两个主要的实现类
DefaultHandlerExceptionResolver
:Spring MVC中默认使用的异常处理器,无需我们配置SimpleMappingExceptionResolver
:Spring MVC中提供的一个可以自定义异常处理的异常处理器,即可以对每一个异常设置指定的视图名,当遇到对应异常时就跳转到指定的视图,需要我们在配置文件中配置或使用注解配置
2.2 配置
2.2.1 springmvc.xml中配置
🔑配置方式
<!-- 配置异常处理器,自定义异常对应的视图 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!-- 属性① -->
<property name="exceptionMappings">
<!-- 通过<props>标签注入 -->
<props>
<!-- 映射关系写在<prop>标签中 -->
<prop key="java.lang.ArithmeticException">error</prop>
</props>
</property>
<!-- 属性② ,request域中异常信息对应的键为ex-->
<property name="exceptionAttribute" value="ex"></property>
</bean>
-
在Spring MVC核心配置文件springmvc.xml中添加
<bean>
标签,将SimpleMappingExceptionResolver
实现类的全类名导入,即创建SimpleMappingExceptionResolver
实现类的对象 -
然后在
<bean>
标签中添加<property>
标签,为类注入属性,SimpleMappingExceptionResolver
有两个重要属性① exceptionMappings(异常映射):该属性是
Proerties
类型,用于将异常类与对应需要跳转的视图名建立映射关系;属性值用<props>
标签书写,<props>
标签中再添加<prop>
标签,<prop>
标签中添加需要处理的异常全类名以及与之建立映射关系的视图名🔺
<prop>
标签介绍- key:
<prop>
标签的一个属性,属性值为异常对应的的全类名 - 标签体中的内容:标签体中内容就是与异常建立映射关系的视图名,该视图名的书写与控制器方法中返回值一样(改变视图名的前缀同样可以改变视图对象的类型)
② exceptionAttribute:该属性用于将当前出现的异常信息(异常全类名和对应的错误类型)存储到request域中进行共享,对应的页面上就可以从request域中获取出异常信息并打印到页面上
🔺
exceptionAttribute
的属性值ex
:exceptionAttribute
的属性值ex
对应此时request域中存储异常信息的数据名,即键(数据值就是异常信息),在对应页面上直接通过ex
获取request域中存储的异常信息(request域中数据无需使用内置对象获取) - key:
🔑测试
① 创建一个测试控制器方法,方法中自定义一个数学异常(1/0)
@RequestMapping("/testEx")
public String showTestEx() {
int i = 1/0;
return "success";
}
② 在springmvc.xml中配置异常处理器SimpleMappingExceptionResolver
,其中添加数学异常ArithmeticException
,与异常建立映射关系的视图名为error,并将异常信息存储到request域中
③ 创建异常对应的页面error.html,获取request域中的异常信息并打印到页面上
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>异常处理</title>
</head>
<body>
<p>不好意思!出现异常了!</p><br/>
<!-- 打印异常信息 -->
<p th:text="${ex}"></p>
</body>
</html>
④ 测试结果
2.2.2 注解配置
🔑配置方式
- 创建一个类,在类上添加
@ControllerAdvice
注解,然后创建处理异常的方法,该方法与控制器方法类似,返回值类型为String,返回值同样也对应一个视图名,但这里设置的是与异常建立映射关系的视图名 - 然后在方法上添加
@ExceptionHandler
注解,注解中的value
属性值对应的是要处理的异常类的class对象 - 方法形参上添加一个异常对象
ex
以及可以处理request域对象数据的对象,如model
对象(ModelMap
、Map
都可),Spring MVC会自动识别出形参上异常对象ex
,并将当前的异常信息存储到该对象中,而形参上的model
对象则用来将ex
存储到request域中
@ControllerAdvice
public class ExceptionController {
/**
* 处理异常的方法
*/
@ExceptionHandler(ArithmeticException.class)
public String showExPage(Exception ex, Model model) {
// 将异常对象存储到request域中
model.addAttribute("ex", ex);
return "error";
}
}
❓ 异常处理器中使用的两个注解
@ControllerAdvice
:作用于类上,将对应类标识为异常处理器组件,该注解中包含了@Component
注解,因此被@ControllerAdvice
注解标识的类在一定程度上也是一个控制器,而且在该类里创建的方法与控制器方法也类似@ExceptionHandler
:作用于异常处理器类里的方法上,注解中的value
属性是一个数组,因此在@ExceptionHandler
注解中可以配置多个异常类,但方法中只能返回一个视图名,说明一个视图名(即一个页面)可以对应多个异常
🔑测试
① 创建测试控制器方法,添加一个数学异常(同上)
② 创建异常处理器类,类上用@ControllerAdvice
标识,类里创建处理异常的方法,方法上添加@ExceptionHandler
注解,注解中添加两个异常类的class对象(数学异常和空指针),方法返回值即视图名设置为error
@ExceptionHandler(
value = {ArithmeticException.class, NullPointerException.class}
)
public String showExPage(Exception ex, Model model) {
// 将异常对象存储到request域中
model.addAttribute("ex", ex);
return "error";
}
③ 创建异常对应的页面error.html,页面上打印出对应的异常信息(同上👆)
④ 测试结果:结果同上👆