SpringBoot 统⼀功能处理

目录

前言

1.⽤户登录权限效验

1.1、最初⽤户登录效验

[1.2、Spring AOP ⽤户统⼀登录验证的问题](#1.2、Spring AOP ⽤户统⼀登录验证的问题)

[1.3、Spring 拦截器](#1.3、Spring 拦截器)

[了解 创建一个 Spring 拦截器 的流程](#了解 创建一个 Spring 拦截器 的流程)

[1、 创建自定义拦截器,实现 HandlerInterceptor 接⼝的preHandle(执⾏具体⽅法之前的预处理)⽅法。](#1、 创建自定义拦截器,实现 HandlerInterceptor 接⼝的preHandle(执⾏具体⽅法之前的预处理)⽅法。)

2、将⾃定义拦截器加⼊到框架的配置中,并且设置拦截规则。

实践:实现一个自定义的拦截器,使其在项目中生效。

[预备工作:创建一个 Spring AOP 的项目。](#预备工作:创建一个 Spring AOP 的项目。)

[1、 创建⾃定义拦截器,实现 HandlerInterceptor 接⼝的preHandle(执⾏具体⽅法之前的预处理)⽅法。](#1、 创建⾃定义拦截器,实现 HandlerInterceptor 接⼝的preHandle(执⾏具体⽅法之前的预处理)⽅法。)

2、将⾃定义拦截器加⼊到框架的配置中,并且设置拦截规则。​编辑​

3、验收成果

拦截器实现原理

实现原理源码分析

拦截器小结

扩展:统⼀访问前缀添加

2.统⼀异常处理

3.统⼀数据返回格式

为什么需要统⼀数据返回格式?

统⼀数据返回格式的实现

总结

[最后,补充一点:@ControllerAdvice 源码分析 - 了解](#最后,补充一点:@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 前置通知或环绕通知来实现,实现模板代码如下:

@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,直接返回一个 错误信息,后面的代码就不用执行了。

下面我们来看个具体的代码

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 拦截器,Spring Boot 项目 本身就支持!

所以,就不用引入 AOP 框架的支持了。

1、 创建⾃定义拦截器,实现 HandlerInterceptor 接⼝的preHandle(执⾏具体⽅法之前的预处理)⽅法。

先在根目录下 创建一个 config 子包,毕竟拦截类使用的是 @configuration 注解。属于它的一部分。

并且,在里面创建一个拦截类(拦截器)LoginIntercept。

因为我们要做的是 登录验证嘛,就取名 Login,后面的 Intercept 是拦截器的意思。

接下来开始实现 拦截器。

此时,我们就实现了一个最简单的 验证用户登录状态的 拦截器(自定义)。

2、将⾃定义拦截器加⼊到框架的配置中,并且设置拦截规则。

你们说难吗?

其实也不难,就是制定拦截规则需要过细一点。

不要把不该拦截的url,忘记写,就行了。

3、验收成果

我们先来给它加一个页面资源。

页面有了,下面我们来创建 controller 层的代码。【负责与前端交互的代码】

下面,我们先在没有登录的情况,访问那些被拦截的资源

还有一个 index 的 静态页面对吧,我们也来试一下,效果与 访问 index 方法是一样的效果。因为没有登录嘛再来访问那些 没有被拦截的资源。

再来访问那些 没有被拦截的资源。

我们在这里在拓展一下业务。

在未登录的情况下访问其他访问方法,或者页面,让其跳转到登录页面。

此时,我们先去访问 login方法,登录成功后,再去访问 index 方法

效果就出来了,非常的nice!!!

拦截器实现原理

拦截器实现原理

然⽽有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理,执⾏的流程如下图所示:

实现原理源码分析

所有的 Controller 执⾏都会通过⼀个调度器 DispatcherServlet 来实现,这⼀点可以从 Spring Boot 控制台的打印信息看出,如下图所示:

那么,问题来了:为什么执行每一条请求的时候,都会先去调用 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 ⽅法实现。

相关推荐
大梦百万秋40 分钟前
Spring Boot实战:构建一个简单的RESTful API
spring boot·后端·restful
忒可君1 小时前
C# winform 报错:类型“System.Int32”的对象无法转换为类型“System.Int16”。
java·开发语言
斌斌_____1 小时前
Spring Boot 配置文件的加载顺序
java·spring boot·后端
路在脚下@1 小时前
Spring如何处理循环依赖
java·后端·spring
一个不秃头的 程序员2 小时前
代码加入SFTP JAVA ---(小白篇3)
java·python·github
丁总学Java2 小时前
--spring.profiles.active=prod
java·spring
苹果醋32 小时前
React系列(八)——React进阶知识点拓展
运维·vue.js·spring boot·nginx·课程设计
上等猿2 小时前
集合stream
java
java1234_小锋2 小时前
MyBatis如何处理延迟加载?
java·开发语言
菠萝咕噜肉i2 小时前
MyBatis是什么?为什么有全自动ORM框架还是MyBatis比较受欢迎?
java·mybatis·框架·半自动