Spring Boot 拦截器实现:登录验证 & 统一异常处理 & 返回数据规范化



        学习 Spring 和 servlet 初期,我们在判断用户身份时,都是在每个方法中获取会话、获取对象,这种方式冗余度高,增加代码复杂度,维护成本也高,因此想到可以使用 AOP 来实现一个公共的方法,这个公共的方法专门做判断用户身份,但 AOP 的通知方法是整个横切面都会其进行拦截的,所以对于有的不需要判断用户身份的方法而言,这样处理太过暴力,也不合法,除此之外,使用 AOP 也在获取获取参数上也较为困难,因此使用 Spring Boot 拦截器来实现更加合适。

        下面使用拦截器分别实现(1) 统一登录验证、(2)统一异常处理、(3)统一返回数据格式,这三个实战性的功能。


1. 统一登录验证:

第一步:自定义拦截器

        创建一个普通类,实现 HandlerInterceptor 接口,重写 preHandle 方法,由于拦截器非常常用,所以Spring boot 内置了拦截器的依赖;

  1. preHandle方法:在请求到达Controller之前调用。在拦截器链中的每个拦截器的preHandle方法都会被依次调用。如果某个拦截器的preHandle方法返回false,则后续的拦截器的preHandle方法和Controller方法都不会被执行,请求将被拦截下来。

  2. postHandle方法:在Controller方法执行后,DispatcherServlet渲染视图之前调用。在拦截器链中的每个拦截器的postHandle方法都会被依次调用。可以在这个方法中对ModelAndView进行处理或者添加公共的模型数据。

  3. 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,为什么报错信息是反过来的?

这里要明白返回的执行流程:

  1. 原方法返回的原 body 是 String 类型;
  2. 统一数据返回之前:将 String 转换为 hashMap;
  3. 浏览器会借助 StringHttpMessageConverter 将 hashMap 转换为 json 字符串;

问题就出在了第三步,浏览器判断时用原body判断是不是 String 类型,是的话就用  StringHttpMessageConverter 将 hashMap 转换为 json,发现不能转换,所以报错。

解决方案1:

在统一数据重写时,单独处理返回值为 String 类型的情况,重写成返回一个 Json 字符串(可以拼接、可以使用jackson),而非 HashMap;

 使用 Jackson 转成 String:

解决方案2:

        既然问题出在了浏览器的转换器上,那我们就想办法不用这个转换器了;

        可以手动将 StringHttpMessageConverter 转换器去掉:

 这样就好了:


4. 拦截器的实现原理:

 图解:

        所有的 Controller 执行都会通过⼀个调度器 DispatcherServlet 来实现

上图解释了拦截器的实现原理:

  1. 请求到达DispatcherServlet,DispatcherServlet是前端控制器,负责接收请求并进行分发。

  2. DispatcherServlet根据配置的拦截器链,依次调用每个拦截器的方法。

  3. 拦截器链中的每个拦截器都实现了 HandlerInterceptor 接口,拦截器的方法会在请求处理的不同阶段被调用。

  4. 在调用每个拦截器的方法之前和之后,会根据返回值来决定是否继续执行下一个拦截器或者Controller方法。

  5. 当所有拦截器的方法都执行完毕后,DispatcherServlet会进行视图渲染,生成响应结果。

通过拦截器的实现,可以在请求处理的不同阶段进行拦截和处理,实现一些通用的功能,比如权限验证、日志记录、异常处理等。拦截器的实现原理基于Java的反射机制和设计模式,通过动态代理生成代理对象,来实现拦截器的调用。

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@糊糊涂涂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值