学习 Spring 和 servlet 初期,我们在判断用户身份时,都是在每个方法中获取会话、获取对象,这种方式冗余度高,增加代码复杂度,维护成本也高,因此想到可以使用 AOP 来实现一个公共的方法,这个公共的方法专门做判断用户身份,但 AOP 的通知方法是整个横切面都会其进行拦截的,所以对于有的不需要判断用户身份的方法而言,这样处理太过暴力,也不合法,除此之外,使用 AOP 也在获取获取参数上也较为困难,因此使用 Spring Boot 拦截器来实现更加合适。
下面使用拦截器分别实现(1) 统一登录验证、(2)统一异常处理、(3)统一返回数据格式,这三个实战性的功能。
1. 统一登录验证:
第一步:自定义拦截器
创建一个普通类,实现 HandlerInterceptor 接口,重写 preHandle 方法,由于拦截器非常常用,所以Spring boot 内置了拦截器的依赖;
preHandle
方法:在请求到达Controller之前调用。在拦截器链中的每个拦截器的preHandle
方法都会被依次调用。如果某个拦截器的preHandle
方法返回false
,则后续的拦截器的preHandle
方法和Controller方法都不会被执行,请求将被拦截下来。
postHandle
方法:在Controller方法执行后,DispatcherServlet渲染视图之前调用。在拦截器链中的每个拦截器的postHandle
方法都会被依次调用。可以在这个方法中对ModelAndView进行处理或者添加公共的模型数据。
afterCompletion
方法:在DispatcherServlet完成视图渲染之后调用。在拦截器链中的每个拦截器的afterCompletion
方法都会被依次调用。可以在这个方法中进行一些资源清理操作,比如释放资源、记录日志等。该方法在整个请求处理流程结束之后被调用。
第二步:注册拦截配置
写一个普通方法,实现WebMvcConfigurer接口,重写addInterceptor方法:目的是将自定义拦截器配置到系统配置项里,并设置合理的拦截路径。
第三步:添加 Controller 方法
执行效果:login不能进入,register可以进入
扩展:添加统一访问前缀:
假设一种场景,当多个项目有同名的 url 时,在测试抓包等观察过程中如何分别它们各自属于哪个项目呢?
给请求地址添加一个访问前缀,就可以实现区分了。
加上前缀后还要记得更改拦截路径:
2. 统一异常处理:
统一异常处理是指,某些方法可能会触发同一类异常,我们可以借助拦截器去统一拦截获取到,并根据约定返回需要的结果。
第一步:创建异常处理类和方法
写一个普通类,它内部包含捕获所有异常类的方法,给处理类添加 @ControllerAdvice 注解,给自定义拦截方法添加@ExceptionHandler 注解,注解内传参要传要捕获的异常类对象:
第二步:编写可能出现异常的方法
有异常检测时,如果出现对应异常,服务器依然按照异常处理方法里的规定给前端返回该返回的东西:
当没有异常检测时,即使出现对应的异常,服务器的返回和约定无关:
2.1 父子异常:
上面这种,注解@ExceptionHandler 参数中只有一种异常时,只能捕获处理对应这一种异常,如果想处理很多异常,不用每个都写一个方法,可以在参数中直接传所有异常的父类异常类如:Exception.class
如果有子异常和父异常的处理方法同时存在时,就先匹配子异常检测方法
3. 统一数据格式返回:
在日常开发中,后端每个业务给前端返回的数据格式要符合预定格式,但如果有的人忘记了约定格式、或者新人不知道预定格式,可能会按照自己的想法返回数据,这样就会导致业务事故。
统一数据返回,使用 @ControllerAdvice 注解 + ResponseBodyAdvice 接口实现,我们可以在自定义类中设定好符合约定的返回数据格式,然后拦截每个业务返回的数据,判断它返回的数据格式是否符合约定规范,如果不符合就重新封装数据,返回拦截规则里的数据格式。
强制性统一数据返回,在返回数据之前进行数据重写
第一步:创建数据处理类
写一个普通类,实现 ResponseAdvice 接口,重写 supports() 和 beforeBodyWrite() 方法。
- supports() 方法:该方法相当于一个开关,用来告诉处理方法是否要重写拦截到的内容,返回 true 表示要重写;
- beforeBodyWrite() 方法:拦截到业务方法要返回的数据,在它返回自己数据前重写错误格式的数据;
第二步:创建两个可能出现错误格式数据的方法
getRet1() 方法返回一个 1,getRet2() 方法返回一个 hashMap
约定正确的格式为一个 hashMap
总结:
这种自定义拦截数据格式返回存在两个问题:
1.处理方法中设定的返回内容的值是固定写死的;
2.当返回String类型,不能被处理成正确数据;
3.1 扩展:解决转换 String 类型返回值出现错误的问题
先演示一下,getRer3() 方法,返回的是字符串,理想状态应该是被拦截后被转换成 封装好的 hashMap进行返回,但是没有这样做,因为无法转换,所以出现类转换异常,被之前写的异常处理类捕获到返回异常: HashMap 不能转换为 String
错误原因:
肯定有疑问:我明明是将 String 转换为 hashMap,为什么报错信息是反过来的?
这里要明白返回的执行流程:
- 原方法返回的原 body 是 String 类型;
- 统一数据返回之前:将 String 转换为 hashMap;
- 浏览器会借助 StringHttpMessageConverter 将 hashMap 转换为 json 字符串;
问题就出在了第三步,浏览器判断时用原body判断是不是 String 类型,是的话就用 StringHttpMessageConverter 将 hashMap 转换为 json,发现不能转换,所以报错。
解决方案1:
在统一数据重写时,单独处理返回值为 String 类型的情况,重写成返回一个 Json 字符串(可以拼接、可以使用jackson),而非 HashMap;
使用 Jackson 转成 String:
解决方案2:
既然问题出在了浏览器的转换器上,那我们就想办法不用这个转换器了;
可以手动将 StringHttpMessageConverter 转换器去掉:
这样就好了:
4. 拦截器的实现原理:
图解:
所有的 Controller 执行都会通过⼀个调度器 DispatcherServlet 来实现
上图解释了拦截器的实现原理:
请求到达DispatcherServlet,DispatcherServlet是前端控制器,负责接收请求并进行分发。
DispatcherServlet根据配置的拦截器链,依次调用每个拦截器的方法。
拦截器链中的每个拦截器都实现了 HandlerInterceptor 接口,拦截器的方法会在请求处理的不同阶段被调用。
在调用每个拦截器的方法之前和之后,会根据返回值来决定是否继续执行下一个拦截器或者Controller方法。
当所有拦截器的方法都执行完毕后,DispatcherServlet会进行视图渲染,生成响应结果。
通过拦截器的实现,可以在请求处理的不同阶段进行拦截和处理,实现一些通用的功能,比如权限验证、日志记录、异常处理等。拦截器的实现原理基于Java的反射机制和设计模式,通过动态代理生成代理对象,来实现拦截器的调用。