文章目录
- 前言
- 1.⽤户登录权限效验
-
- 1.1、最初⽤户登录验证
- 1.2、Spring AOP ⽤户统⼀登录验证的问题
- 1.3、Spring 拦截器
-
- 了解 创建一个 Spring 拦截器 的流程
-
- 1、 创建⾃定义拦截器,实现 HandlerInterceptor 接⼝的preHandle(执⾏具体⽅法之前的预处理)⽅法。
- 2、将⾃定义拦截器加⼊到框架的配置中,并且设置拦截规则。
- 实践:实现一个自定义的拦截器,使其在项目中生效。
-
- 预备工作:创建一个 Spring AOP 的项目。
- 1、 创建⾃定义拦截器,实现 HandlerInterceptor 接⼝的preHandle(执⾏具体⽅法之前的预处理)⽅法。
- 2、将⾃定义拦截器加⼊到框架的配置中,并且设置拦截规则。
- 3、验收成果
- 拦截器实现原理
-
- 实现原理源码分析
- 拦截器小结
- 扩展:统⼀访问前缀添加
- 2.统⼀异常处理
- 3.统⼀数据返回格式
-
- 为什么需要统⼀数据返回格式?
- 统⼀数据返回格式的实现
- 总结
-
- 最后,补充一点:@ControllerAdvice 源码分析 - 了解
前言
接下来是 Spring Boot 统⼀功能处理模块了,也是 AOP 的实战环节,要实现的⽬标有以下 3 个:
1、统⼀⽤户登录权限验证;
2、统⼀数据格式返回;
3、统⼀异常处理。接下我们⼀个⼀个来看。
1.⽤户登录权限效验
⽤户登录权限的发展从之前每个⽅法中⾃⼰验证⽤户登录权限,到现在统⼀的⽤户登录验证处理,它是⼀个逐渐完善和逐渐优化的过程。
1、最初的用户登录效验:在每个方法里面获取 session 和 session 中的 用户信息,如果用户信息存在,那么就登录成功了,否则就登录失败了。
2、第二版用户登录效验:提供了统一的方法,在每个需要验证的方法中调用统一的用户登录身份效验方法来判断。
3、第三版用户登录效验: 使用 Spring AOP 来使用统一的用户登录效验。
就是说:不再需要我们敲代码去调用统一方法了,Spring AOP 会帮我们自动调用。
1.1、最初⽤户登录验证
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 某⽅法 1
*/
@RequestMapping("/m1")
public Object method(HttpServletRequest request) {
// 有 session 就获取,没有不会创建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null)
{
// 说明已经登录,业务处理
return true;
} else {
// 未登录
return false;
}
}
/**
* 某⽅法 2
*/
@RequestMapping("/m2")
public Object method2(HttpServletRequest request) {
// 有 session 就获取,没有不会创建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null)
{
// 说明已经登录,业务处理
return true;
} else {
// 未登录
return false;
}
}
// 其他⽅法...
}
从上述代码可以看出,每个⽅法中都有相同的⽤户登录验证权限,它的缺点是:
1、 每个⽅法中都要单独写⽤户登录验证的⽅法,即使封装成公共⽅法,也⼀样要传参调⽤和在⽅法中进⾏判断。
2、 添加控制器越多,调⽤⽤户登录验证的⽅法也越多,这样就增加了后期的修改成本和维护成本。
3、 这些⽤户登录验证的⽅法和接下来要实现的业务⼏何没有任何关联,但每个⽅法中都要写⼀遍。所以提供⼀个公共的 AOP ⽅法来进⾏统⼀的⽤户登录权限验证迫在眉睫。
1.2、Spring AOP ⽤户统⼀登录验证的问题
说到统⼀的⽤户登录验证,我们想到的第⼀个实现⽅案是 Spring AOP 前置通知或环绕通知来实现,实现模板代码如下:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserAspect {
// 定义切点⽅法 controller 包下、⼦孙包下所有类的所有⽅法
@Pointcut("execution(* com.example.demo.controller..*.*(..))")
public void pointcut(){ }
// 前置⽅法
@Before("pointcut()")
public void doBefore(){
}
// 环绕⽅法
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object obj = null;
System.out.println("Around ⽅法开始执⾏");
try {
// 执⾏拦截⽅法
obj = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("Around ⽅法结束执⾏");
return obj;
}
}
如果要在以上 Spring AOP 的切⾯中实现⽤户登录权限效验的功能,有以下两个问题:
1.、没办法获取到 HttpSession 对象。
2、我们要对⼀部分⽅法进⾏拦截,⽽另⼀部分⽅法不拦截,
如用户的注册⽅法和登录⽅法是不拦截的,这样的话切点方法的拦截规则很难定义,甚⾄没办法定义。
那这样如何解决呢?
接下来,我们就使用 Spring 提供的方案来解决 原始 AOP 所带来的问题。
这个解决方案,也就是 第四版用户登录效验,也是下面要讲的内容:Spring 拦截器。
1.3、Spring 拦截器
对于以上问题 Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现分为以下两个步骤:
1、 创建⾃定义拦截器,实现 HandlerInterceptor 接⼝的 preHandle(执⾏具体⽅法之前的预处理)⽅法。
2、 将⾃定义拦截器加⼊ WebMvcConfigurer 的 addInterceptors ⽅法中。有的人可能会有疑问:Spring 拦截器 和 AOP 有什么关系呢?
Spring 拦截器是基于 AOP 实现的。
因为 原生 AOP 没有办法获取 httpSession 和 Request 对象。而且,拦截规则也非常难以定义。
所以,官方就做了一个拦截器。
然后,把我们之前最基础的 原生 AOP 做了一个封装
得到了一个新东西 “ Spring拦截器 ”。
我们也可以将 Spring 拦截器 称为是 Spring AOP 的一种实现。
Spring 拦截器中,封装了很多对象,对象里面就提供了专门的方法 来解决 原生 AOP 所带来的两个问题。
了解 创建一个 Spring 拦截器 的流程
1、 创建⾃定义拦截器,实现 HandlerInterceptor 接⼝的preHandle(执⾏具体⽅法之前的预处理)⽅法。
HandlerInterceptor 的 中文意思 就是 拦截器。
重写 的 preHeadle 方法,返回值的类型是 布尔类型。返回是 true,则表示通过了 拦截器的验证,可以继续 执行,调用 目标方法了。
反之,验证没有通过,直接返回一个错误信息。总的来说:拦截器 运行的模式 和 Spring AOP 是一样的。
起着一个 代理的作用。现在是 前端先访问 拦截器,执行里面的 preHandle 方法,如果方法返回一个 true,则继续执行后面的层序。
反之如果返回的是 false,直接返回一个 错误信息,后面的代码就不用执行了。下面我们来看个具体的代码
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null)
{
return true;
}
response.setStatus(401);
return false;
}
}
我们可以发现: preHandle方法中提供了 HttpServletRequest 和HttpServletResponse 的 对象!!!
有了请求对象,我们就可以获取到 Session 对象,从而获取到里面的用户信息。
也就意味着我们可以进行用户登录状态的检测了!
写法 和 Servlet 是 一样的,这个你看上面的代码就知道了。
而且,我们可以通过响应对象,直接向前端返回一个 JSON 格式(或者 HTML)的数据。
甚至,我还可以实现一些业务。
假设 用户处于未登录 状态,我们可以通过 HttpServletResponse 发送重定向。
让用户直接跳转到 登录界面,让他先去登录。需要注意的是:
虽然我们实现了拦截器,但是 preHandle 方法 是一个普通方法。
没有程序去调用它。
因为我们没有给它加上任何注解。
没有加注解,意味着 框架的 启动 和 这个没有任何关系。
另外,这个自定义拦截器的方法,只是具有拦截器的业务逻辑,但是没有拦截的规则!
所以,才会有第二步:将⾃定义拦截器加⼊到系统配置。
或者说:配置到当前项目中。并且设置拦截的规则
2、将⾃定义拦截器加⼊到框架的配置中,并且设置拦截规则。
将上一步中的自定义拦截器加入到框架配置中,具体代码实现如下:
@Configuration
public class AppConfig implements WebMvcConfigurer {
// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截所有接⼝
.excludePathPatterns("/art/param11"); // 排除接⼝
}
}
大家需要注意一点:
不管是 Spring,还是 Spring Boot。
它们默认的配置文件都是叫做 Spring MVC Configurer 的一个文件。
我们要设置当前的一些配置信息的时候,我们是一定要去实现WebMvcConfigurer 接口的。
然后,去重写里面的 addInterceptors(添加多个拦截器)方法。
只有这样,系统才会认为 当前设置的东西 是给 当前项目去设置的配置文件。
所以说:光加上 @Configuration 注解是不行,还必须实现 WebMvcConfigurer 接口,重写里面的方法,当前这个类才是一个配置文件。
这是一种固定写法,注解 和 实现接口,都不能少。
所有的当前项目的配置文件,都是 来自于实现 WebMvcConfigurer 接口 。
所以,这个接口是一定要实现的。
实现它之后,
在当前类上, 加上@Configuration 注解,是为了让 当前这个类 作为 系统的一个配置类。
此时,类里面设置的东西(在这里指的是拦截器),才能生效。
而 addInterceptors - 添加多个拦截器方法,表示 拦截器,可以添加(支持)多个自定义拦截器。
即:一个项目中可以设置多个拦截器。
比如:
1、一个拦截器 去做登录的效验
2、一个拦截器 去验证 前端数据 的正确性 合法性。
等等。。。
并且,不同的拦截器可以配置 不同的路由规则。
这个先暂且方法,后面会细讲⾃定义拦截器加⼊到框架的配置中,并且设置拦截规则。
实现步骤:
1、在类上,添加 Configuration 注解,使其成系统的配置类
2、当前类实现 WebMvcConfigurer 接口
3、重写 WebMvcConfigurer 接口中的 addInterceptors 方法。
上面讲述了 实现 Spring 拦截器的流程。
下面,我们可以开始实践了!
实践:实现一个自定义的拦截器,使其在项目中生效。
预备工作:创建一个 Spring AOP 的项目。
我以专业版为准,跟社区版都一样的。
只是 社区版需要安装 插件 Spring Assistant 插件,我不就再赘述。在上一篇 AOP 理论知识博文中,就讲过了 如何创建一个 aop 项目。
但是!现在有点不同!
就是 这个 Spring 拦截器,Spring Boot 项目 本身就支持!
所以,就不用引入 AOP 框架的支持了。
如果你们想了解 原生 AOP 的 操作模式,可以前面的 aop 文章。
其实,你们可以点击文章左下角
就能查看与之相关的文章。
而且,手机也有这个功能。
废话不多说,先创建好项目。
Maven 中央仓库的链接https://mvnrepository.com/
1、 创建⾃定义拦截器,实现 HandlerInterceptor 接⼝的preHandle(执⾏具体⽅法之前的预处理)⽅法。
先在根目录下 创建一个 config 子包,毕竟拦截类使用的是 @configuration 注解。属于它的一部分。
并且,在里面创建一个拦截类(拦截器)LoginIntercept。因为我们要做的是 登录验证嘛,就取名 Login,后面的 Intercept 是拦截器的意思。
接下来开始实现 拦截器。
此时,我们就实现了一个最简单的 验证用户登录状态的 拦截器(自定义)。
2、将⾃定义拦截器加⼊到框架的配置中,并且设置拦截规则。
你们说难吗?
其实也不难,就是制定拦截规则需要过细一点。
不要把不该拦截的url,忘记写,就行了。
3、验收成果
我们先来给它加一个页面资源。
页面有了,下面我们来创建 controller 层的代码。【负责与前端交互的代码】
下面,我们先在没有登录的情况,访问那些被拦截的资源。
还有一个 index 的 静态页面对吧,我们也来试一下,效果与 访问 index 方法是一样的效果。因为没有登录嘛
再来访问那些 没有被拦截的资源。
我们在这里在拓展一下业务。
在未登录的情况下访问其他访问方法,或者页面,让其跳转到登录页面。
此时,我们先去访问 login方法,登录成功后,再去访问 index 方法
效果就出来了,非常的nice!!!
拦截器实现原理
拦截器实现原理
然⽽有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理,执⾏的流程如下图所示:
实现原理源码分析
所有的 Controller 执⾏都会通过⼀个调度器 DispatcherServlet 来实现,这⼀点可以从 Spring Boot 控制台的打印信息看出,如下图所示:
【启动项目访问某个映射方法之后,再去看 idea 控制台打印内容,你就能看到】
那么,问题来了:为什么执行每一条请求的时候,都会先去调用 DispatcherServlet 呢?
先从字面意思来看: Dispatcher 的 中文意思是 调度器(分发器)。
也就是说:所有的请求进来之后,会先进入调度器,由调度器进行分发。
举个例子:
上学的时候,每个老师都有一个课代表,对不对?
没次发作业本的时候,不是直接由老师发到你手里的。
一般都是把作业全部交给课代表,有课代表分法作业。
原因很简单!
1、方便,不用自己动手
2、老师不止带一个班,所以对每个学生坐的位置不是了解。分发的效率低。
3、课代表也班级中一员。对每个人的位置很熟悉,发作业效率要高一些。
放在程序里,也是一样的理由。
DispatcherServlet 就是 “课代表”,对方每个映射方法都“轻车熟路”,执行的效率极高。⽽所有⽅法都会执⾏ DispatcherServlet 中的 doDispatch 调度⽅法.
另外,为什么Spring 封装的拦截器的 preHandle 方法中会封装了 HttpServletRequest 和HttpServletResponse 呢?
也是因为 它执行目标方法之前,回调用 DispatcherServlet 中 doDispatch 方法。
进行预处理的时候,就会使用 这两参数,
顺水推舟,你要用,我就给你传嘛。
拦截器小结
通过上⾯的源码分析,我们可以看出,Spring 中的拦截器也是通过动态代理和环绕通知的思想实现的,⼤体的调⽤流程如下:
这个时候,原本 Spring AOP 的 切面类(代理对象) 换成了 DispatcherServlet 。
一个是我们自定义的,一个 Spring 框架 自带的。
不过执行原理并没有变。
用户 想要与 目标对象 直接交互,必须要通过 代理对象(拦截器)的验证。
验证通过,才有资格访问目标对象。
扩展:统⼀访问前缀添加
所有请求地址添加 api 前缀:
index 方法也是一样的。
为了不影响后面的操作,我把 添加前缀的代码给注销了
2.统⼀异常处理
对于 统一异常处理,提出一个问题:如果不统一处理异常会是什么效果?
本来安道理来说:是返回一个 JSON 格式的信息,因为前后端都是通过JSON 来交流的。
但是!我 “ 不小心 ” 写出了一个 bug
解决的办法:我们就需要对所有的异常做一个拦截。
有的人可能会说:我可以给那些可能会出现异常的带啊,使用 tryCatch 包裹。但是这些导致代码太长了!阅读性还低!
尤其是涉及到 事务,如果你乱使用 tryCatch 会导致 事务 无法正常进行回滚。
从而出现 预料之外的错误。为了解决这个问题,我们在 Spring 中 使用的是 统一异常处理的方式来解决。
对所有的异常进行一个拦截。
拦截之后,你 “ 想干嘛就干嘛 ”.
你想返回一个什么形式的代码给前端,都可以。统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实现的.
@ControllerAdvice 表示:控制器通知类
@ExceptionHandler 是异常处理器
两个结合表示:
当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件。统⼀异常处理 的实现有两步:
1、给当前统一处理的类上加上 @ControllerAdvice 注解。
@ControllerAdvice 注解,你可以认为该类是 controller 的通知类,或者 说:这是对 Controller 里面方法的增强。
Controller的通知,指的是 controller 里面出现什么问题之后,我能走到这个通知的方法里执行相应的处理。
2、给方法上添加一个 @ExceptionHandler 注解。
ExceptionHandler : Exception(异常) + Handler (处理器 / 管理器)
然后呢, @ExceptionHandler 注解后面 需要带有参数,表明处理异常的类型。
进一步认证一下:我们使用 fiddler 抓个包。
现在放心了吧!
前端在拿到这个响应,知道它是一个算术异常,他就可以对其进行“包装”,然后返回给用户,比如:非法操作,此接口异常,稍后再试、
也就是说:后端一般返回的错误信息,是给 前端程序员去使用的。
前端程序员会根据错误信息,比如:对错误码,进行“包装”,让用户看的懂。
比如 404 未找到对应的资源。
我们来访问一个不存在的资源,以b栈为例。因为它的页面做的确实很好!
这里需要注意的是 没有定义拦截的异常。
是不会被拦截的。
来看下面的例子
此时,我们丢出一个想法:
上面写的异常拦截方法,是具有很强的针对性的,一个方法针只能针对一种错误类型。
那么,存不存在一种 “ 灵活 ” 写法。
简单来说:对于 异常类型,我们不写死。
我们一个方法,就能拦截多种类型的异常,甚至所有的异常,都可以拦截?
当然自定义的异常类型,另说。不过差别也不大。
我们来一个暴力解法,直接 异常的源头 Exception 作为 @ExceptionHandler 参数对象。
这样我们不就可以拦截所有的异常类型了嘛!
3.统⼀数据返回格式
为什么需要统⼀数据返回格式?
统⼀数据返回格式的优点有很多,⽐如以下⼏个:
1、 ⽅便前端程序员更好的接收和解析后端数据接⼝返回的数据。
2、 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就⾏了,因为所有接⼝都是这样返回的。
3、 有利于项⽬统⼀数据的维护和修改。
4、 有利于后端技术部⻔的统⼀规范的标准制定,不会出现稀奇古怪的返回内容。
我们之前是使用一个对象,来作为 返回格式的标准。
但是!还是有那么一点丁麻烦的!
我们还有更简单的做法!
举个例子:
就是开惯手动挡的车,觉得还行。
但是,开过自动挡车,很容易就回不去了。
因为 自动挡,实在是香!
再往后看,N年以后,出现自动驾驶的汽车,我们也就该彻底 “堕落” 了。。。。
程序也是一样的,它也在不断的进化!
之前。我们刚学的时候,比较菜嘛。
老老实实的每个方法都去自己实现一个 HashMap,搞得自己很累,很憔悴。
然后呢,后面能力得到的提升,把 它给封装起来,只需要去new一个对象就行了。不再需要我们去手动实现了。
但是!依然觉得不是那么爽。
学到当前这一块,之前是怎么做的,现在还是这么做的。【“ 大道至简!”】
就比如说:登录的时候,本来就只需要返回 true,或者 false。
那么,我就只返回 true / false,就行了。
我呢,会拦截这个 true 和 false,并将这个 true / false 填充到 data 里面。
再去封装好,让前端能够识别 的 统一的对象,这是不是很爽?
这样做,开发者就不需要关注,到底返回的是一个什么格式的数据给前端。
对于前端来说,都一样。我按到的数据格式是不变的。
中间的处理,就是由统一功能去处理的。
统⼀数据返回格式的实现
统⼀的数据返回格式可以使⽤ @ControllerAdvice + ResponseBodyAdvice 的⽅式实现。
类上加 @ControllerAdvice 。
并且,该类实现 ResponseBodyAdvice 接口。
必须重写 里面的 supports 和 beforeBodyWrite 方法。
supports(支持):当它返回 true,就表示要对 返回的数据,进行 格式的重写。反之,当它返回 false,就不需要重写了。
当需要 到 重写,就会进入 beforeBodyWrite 方法。
beforeBodyWrite 方法的意义:就是将数据返回之前,要不要进行重写 响应数据。
下面来验证一下效果。
抓包的结果,显示 返回的响应是 JSON 类型的。
我们再来试一下,用户名密码 不匹配的情况
非常滴舒服!
我们再来写一个极其简单的伪代码。
等等!我好像还没有演示 supports 方法 返回 false ,是否还会触发重写。
搞起!
总结
经过上述的练习。
我们可以发现 统一功能处理 是存在缺陷的,就是 返回数据的信息描述被写死了。
因为都是统一的格式,所以返回的键值对 的 键值 是无法改变的。
换句话来说:统一带来的问题就是:牺牲了 “ 灵活 ”!
所以,关于统一功能的使用,还是需要根据实际情况来决定!
最后,补充一点:@ControllerAdvice 源码分析 - 了解
大家其实回想一下 第二个功能(异常) 和 第三个功能(返回数据的格式),其实都是事件啊!
我们代码出现一个异常,这就是一个异常事件啊!
事件发生了,我们是可以感知到的。
感知到了之后,就可以根据感知到的事件的处理方法,敲自己的业务代码。
这样的话,如果感知到事件,我们写代码,就能起到作用了。
异常是这样,返回统一的数据格式,也是这样的!
每一次返回一个数据给前端的时候,它也是一个事件(数据传输事件)。
事件发生了,我们的程序是能感知到的。所以才能做统一额处理。
说白了:@ControllerAdvice注解,就是针对项目中发生的事件,做拦截的。通过对 @ControllerAdvice 源码的分析我们可以知道上⾯统⼀异常和统⼀数据返回的执⾏流程,我们先从 @ControllerAdvice 的源码看起,点击 @ControllerAdvice 实现源码如下:
从上述源码可以看出 @ControllerAdvice 派⽣于 @Component 组件,⽽所有组件初始化都会调⽤ InitializingBean 接⼝。
所以接下来我们来看 InitializingBean 有哪些实现类?
在查询的过程中我们发现了,
其中 Spring MVC 中的实现⼦类是 RequestMappingHandlerAdapter,
它⾥⾯有⼀个⽅法 afterPropertiesSet() ⽅法,表示所有的参数设置完成之后执⾏的⽅法。
如下图所示:
⽽这个⽅法中有⼀个 initControllerAdviceCache ⽅法,查询此⽅法的源码如下:
我们发现这个⽅法在执⾏是会查找使⽤所有的 @ControllerAdvice 类,这些类会被容器中,但发⽣某个事件时,调⽤相应的 Advice ⽅法。
⽐如
返回数据前调⽤统⼀数据封装,⽐如发⽣异常是调⽤异常的Advice ⽅法实现。