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

目录

1. 用户登录权限校验

1.1 最初用户登录权限效验

1.2 Spring AOP 用户统⼀登录验证

1.3 Spring 拦截器

(1)创建自定义拦截器

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

1.4 练习:登录拦截器

(1)实现 UserController 实体类

(2)返回的登录页面:login.html

(3)实现效果

 1.5 拦截器实现原理

(1)实现原理源码分析

1.6 统一访问前缀添加

(1)在系统的配置文件中设置

 (2)在 application.properies 中配置

2. 统一的异常处理

2.1 异常的统一封装

(1)创建一个类,并在类上标识:@ControllerAdvice

 (2)添加方法 @ExceptionHandler 来订阅异常

3. 统一数据返回格式

3.1 为什么要统一数据返回格式

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

(1)创建一个类,并添加 @ControllerAdvice

(2)实现 ResponseBodyAdvice 接口,并重写 supports 和 beforeBodyAdvice 方法

4. @ControllerAdvice 源码分析

(1) @ControllerAdvice 源码

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

 (3)查询 initControllerAdviceCache 方法


本节主要讲解Spring Boot 统一功能处理,同样也是 AOP 的实战环节,我们希望能够实现以下目标:

  1. 统一用户登陆权限验证
  2. 统一异常处理
  3. 统一数据格式返回

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;}}
}

这种方式写的代码,每个方法中都有相同的用户登录验证权限,缺点是:

  •     每个方法中都要单独写用户登录验证的方法,即使封装成公共方法,也一样要传参调用和在方法中进行判断
  •     添加控制器越多,调用用户登录验证的方法也越多,这样就增加了后期的修改成功和维护成功
  •     这些用户登录验证的方法和现在要实现的业务几乎没有任何关联,但还是要在每个方法中都要写一遍,所以提供一个公共的 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)创建自定义拦截器

//实现 HandlerInterceptor 接口
public class loginInterceptor implements HandlerInterceptor {/*** 返回 true 继续下序流程* false 表示验证失败*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 用户登录业务判断// false 表示当不存在 session 不存在时不需要创造一个会话信息HttpSession session = request.getSession(false);if (session != null && session.getAttribute("userinfo") != null){// 说明用户已经登录return true;}// 可以直接跳转到登录页面 或 返回一个 401、403 没有权限码response.sendRedirect("/login.html");
//        response.setStatus(401);return false;}
}

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

  • addPathPatterns:表示需要拦截的 URL,**表示拦截所有⽅法
  • excludePathPatterns:表示需要排除的 URL
@Configuration // 让随着spring启动而启动
public class AppConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new loginInterceptor()).addPathPatterns("/**")// 拦截所有请求.excludePathPatterns("/user/login")// 不拦截的 url 地址.excludePathPatterns("/user/reg").excludePathPatterns("/**/*.html");}
}

1.4 练习:登录拦截器

实现愿望:

  1. 登录、注册页面不拦截,其余页面都拦截
  2. 等登陆成功写入 session 后,拦截页面可访问

(1)实现 UserController 实体类


@RestController
@RequestMapping("/user")
public class UserController {@RequestMapping("/getUser")public String getuser(){System.out.println("执行了 getUser !");return "get user";}@RequestMapping("/login")public String login(){System.out.println("执行了 login !");return "get login";}@RequestMapping("/reg")public String reg(){System.out.println("执行了 reg !");return "get reg";}
}

(2)返回的登录页面:login.html

<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport"content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>登录页面</title>
</head>
<body>
<h1>登录页面</h1>
</body>
</html>

(3)实现效果

 

 1.5 拦截器实现原理

 

(1)实现原理源码分析

  1. 所有的 Controller 执行都会通过一个调度器 DispatcherServlet 来实现 
  2. 而所有方法都会执行 DispatcherServlet 中的 doDispatch 调度⽅法,doDispatch 源码分析如下:

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

1.6 统一访问前缀添加

方法:

  1. 在系统的配置文件中设置
  2. 在 application.properies 中配置

(1)在系统的配置文件中设置

/*** 所有的接口添加 api 前缀* c 代表所有的请求(Controller)* 表示所有的地址都会加上这个前缀* @param configurer*/
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {configurer.addPathPrefix("api",c -> true);
}

现在我们去查看之前不被拦截的地址

 (2)在 application.properies 中配置

 

2. 统一的异常处理

  1. 给当前的类上加 @ControllerAdvice 表示控制器通知类
  2. 给方法上添加 @ExceptionHandler(xxx.class),表示异常处理器,添加异常返回的业务代码

我们先去制造些异常:

 

2.1 异常的统一封装

(1)创建一个类,并在类上标识:@ControllerAdvice

@ControllerAdvice
public class ExceptionHandler {
}

 (2)添加方法 @ExceptionHandler 来订阅异常

@ControllerAdvice
@ResponseBody// 表示当前的所有方法返回的都是数据不是页面
public class ExHandler {/*** 拦截所有的空指针异常,继续统一的数据返回*/@ExceptionHandler(NullPointerException.class)// 空指针异常public HashMap<String,Object> nullException(NullPointerException e){HashMap<String,Object> result = new HashMap<>();result.put("code","-1");result.put("msg","空指针异常:" + e.getMessage());//错误码的描述信息result.put("date",null);return result;}}

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

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

@ControllerAdvice
@ResponseBody// 表示当前的所有方法返回的都是数据不是页面
public class ExHandler {/*** 拦截所有的空指针异常,继续统一的数据返回*/@ExceptionHandler(NullPointerException.class)// 空指针异常public HashMap<String,Object> nullException(NullPointerException e){HashMap<String,Object> result = new HashMap<>();result.put("code","-1");result.put("msg","空指针异常:" + e.getMessage());//错误码的描述信息result.put("date",null);return result;}@ExceptionHandler(Exception.class)// 所有异常public HashMap<String,Object> AllException(NullPointerException e){HashMap<String,Object> result = new HashMap<>();result.put("code","-1");result.put("msg","异常:" + e.getMessage());//错误码的描述信息result.put("date",null);return result;}
}

 

 

3. 统一数据返回格式

3.1 为什么要统一数据返回格式

  1. 方便前端程序员更好的接收和解析后端数据接口返回的数据。
  2. 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就行了,因为所有接口都是这样返回的
  3. 有利于项目统一数据的维护和修改。
  4. 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容。

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

(1)创建一个类,并添加 @ControllerAdvice

@ControllerAdvice
public class ResponseAdvice {}

(2)实现 ResponseBodyAdvice 接口,并重写 supports 和 beforeBodyAdvice 方法

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {/*** 表示是否需要重写* 返回true则执行beforeBodyWrite方法,反之则不执行*/@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> hashMap = new HashMap<>();hashMap.put("code",200);// 状态码hashMap.put("msg","");// 错误的描述信息hashMap.put("date",body);return hashMap;}
}

supports方法相当于是一个开关,只有当 true 时才能执行重写 beforeBodyWrite 方法,false就不重写

当访问 getUser 时发生异常了,类型访问异常 

注意:

我们知道String既不属于基本数据类型,又不属于对象且在重写方法的时候其余类型都是用的统一的格式化工具,而String用的是它自身的格式化工具,String自身的格式化工具在执行的时候还没有加载好,就会导致 原始类型 是String的时候,在转化成HashMap的时候就会报错

所以在统一返回的时候需要对String进行单独的处理

 

jackson就是用于 json 数据转换的,json的转换工具 

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {HashMap<String,Object> hashMap = new HashMap<>();hashMap.put("code",200);// 状态码hashMap.put("msg","");// 错误的描述信息hashMap.put("date",body);if (body instanceof String){// 判断数据类型是不是 String,是String需要特殊处理,因为 String 在转换的时候会报错try {return objectMapper.writeValueAsString(hashMap);} catch (JsonProcessingException e) {e.printStackTrace();}}return hashMap;
}

4. @ControllerAdvice 源码分析

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

(1) @ControllerAdvice 源码

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

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

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

 (3)查询 initControllerAdviceCache 方法

发现这个方法在执行时会查找使用所有的 @ControllerAdvice 类,发送某个事件时,调用相应的 Advice 方法,比如返回数据前调用统一数据封装,比如发生异常是调用异常的 Advice 方法实现的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/87775.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

for macOS-21.1.0.3267中文直装版功能介绍及系统配置要求

FL Studio 21简称FL水果软件,全称是&#xff1a;Fruity Loops Studio编曲&#xff0c;由于其Logo长的比较像一款水果因此&#xff0c;在大家更多的是喜欢称他为水果萝卜&#xff0c;FL studio21是目前最新的版本&#xff0c;这是一款可以让你的计算机就像是一个全功能的录音室&…

最强自动化测试框架Playwright(10)- 截图

截图 捕获屏幕截图并将其保存到文件中&#xff1a; page.screenshot(path"screenshot.png")可将页面截图保存为screen.png import osfrom playwright.sync_api import Playwright, expect, sync_playwrightdef run(playwright: Playwright) -> None:browser p…

数学建模(二)线性规划

课程推荐&#xff1a;6 线性规划模型基本原理与编程实现_哔哩哔哩_bilibili 在人们的生产实践中&#xff0c;经常会遇到如何利用现有资源来安排生产&#xff0c;以取得最大经济效益的问题。此类问题构成了运筹学的一个重要分支&#xff1a;数学规划。而线性规划(Linear Program…

android Ndk Jni动态注册方式以及静态注册

目录 一.静态注册方式 二.动态注册方式 三.源代码 一.静态注册方式 1.项目名\app\src\main下新建一个jni目录 2.在jni目录下,再新建一个Android.mk文件 写入以下配置 LOCAL_PATH := $(call my-dir)//获取当前Android.mk所在目录 inclu

【Redis】Spring/SpringBoot 操作 Redis Java客户端

目录 操作 Redis Java客户端SpringBoot 操作Redis 步骤 操作 Redis Java客户端 1.Jedis 2.Lettuce(主流) <-Spring Data Redis SpringBoot 操作Redis 步骤 1.添加Redis 驱动依赖 2.设置Redis 连接信息 spring.redis.database0 spring.redis.port6379 spring.redis.host…

【Linux操作系统】深入理解系统调用中的read和write函数

在操作系统中&#xff0c;系统调用是用户程序与操作系统之间进行交互的重要方式。其中&#xff0c;read和write函数是常用的系统调用函数&#xff0c;用于在用户程序和操作系统之间进行数据的读取和写入。本文将深入介绍read和write函数的工作原理、用法以及示例代码&#xff0…

springboot异步任务

在Service类声明一个注解Async作为异步方法的标识 package com.qf.sping09test.service;import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service;Service public class AsyncService {//告诉spring这是一个异步的方法Asyncp…

使用gpt对对话数据进行扩增,对话数据扩增,数据增强

我们知道一个问题可以使用很多方式问&#xff0c;但都可以使用完全一样的回答&#xff0c;基于这个思路&#xff0c;我们可以很快的扩增我们的数据集。思路就是使用chatgpt或者gpt4生成类似问题&#xff0c;如下&#xff1a; 然后我们可以工程化这个过程&#xff0c;从而快速扩…

【Github】SourceTree技巧汇总

sourceTree登录github账户 会跳转到浏览器端 按照Git Flow 初始化仓库分支 克隆远程仓库到本地 推送变更到远程仓库 合并分支 可以看到目前的本地分支&#xff08;main、iOS_JS&#xff09;和远程分支&#xff08;origin/main、origin/HEAD、origin/iOS_JS&#xff09;目前所处…

【问题记录】antd icons报rev属性缺失错误

闲来无事将项目中的antd从v4升级到了v5&#xff0c;之前正常的页面中如有图标&#xff0c;如<PlusOutlined />&#xff0c;总是报以下错误&#xff1a; TS2741: Property rev is missing in type {} but required in type Pick<AntdIconProps, "name" …

如何实现Vue路由的二级菜单

目录 Vue路由&#xff08;一、二级路由&#xff09; 一级路由配置 二级路由配置 Vue中展示二级路由的默认模块/二级路由默认显示 Vue路由&#xff0c;二级路由及跳转 如何用vue实现二级菜单栏 ◼️ 相关参考资料 当朋友们看到这个文章时想必是想要了解vue路由二级菜单相…

React UI组件库

1 流行的开源React UI组件库 1 material-ui(国外) 官网: Material UI: React components based on Material Design github: GitHub - mui/material-ui: MUI Core: Ready-to-use foundational React components, free forever. It includes Material UI, which implements Go…

人大金仓助力某大型金融机构业务系统异地容灾优化升级

日前&#xff0c;人大金仓助力某大型金融机构应收账款融资服务平台异地容灾项目顺利上线&#xff0c;保证了平台系统运行的连续性和数据安全&#xff0c;为充分发挥平台的融资功能&#xff0c;缓解中小微企业融资难提供了强有力的保障。 “ 缓解中小微企业融资难 某大型金融机构…

Stephen Wolfram:意义空间和语义运动规律

Meaning Space and Semantic Laws of Motion 意义空间和语义运动规律 We discussed above that inside ChatGPT any piece of text is effectively represented by an array of numbers that we can think of as coordinates of a point in some kind of “linguistic feature …

【第二阶段】在函数中定义参数是函数的函数

1.理解&#xff1a;在一个函数中有一个参数a,这个参数a又属于一个函数&#xff0c;a即时参数又是函数 2.用kotlin实现登录 /*** You can edit, run, and share this code.* play.kotlinlang.org*/ fun main() {//调用传参//普通参数传入即可,针对在调用函数中的参数函数传入使用…

Android Studio实现Spinner下拉列表

效果图 点击下拉列表 点击某一个下拉列表 MainActivity package com.example.spinneradapterpro;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.Spinn…

关于使用pycharm遇到只能使用unittest方式运行,无法直接选择Run

相信大家可能都遇到过这个问题&#xff0c;使用pycharm直接运行脚本的时候&#xff0c;只能选择unittest的方式&#xff0c;能愁死个人 经过几次各种尝试无果之后&#xff0c;博主就放弃死磕了&#xff0c;原谅博主是个菜鸟 后来遇到这样的问题&#xff0c;往往也就直接使用cm…

差分升级在物联网水表上的实现与应用(学习)

摘要 当越来越多的物联网水表加入抄表系统后&#xff0c;实现了水表数据的信息化&#xff0c;并且当水表终端需要技术更新时&#xff0c;通过网络方式来升级产品可以高效修复设备面临的问题&#xff0c;减少用户损失&#xff0c;降低维护成本&#xff0c;但同时也对有限的网络…

HCIP VRRP技术

一、VRRP概述 VRRP&#xff08;Virtual Router Pedundancy Protocol&#xff09;虚拟路由器冗余协议&#xff0c;既能够实现网关的备份&#xff0c;又能够解决多个网关之间互相冲突的问题&#xff0c;从而提高网络可靠性。 局域网中的用户的终端通常采用配置一个默认网关的形…

Leetcode-每日一题【剑指 Offer 12. 矩阵中的路径】

题目 单词必须按照字母顺序&#xff0c;通过相邻的单元格内的字母构成&#xff0c;其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。 例如&#xff0c;在下面的 34 的矩阵中包含单词 "ABCCED"&#xff08;单词中的字母…