【Spring Boot】拦截器与统一功能处理

  • 博主简介:想进大厂的打工人
  • 博主主页:@xyk:
  • 所属专栏: JavaEE进阶 

上一篇文章我们讲解了Spring AOP是一个基于面向切面编程的框架,用于将某方面具体问题集中处理,通过代理对象来进行传递,但使用原生Spring AOP实现统一的拦截是非常繁琐的。而在本节,我们将使用一种简单的方式进行统一功能处理,具体如下:

  • 统一用户登录权限验证
  • 统一数据格式返回
  • 统一异常处理

目录

文章目录

0、我们先来回顾⼀下最初⽤户登录验证的实现方法:

一、统一用户登录权限验证

1.1 使用原生Spring AOP 实现统一拦截

1.2 使用Spring 拦截器实现统一用户登录验证

1.3 拦截器实现原理

1.4 实现原理源码分析

二、统一异常处理

三、统一数据返回格式

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

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

结语


0、我们先来回顾⼀下最初用户登录验证的实现方法:

@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. 这些⽤户登录验证的⽅法和接下来要实现的业务⼏何没有任何关联,但每个⽅法中都要写⼀遍。

一、统一用户登录权限验证

1.1 使用原生Spring AOP 实现统一拦截

我们上一篇文章讲解了原生AOP:【Spring】Spring AOP 初识及实现原理解析

以使用原生 Spring AOP 来实现⽤户统⼀登录验证为例, 主要是使用前置通知和环绕通知实现的,具体实现如下:

package com.example.demo.aop;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Component // 随着框架的启动而启动
@Aspect // 告诉框架我是一个切面类
public class UserAspect {// 定义切点(配置拦截规则)@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")public void pointcut(){}@Before("pointcut()")public void beforeAdvice(){System.out.println("执行了前置通知~");}@After("pointcut()")public void AfterAdvice(){System.out.println("执行了后置通知~");}/*** 环绕通知* @param joinPoint* @return*/@Around("pointcut()")public Object aroundAdvice(ProceedingJoinPoint joinPoint){System.out.println("进入了环绕通知~");Object obj = null;try {obj = joinPoint.proceed();} catch (Throwable e) {e.printStackTrace();}System.out.println("退出了环绕通知~");return obj;}}

从上面代码可以看出,使用原生的 Spring AOP 实现统一拦截的难点比较复杂,不易实现。定义拦截规则非常困难。如注册⽅法和登录⽅法是不拦截的,这样的话排除⽅法的规则很难定义,甚⾄没办法定义。而且在切面类中拿到 HttpSession 比较难。

为了解决Spring AOP的问题,Spring 提供了拦截器~

1.2 使用Spring 拦截器实现统一用户登录验证

要使用Spring 拦截器,需要先创建一个实现了 HandlerInterceptor 接口的拦截器类,该接口定义了三个方法:preHandle、postHandle和afterCompletion。

  1. preHandle方法在请求到达控制器之前执行,可以用于进行身份验证、参数校验等;
  2. postHandle方法在控制器处理完请求后执行,可以对模型和视图进行操作;
  3. afterCompletion方法在视图渲染完成后执行,用于清理资源或记录日志。

拦截器的实现分为以下两个步骤:

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

2. 将⾃定义拦截器加入 WebMvcConfigurer 的 addInterceptors 方法中

具体实现如下:

1.先创建自定义拦截器

package com.example.demo.config;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 {/*** 此方法返回一个 boolean,如果为 true 表示验证成功,可以继续执行后续流程* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 用户登录业务判断HttpSession session = request.getSession(false);if (session != null && session.getAttribute("userinfo") != null){// 说明用户已经登录return true;}// 可以跳转到登录页面 or 返回一个 401 / 403 没有权限码response.sendRedirect("/login.html");
//        response.setStatus(403);return false;}
}

2.配置拦截器并设置拦截规则:

package com.example.demo.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
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.3 拦截器实现原理

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

1.4 实现原理源码分析

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

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

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {try {ModelAndView mv = null;Object dispatchException = null;try {processedRequest = this.checkMultipart(request);multipartRequestParsed = processedRequest != request;mappedHandler = this.getHandler(processedRequest);if (mappedHandler == null) {this.noHandlerFound(processedRequest, response);return;}HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());String method = request.getMethod();boolean isGet = HttpMethod.GET.matches(method);if (isGet || HttpMethod.HEAD.matches(method)) {long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {return;}}// 调⽤预处理【重点】if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}// 执⾏ Controller 中的业务mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}this.applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);} catch (Exception var20) {dispatchException = var20;} catch (Throwable var21) {dispatchException = new NestedServletException("Handler dispatch failed", var21);}this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);} catch (Exception var22) {this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);} catch (Throwable var23) {this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));}} finally {if (asyncManager.isConcurrentHandlingStarted()) {if (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}} else if (multipartRequestParsed) {this.cleanupMultipart(processedRequest);}}}

从上述源码可以看出在开始执⾏ Controller 之前,会先调⽤ 预处理⽅法 applyPreHandle,⽽
applyPreHandle ⽅法的实现源码如下:

    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);if (!interceptor.preHandle(request, response, this.handler)) {this.triggerAfterCompletion(request, response, (Exception)null);return false;}}return true;}

从上述源码可以看出,在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor 并执⾏拦截器中的 preHandle ⽅法,这样就会我们前⾯定义的拦截器对应上了,如下图所示:

 此时,相应的preHandle中的业务逻辑就会执行。

二、统一异常处理

统一异常处理是指在应用程序中定义一个公共的异常处理机制,用来处理所有的异常情况。

本文讲述的统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的:

  • @ControllerAdvice 表示控制器通知类。
  • @ExceptionHandler 异常处理器。

以上两个注解组合使用,表示当出现异常的时候执行某个通知,即执行某个方法事件,具体实现代码如下:

package com.example.demo.config;import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;import java.util.HashMap;/*** @author xyk的电脑* @version 1.0* @description: TODO* @date 2023/7/15 18:49*/@ControllerAdvice
@ResponseBody
public class MyExHandler {/*** 拦截所有的空指针异常,进行统一的数据返回了* @param e* @return*/@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("data",null);return result;}@ExceptionHandler(Exception.class)public HashMap<String,Object> exception(Exception e){HashMap<String,Object> result = new HashMap<>();result.put("code",-1);result.put("msg","异常:" + e.getMessage()); // 错误码的描述信息result.put("data",null);return result;}}

上述代码中,实现了对所有空指针异常和所有异常的保底措施的拦截并进行统一的数据返回。

三、统一数据返回格式

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

统⼀数据返回格式的优点有很多,⽐如以下几个:
1. ⽅便前端程序员更好的接收和解析后端数据接口返回的数据。
2. 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就⾏了,因为所有接⼝都是这样返回的。
3. 有利于项⽬统⼀数据的维护和修改。
4. 有利于后端技术部⻔的统⼀规范的标准制定,不会出现稀奇古怪的返回内容

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

统⼀的数据返回格式可以使⽤ @ControllerAdvice + ResponseBodyAdvice 的⽅式实现,具体实现代码如下:

1.创建一个类,并添加@ControllerAdvice注解

2.实现ResponseBodyAdvice接口,并重写 supports 和 beforeBodyWrite方法

package com.example.demo.config;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;import java.util.HashMap;/*** @author xyk的电脑* @version 1.0* @description: TODO* @date 2023/7/15 19:18*/@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Autowiredprivate ObjectMapper objectMapper;/*** 此方法返回 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> result = new HashMap<>();result.put("code",200);result.put("msg","");result.put("data",body);if (body instanceof String){// 需要特殊处理,因为 String 在转换的时候会报错try {return objectMapper.writeValueAsString(result);} catch (JsonProcessingException e) {e.printStackTrace();}}return result;}
}

注意,如果返回的 body 原始数据类型是 String ,则会出现类型转化异常,因为当我们返回给前端Spring引用类型时,Spring的渲染引擎还没有加载好就已经返回给前端了,会出现ClassCastException因此,如果原始返回数据类型为 String ,则需要使用 jackson 进行单独处理。


结语

创作不易,如果你有任何问题,欢迎评论区讨论哦~~ 

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

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

相关文章

平板选择什么电容笔比较好?ipad手写笔推荐品牌

在现在的生活上&#xff0c;有了iPad平板&#xff0c;一切都变得简单了许多&#xff0c;也让我们的学习以及工作都更加的便利。这其中&#xff0c;电容笔就起到了很大的作用&#xff0c;很多人都不知道&#xff0c;到底要买什么牌子的电容笔&#xff1f;哪些电容笔的性价比比较…

【C语言】11-三大结构之循环结构-2

1. 改变循环的状态 1.1 break 语句 break 语句在之前 switch 语句的学习中已经使用过了,它是用来退出 switch 语句的。在介绍控制语句时也提到了 break 语句可以和 switch 语句搭配使用以及退出循环,在这里我们将会正式学习使用 break 语句退出循环的用法 在之前的例子中,…

myql驱动升级flowable项目启动报错

最近系统升级需要将mysql的驱动升到8.0.27&#xff0c;升级后启动项目发现涉及到工作流的项目报错。Caused by: java.lang.ClassCastException: java.time.LocalDateTime cannot be cast to java.lang.String。报错信息是类型转换异常&#xff0c;我们找到报错的代码。这是因为…

CentOS7 启动谷歌浏览器 java+Selenium+chrome+chromedriver

前言&#xff1a;自己想使用该技术实现自动化抓取音乐&#xff0c;目前在window上运行成功&#xff0c;需要在Linux Centos服务上跑&#xff0c;配置上出现了许多问题&#xff0c;特此记录。 参考文档&#xff1a;CentOS7 安装Seleniumchromechromedriverjava_远方丿的博客-CSD…

后端开发7.轮播图模块【mongdb开发】

概述 轮播图模块数据库采用mongdb开发 效果图 数据库设计 创建数据库 use sc; 添加数据 db.banner.insertMany([ {bannerId:"1",bannerName:"商城轮播图1",bannerUrl:"http://xx:8020/img/轮播图/shop1.png"}, {bannerId:"2"…

篇十一:享元模式:共享细粒度对象

篇十一&#xff1a;“享元模式&#xff1a;共享细粒度对象” 设计模式是软件开发中的重要工具&#xff0c;享元模式&#xff08;Flyweight Pattern&#xff09;是结构型设计模式的一种。享元模式旨在通过共享细粒度的对象&#xff0c;减少内存消耗和提高性能。在设计模式学习中…

深入学习 Redis - 主从结构配置、流程、底层原理(全网最详细)

目录 前言 一、主从模式 1.1、概述 1.2、配置 redis 主从结构 1.2.1、复制配置文件&#xff0c;修改 1.2.2、配置主从结构 1.2.3、启动 redis 服务 1.2.4、查看复制状态 1.3、slaveof 命令 1.3.1、断开主从复制关系 1.3.2、切换主从复制关系 1.3.3、只读 1.3.4、网…

C语言——自定义类型详解[结构体][枚举][联合体]

自定义类型详解 前言&#xff1a;一、结构体1.1结构体的声明1.2结构体内存对齐1.3位段&#xff08;位域&#xff09; 二、枚举2.1枚举类型的定义2.2枚举类型的优点2.3枚举的使用 三、联合体3.1联合体类型的定义3.2联合体的特点3.3联合体大小的计算 前言&#xff1a; 我打算把结…

solr迁移到另一个solr中(docker单机)

背景介绍 solr数据迁移&#xff0c;或者版本升级&#xff0c;需要用到迁移&#xff0c;此处记录一下迁移方法以及过程中遇到的问题。我这边使用的是docker环境&#xff0c;非docker部署的应该也是一样的。 solr部署教程 准备工作 ● solrA 版本&#xff1a; 8.11.2 (已有so…

Kafka基本概念

文章目录 概要整体架构broker和集群ProducerConsumer和消费者组小结 概要 Kafka是最初由Linkedin公司开发&#xff0c;是一个分布式、分区的、多副本的、多生产者、多订阅者&#xff0c;基于 zookeeper协调的分布式日志系统&#xff08;也可以当做MQ系统&#xff09;&#xff…

怎么建立大型语言模型

建立大型语言模型通常涉及以下主要步骤&#xff1a; 数据收集&#xff1a;收集大规模的文本数据作为模型的训练数据。可以从各种来源获取数据&#xff0c;如互联网、书籍、新闻文章等。数据的质量和多样性对于模型的性能至关重要。 数据预处理&#xff1a;对收集到的数据进行预…

【周赛第70期】4题(2题未测试) 启发式合并 哈希表 最近公共祖先 堆 数学

目录 ~~本次比赛前两题似乎没有数据&#xff0c;所以代码可能有隐藏的错误~~TODO 如果用时间的话&#xff0c;准备自己造一些数据测一下。 1、题目名称&#xff1a;小张的手速大比拼题目答案启发式合并另一种思路&#xff08;非正解&#xff0c;未经充分测试&#xff09; 2、题…

重构内置类Function原型上的call方法

重构内置类Function原型上的call方法 // > 重构内置类Function原型上的call方法 ~(function () {/*** call: 改变函数中的this指向* params* context 可以不传递&#xff0c;传递必须是引用类型的值&#xff0c;因为后面要给它加 fn 属性**/function myCall(context) {/…

最佳实践:Swagger 自动生成 Api 文档

目录 Tapir 介绍 为什么使用 Tapir 快速使用 Tapir 添加依赖 定义一个端点(Endpoint) 生成 Swagger ui 根据 yaml 生成 endpoint 自动生成 API 文档的好处不言而喻&#xff0c;它可以提供给你的团队或者外部协作者&#xff0c;方便 API 使用者准确地调用到你的 API。为了…

list的使用和模拟实现

目录 1.list的介绍及使用 1.1 list的介绍 1.2 list的使用 1.2.1 list的构造 1.2.2 list iterator的使用 1.2.3 list capacity 1.2.4 list element access 1.2.5 list modifiers 2.为什么使用迭代器&#xff1f; 3.list的模拟实现 3.1完整代码 3.2代码解析 4.list与…

YOLOv5-7.0实例分割+TensorRT部署

一&#xff1a;介绍 将YOLOv5结合分割任务并进行TensorRT部署&#xff0c;是一项既具有挑战性又令人兴奋的任务。分割&#xff08;Segmentation&#xff09;任务要求模型不仅能够检测出目标的存在&#xff0c;还要精确地理解目标的边界和轮廓&#xff0c;为每个像素分配相应的…

Spring Boot配置文件与日志文件

1. Spring Boot 配置文件 我们知道, 当我们创建一个Spring Boot项目之后, 就已经有了配置文件存在于目录结构中. 1. 配置文件作用 整个项目中所有重要的数据都是在配置文件中配置的&#xff0c;比如: 数据库的连接信息 (包含用户名和密码的设置) ;项目的启动端口;第三方系统的调…

KMP字符串 (简单清晰/Java)

Kmp算法 解决问题&#xff1a; 字符串匹配问题 怎么解决&#xff1f; 前缀表next[]数组 #分析 先看暴力做法&#xff1a; 两层for循环&#xff0c;一层遍历文本串&#xff0c;一层遍历模式串&#xff08;子串&#xff09;对应的每个字符进行匹配&#xff0c;匹配成功就 i &a…

大数据——协同过滤推荐算法:线性回归算法

推荐系统中的协同过滤算法一般分为两大类&#xff1a; 基于行为的协同过滤算法(Memory-Based CF)&#xff0c;利用用户行为数据计算相似度&#xff0c;包括用户之间的相似度和物品之间的相似度。基于模型的协同过滤算法(Model-Based CF)&#xff0c;利用机器学习算法预测用户的…

华纳云:Ubuntu安装Drupal报错怎么解决

在安装 Drupal 过程中遇到错误可能是由于各种原因引起的。以下是一些常见的安装 Drupal 时可能遇到的问题以及相应的解决方法&#xff1a; 数据库连接问题&#xff1a; 安装 Drupal 时需要连接数据库&#xff0c;如果数据库连接配置不正确&#xff0c;可能会导致安装失败。确保…