Spring Boot 统一功能处理(用户登录权限效验-拦截器、异常处理、数据格式返回)

news/2025/2/4 15:42:21/

文章目录

  • 1. 统一用户登录权限效验
    • 1.1 最初用户登录权限效验
    • 1.2 Spring AOP 统一用户登录验证
    • 1.3 Spring 拦截器
    • 1.4 练习:登录拦截器
    • 1.5 拦截器实现原理
    • 1.6 统一访问前缀添加
  • 2. 统一异常处理
  • 3. 统一数据格式返回
    • 3.1 统一数据格式返回的实现
    • 3.2 @ControllerAdvice 源码分析

本篇将要学习 Spring Boot 统一功能处理模块,这也是 AOP 的实战环节

  • 统一用户登录权限的效验实现接口 HandlerInterceptor + WebMvcConfigurer
  • 统一异常处理使用注解 @RestControllerAdvice + @ExceptionHandler
  • 统一数据格式返回使用注解 @ControllerAdvice 并且实现接口 @ResponseBodyAdvice

1. 统一用户登录权限效验

用户登录权限的发展完善过程

  1. 最初用户登录效验:在每个方法中获取 Session 和 Session 中的用户信息,如果存在用户,那么就认为登录成功了,否则就登录失败了
  2. 第二版用户登录效验:提供统一的方法,在每个需要验证的方法中调用统一的用户登录身份效验方法来判断
  3. 第三版用户登录效验:使用 Spring AOP 来统一进行用户登录效验
  4. 第四版用户登录效验:使用 Spring 拦截器来实现用户的统一登录验证

1.1 最初用户登录权限效验

@RestController
@RequestMapping("/user")
public class UserController {@RequestMapping("/a1")public Boolean login (HttpServletRequest request) {// 有 Session 就获取,没有就不创建HttpSession session = request.getSession(false);if (session != null && session.getAttribute("userinfo") != null) {// 说明已经登录,进行业务处理return true;} else {// 未登录return false;}}@RequestMapping("/a2")public Boolean login2 (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.springaop.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 e) {e.printStackTrace();}System.out.println("Around 方法结束执行");return obj;}
}

但如果只在以上代码 Spring AOP 的切面中实现用户登录权限效验的功能,有这样两个问题:

  1. 没有办法得到 HttpSession 和 Request 对象
  2. 我们要对一部分方法进行拦截,而另一部分方法不拦截,比如注册方法和登录方法是不拦截的,也就是实际的拦截规则很复杂,使用简单的 aspectJ 表达式无法满足拦截的需求

1.3 Spring 拦截器

针对上面代码 Spring AOP 的问题,Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现有两步:

  1. 创建自定义拦截器,实现 Spring 中的 HandlerInterceptor 接口中的 preHandle方法

  2. 将自定义拦截器加入到框架的配置中,并且设置拦截规则

    1) 给当前的类添加 @Configuration 注解

    2)实现 WebMvcConfigurer 接口

    3)重写 addInterceptors 方法

注意:一个项目中可以同时配置多个拦截器

(1)创建自定义拦截器

/*** @Description: 自定义用户登录的拦截器* @Date 2023/2/13 13:06*/
@Component
public class LoginIntercept implements HandlerInterceptor {// 返回 true 表示拦截判断通过,可以访问后面的接口// 返回 false 表示拦截未通过,直接返回结果给前端@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception {// 1.得到 HttpSession 对象HttpSession session = request.getSession(false);if (session != null && session.getAttribute("userinfo") != null) {// 表示已经登录return true;}// 执行到此代码表示未登录,未登录就跳转到登录页面response.sendRedirect("/login.html");return false;}
}

(2)将自定义拦截器添加到系统配置中,并设置拦截的规则

  • addPathPatterns:表示需要拦截的 URL,**表示拦截所有⽅法
  • excludePathPatterns:表示需要排除的 URL

说明:拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件、JS 和 CSS 等⽂件)。

/*** @Description: 将自定义拦截器添加到系统配置中,并设置拦截的规则* @Date 2023/2/13 13:13*/
@Configuration
public class AppConfig implements WebMvcConfigurer {@Resourceprivate LoginIntercept loginIntercept;@Overridepublic void addInterceptors(InterceptorRegistry registry) {
//        registry.addInterceptor(new LoginIntercept());//可以直接new 也可以属性注入registry.addInterceptor(loginIntercept).addPathPatterns("/**").    // 拦截所有 urlexcludePathPatterns("/user/login"). //不拦截登录注册接口excludePathPatterns("/user/reg").excludePathPatterns("/login.html").excludePathPatterns("/reg.html").excludePathPatterns("/**/*.js").excludePathPatterns("/**/*.css").excludePathPatterns("/**/*.png").excludePathPatterns("/**/*.jpg");}
}

1.4 练习:登录拦截器

要求

  1. 登录、注册页面不拦截,其他页面都拦截
  2. 当登录成功写入 session 之后,拦截的页面可正常访问

在 1.3 中已经创建了自定义拦截器 和 将自定义拦截器添加到系统配置中,并设置拦截的规则

(1)下面创建登录和首页的 html

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q9Gqxzj7-1676278749702)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676269833666.png)]

(2)创建 controller 包,在包中创建 UserController,写登录页面和首页的业务代码

@RestController
@RequestMapping("/user")
public class UserController {@RequestMapping("/login")public boolean login(HttpServletRequest request,String username, String password) {boolean result = false;if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {if(username.equals("admin") && password.equals("admin")) {HttpSession session = request.getSession();session.setAttribute("userinfo","userinfo");return true;}}return result;}@RequestMapping("/index")public String index() {return "Hello Index";}
}

(3)运行程序,访问页面,对比登录前和登录后的效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jRWMgoSw-1676278749704)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676268463235.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GHnJ7FuR-1676278749704)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676269389910.png)]


1.5 拦截器实现原理

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cuSCpU20-1676278749705)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676270248519.png)]

实现原理源码分析

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ektZVF7w-1676278749705)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676270414355.png)]

  1. 而所有方法都会执行 DispatcherServlet 中的 doDispatch 调度⽅法,doDispatch 源码分析如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TlRlduCp-1676278749706)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676271200316.png)]

通过源码分析,可以看出,Sping 中的拦截器也是通过动态代理和环绕通知的思想实现的


1.6 统一访问前缀添加

所有请求地址添加 api 前缀,c 表示所有

@Configuration
public class AppConfig implements WebMvcConfigurer {// 所有的接口添加 api 前缀@Overridepublic void configurePathMatch(PathMatchConfigurer configurer) {configurer.addPathPrefix("api", c -> true);}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UGNUF3ox-1676278749706)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676271883098.png)]


2. 统一异常处理

  1. 给当前的类上加 @ControllerAdvice 表示控制器通知类
  2. 给方法上添加 @ExceptionHandler(xxx.class),表示异常处理器,添加异常返回的业务代码
@RestController
@RequestMapping("/user")
public class UserController {@RequestMapping("/index")public String index() {int num = 10/0;return "Hello Index";}
}

在 config 包中,创建 MyExceptionAdvice 类

@RestControllerAdvice // 当前是针对 Controller 的通知类(增强类)
public class MyExceptionAdvice {@ExceptionHandler(ArithmeticException.class)public HashMap<String,Object> arithmeticExceptionAdvice(ArithmeticException e) {HashMap<String, Object> result = new HashMap<>();result.put("state",-1);result.put("data",null);result.put("msg" , "算出异常:"+ e.getMessage());return result;}
}

也可以这样写,效果是一样的

@ControllerAdvice
public class MyExceptionAdvice {@ExceptionHandler(ArithmeticException.class)@ResponseBodypublic HashMap<String,Object> arithmeticExceptionAdvice(ArithmeticException e) {HashMap<String, Object> result = new HashMap<>();result.put("state",-1);result.put("data",null);result.put("msg" , "算数异常:"+ e.getMessage());return result;}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pOxAmSTF-1676278749707)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676273087133.png)]

如果再有一个空指针异常,那么上面的代码是不行的,还要写一个针对空指针异常处理器

@ExceptionHandler(NullPointerException.class)
public HashMap<String,Object> nullPointerExceptionAdvice(NullPointerException e) {HashMap<String, Object> result = new HashMap<>();result.put("state",-1);result.put("data",null);result.put("msg" , "空指针异常异常:"+ e.getMessage());return result;
}
    @RequestMapping("/index")public String index(HttpServletRequest request,String username, String password) {Object obj = null;System.out.println(obj.hashCode());return "Hello Index";}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1bjJWAtS-1676278749707)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676273765910.png)]

但是需要考虑的一点是,如果每个异常都这样写,那么工作量是非常大的,并且还有自定义异常,所以上面这样写肯定是不好的,既然是异常直接写 Exception 就好了,它是所有异常的父类,如果遇到不是前面写的两种异常,那么就会直接匹配到 Exception

当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配

@ExceptionHandler(Exception.class)
public HashMap<String,Object> exceptionAdvice(Exception e) {HashMap<String, Object> result = new HashMap<>();result.put("state",-1);result.put("data",null);result.put("msg" , "异常:"+ e.getMessage());return result;
}

可以看到优先匹配的还是前面写的 空指针异常

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-awVsPmiQ-1676278749708)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676274382111.png)]


3. 统一数据格式返回

3.1 统一数据格式返回的实现

  1. 给当前类添加 @ControllerAdvice

  2. 实现 ResponseBodyAdvice 重写其方法

    supports 方法,此方法表示内容是否需要重写(通过此⽅法可以选择性部分控制器和方法进行重写),如果要重写返回 true

    beforeBodyWrite 方法,方法返回之前调用此方法

@ControllerAdvice
public class MyResponseAdvice implements ResponseBodyAdvice {// 返回一个 boolean 值,true 表示返回数据之前对数据进行重写,也就是会进入 beforeBodyWrite 方法// 返回 false 表示对结果不进行任何处理,直接返回@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}// 方法返回之前调用此方法@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {HashMap<String,Object> result = new HashMap<>();result.put("state",1);result.put("data",body);result.put("msg","");return result;}
}
@RestController
@RequestMapping("/user")
public class UserController {@RequestMapping("/login")public boolean login(HttpServletRequest request,String username, String password) {boolean result = false;if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {if(username.equals("admin") && password.equals("admin")) {HttpSession session = request.getSession();session.setAttribute("userinfo","userinfo");return true;}}return result;}@RequestMapping("/reg")public int reg() {return 1;}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U6hvLVd8-1676278749708)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676275787822.png)]


3.2 @ControllerAdvice 源码分析

通过对 @ControllerAdvice 源码的分析我们可以知道上面统一异常和统一数据返回的执行流程

(1)先看 @ControllerAdvice 源码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tHA3bOkT-1676278749708)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676277580298.png)]

可以看到 @ControllerAdvice 派生于 @Component 组件而所有组件初始化都会调用 InitializingBean 接口

(2)下面查看 initializingBean 有哪些实现类

在查询过程中发现,其中 Spring MVC 中的实现子类是 RequestMappingHandlerAdapter,它里面有一个方法 afterPropertiesSet()方法,表示所有的参数设置完成之后执行的方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1GfmMAY6-1676278749709)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676277786403.png)]

(3)而这个方法中有一个 initControllerAdviceCache 方法,查询此方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y39ZLEbu-1676278749709)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676277853411.png)]
发现这个方法在执行时会查找使用所有的 @ControllerAdvice 类,发送某个事件时,调用相应的 Advice 方法,比如返回数据前调用统一数据封装,比如发生异常是调用异常的 Advice 方法实现的


http://www.ppmy.cn/news/28557.html

相关文章

oracle存储过程的使用

文章目录oracle存储过程的使用基本结构管理存储过程调用存储过程的方法存储过程参数关键词&#xff1a; IN 和outin/out测试案例调用in/out测试案例存储过程语法DECLARE声明关键词赋值使用in/out将值作为子程序的参数分配给变量&#xff0c;看上面的案例为布尔变量赋值表达式串…

【设计模式】策略模式在Java工程中应用

在之前的文章中&#xff0c;曾经给大家介绍过策略模式&#xff1a;【设计模式】策略模式&#xff0c;在该篇文章中&#xff0c;我们曾很清楚的说到&#xff0c;策略模式主要解决的问题是&#xff1a;在有多种算法相似的情况下&#xff0c;解决使用 if...else 所带来的复杂和难以…

C语言青蛙跳台阶【图文详解】

青蛙跳台阶前言1. 题目介绍2. 解题思路3. 利用图片来演示青蛙跳台阶的原理4. 如何用C语言实现青蛙跳台阶前言 在本文&#xff0c;我们要与一只活泼可爱的小青蛙合作&#xff0c;带领着它跳上台阶&#xff0c;这个小家伙精力充沛&#xff0c;特别擅长于跳跃。我们要让它做我们的…

Python 简单可变、复杂可变、简单不可变、复杂不可变类型的copy、deepcopy的行为

copy模块&#xff1a;copy&#xff1a;浅拷贝deepcopy&#xff1a;深拷贝简单可变类型、复杂可变的copy()、deepcopy()&#xff1a;简单不可变、复杂不可变类型的copy()、deepcopy()&#xff1a;结论&#xff1a;对于简单类型的可变类型copy是深拷贝&#xff0c;改变了该拷贝变…

Spring Cloud Sentinel实战(一)- Sentinel介绍

Sentinel介绍 什么是Sentinel 分布式系统的流量防卫兵&#xff1a;随着微服务的普及&#xff0c;服务调用的稳定性变得越来越重要。Sentinel以“流量”为切入点&#xff0c;在流量控制、断路、负载保护等多个领域开展工作&#xff0c;保障服务可靠性。 特点&#xff1a; 1. 2…

QT之图形视图框架概述——Graphics View Framework

QT之图形视图框架概述——Graphics View Framework1. 概述2. 核心类3. 事件传递4. Graphics View 坐标系统5. 参考1. 概述 Graphics View Framework是子Qt 4.2引入的&#xff0c;用来取代之前版本中的QCanvas。Graphics View Framework提拱了用于大量2D图形项的管理和交互的能…

【C语言】寻找隐藏字母游戏

编程实现一个游戏程序&#xff0c;会将连续三个字母中的一个隐去&#xff0c;由玩家填写隐去的那个字母&#xff0c;如屏幕上显示A ? C&#xff0c;则玩家需要输入B&#xff1b;屏幕上显示&#xff1f;B C&#xff0c;则玩家需要输入A。记录玩家完成20次游戏的时间以及正确率。…

RNN相关知识总结

目录RNN结构与原理1.模型总览2.反向传播LSTM结构与原理1.模型总览2.如何解决RNN梯度消失/爆炸问题&#xff1f;GRU结构及原理1.模型总览LSTM与GRU的区别RNN结构与原理 1.模型总览 上图是RNN的展开结构图&#xff0c;由输入层、隐藏层和输出层组成。当前时间步t 的隐藏状态hth_…