告别繁琐:SpringBoot 拦截器与统一功能处理
ztj100 2024-12-15 18:00 11 浏览 0 评论
前言
Spring AOP是一个基于面向切面编程的框架,用于将横切性关注点(如日志记录、事务管理)与业务逻辑分离,通过代理对象将这些关注点织入到目标对象的方法执行前后、抛出异常或返回结果时等特定位置执行,从而提高程序的可复用性、可维护性和灵活性。
但使用原生Spring AOP实现统一的拦截是非常繁琐、困难的。而在本节,我们将使用一种简单的方式进行统一功能处理,这也是AOP的一次实战,具体如下:
- 统一用户登录权限验证
- 统一数据格式返回
- 统一异常处理
基于 Spring Boot + MyBatis Plus + Vue 3.2 + Vite + Element Plus 实现的前后端分离博客,包含后台管理系统,支持文章、分类、标签管理、仪表盘等功能。
GitHub 地址:https://github.com/weiwosuoai/WeBlog
Gitee 地址:https://gitee.com/AllenJiang/WeBlog
0 为什么需要统一功能处理?
统一功能处理是为了提高代码的可维护性、可重用性和可扩展性而进行的一种设计思想。在应用程序中,可能存在一些通用的功能需求,例如身份验证、日志记录、异常处理等。
这些功能需要在多个地方进行调用和处理,如果每个地方都单独实现这些功能,会导致代码冗余、难以维护和重复劳动。通过统一功能处理的方式,可以将这些通用功能抽取出来,以统一的方式进行处理。这样做有以下几个好处:
- 「代码复用」:将通用功能抽取成独立的模块或组件,可以在多个地方共享使用,减少重复编写代码的工作量。
- 「可维护性」:将通用功能集中处理,可以方便地对其进行修改、优化或扩展,而不需要在多个地方进行修改。
- 「代码整洁性」:通过统一功能处理,可以使代码更加清晰、简洁,减少了冗余的代码。
- 「可扩展性」:当需要添加新的功能时,只需要在统一功能处理的地方进行修改或扩展,而不需要在多个地方进行修改,降低了代码的耦合度。
1 统一用户登录权限验证
1.1 使用原生 Spring AOP 实现统一拦截的难点
以使用原生 Spring AOP 来实现?户统?登录验证为例,主要是使用前置通知和环绕通知实现的,具体实现如下
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* @author 兴趣使然黄小黄
* @version 1.0
* @date 2023/7/18 16:37
*/
@Aspect // 表明此类为一个切面
@Component // 随着框架的启动而启动
public class UserAspect {
// 定义切点, 这里使用 Aspect 表达式语法
@Pointcut("execution(* com.hxh.demo.controller.UserController.*(..))")
public void pointcut(){ }
// 前置通知
@Before("pointcut()")
public void beforeAdvice() {
System.out.println("执行了前置通知~");
}
// 环绕通知
@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拦截器是Spring框架提供的一个功能强大的组件,用于在请求到达控制器之前或之后进行拦截和处理。拦截器可以用于实现各种功能,如身份验证、日志记录、性能监测等。
要使用Spring拦截器,需要创建一个实现了HandlerInterceptor接口的拦截器类。该接口定义了三个方法:preHandle、postHandle和afterCompletion。
- preHandle方法在请求到达控制器之前执行,可以用于进行身份验证、参数校验等;
- postHandle方法在控制器处理完请求后执行,可以对模型和视图进行操作;
- afterCompletion方法在视图渲染完成后执行,用于清理资源或记录日志。
拦截器的实现可以分为以下两个步骤:
- 创建自定义拦截器,实现 HandlerInterceptor 接口的 preHandle(执行具体方法之前的预处理)方法。
- 将自定义拦截器加入 WebMvcConfigurer 的 addInterceptors 方法中,并且设置拦截规则。
具体实现如下:
step1. 创建自定义拦截器,自定义拦截器是一个普通类,代码如下:
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* @author 兴趣使然黄小黄
* @version 1.0
* @date 2023/7/19 16:31
* 统一用户登录权限验证 —— 登录拦截器
*/
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; // 验证成功, 继续controller的流程
}
// 可以跳转登录界面或者返回 401/403 没有权限码
response.sendRedirect("/login.html"); // 跳转到登录页面
return false; // 验证失败
}
}
step2. 配置拦截器并设置拦截规则,代码如下:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author 兴趣使然黄小黄
* @version 1.0
* @date 2023/7/19 16:51
*/
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/user/login") // 不拦截的 url 地址
.excludePathPatterns("/user/reg")
.excludePathPatterns("/**/*.html"); // 不拦截所有页面
}
}
1.3 拦截器的实现原理及源码分析
当有了拦截器后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下图所示:
「拦截器实现原理的源码分析」
从上述案例实现结果的控制台的日志信息可以看出,所有的 Controller 执?都会通过?个调度器 DispatcherServlet 来实现。
而所有的方法都会执行 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,该方法源码如下:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
// 获取项目中使用的拦截器 HandlerInterceptor
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中的业务逻辑就会执行。
1.4 统一访问前缀添加
统一访问前缀的添加与登录拦截器实现类似,即给所有请求地址添加 /hxh 前缀,示例代码如下:
@Configuration
public class AppConfig implements WebMvcConfigurer {
// 给所有接口添加 /hxh 前缀
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/hxh", c -> true);
}
}
另一种方式是在application配置文件中配置:
server.servlet.context-path=/hxh
2 统一异常处理
统一异常处理是指 在应用程序中定义一个公共的异常处理机制,用来处理所有的异常情况。这样可以避免在应用程序中分散地处理异常,降低代码的复杂度和重复度,提高代码的可维护性和可扩展性。
基于 Spring Boot + MyBatis Plus + Vue 3.2 + Vite + Element Plus 实现的前后端分离博客,包含后台管理系统,支持文章、分类、标签管理、仪表盘等功能。
GitHub 地址:https://github.com/weiwosuoai/WeBlog
Gitee 地址:https://gitee.com/AllenJiang/WeBlog
需要考虑以下几点:
- 异常处理的层次结构:定义异常处理的层次结构,确定哪些异常需要统一处理,哪些异常需要交给上层处理。
- 异常处理的方式:确定如何处理异常,比如打印日志、返回错误码等。
- 异常处理的细节:处理异常时需要注意的一些细节,比如是否需要事务回滚、是否需要释放资源等
本文讲述的统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的:
- @ControllerAdvice 表示控制器通知类。
- @ExceptionHandler 异常处理器。
以上两个注解组合使用,表示当出现异常的时候执行某个通知,即执行某个方法事件,具体实现代码如下:
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 兴趣使然黄小黄
* @version 1.0
* @date 2023/7/19 18:27
* 统一异常处理
*/
@ControllerAdvice // 声明是一个异常处理器
public class MyExHandler {
// 拦截所有的空指针异常, 进行统一的数据返回
@ExceptionHandler(NullPointerException.class) // 统一处理空指针异常
@ResponseBody // 返回数据
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;
}
}
上述代码中,实现了对所有空指针异常的拦截并进行统一的数据返回。
在实际中,常常设置一个保底,比如发生的非空指针异常,也会有保底措施进行处理,类似于 try-catch 块中使用 Exception 进行捕获,代码示例如下:
@ExceptionHandler(Exception.class)
@ResponseBody
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 统一数据返回格式
为了保持 API 的一致性和易用性,通常需要使用统一的数据返回格式。一般而言,一个标准的数据返回格式应该包括以下几个元素:
- 状态码:用于标志请求成功失败的状态信息;
- 消息:用来描述请求状态的具体信息;
- 数据:包含请求的数据信息;
- 时间戳:可以记录请求的时间信息,便于调试和监控。
实现统一的数据返回格式可以使用 @ControllerAdvice + ResponseBodyAdvice 的方式实现,具体步骤如下:
- 创建一个类,并添加 @ControllerAdvice 注解;
- 实现 ResponseBodyAdvice 接口,并重写 supports 和 beforeBodyWrite 方法。
示例代码如下:
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 兴趣使然黄小黄
* @version 1.0
* @date 2023/7/19 18:59
* 统一数据返回格式
*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
/**
* 此方法返回 true 则执行下面的 beforeBodyWrite 方法, 反之则不执行
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/**
* 方法返回之前调用此方法
*/
@Override
public 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);
return null;
}
}
但是,如果返回的 body 原始数据类型是 String ,则会出现类型转化异常,即 ClassCastException。
因此,如果原始返回数据类型为 String ,则需要使用 jackson 进行单独处理,实现代码如下:
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 兴趣使然黄小黄
* @version 1.0
* @date 2023/7/19 18:59
* 统一数据返回格式
*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
/**
* 此方法返回 true 则执行下面的 beforeBodyWrite 方法, 反之则不执行
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/**
* 方法返回之前调用此方法
*/
@Override
public 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;
}
}
但是,在实际业务中,上述代码只是作为保底使用,因为状态码始终返回的是200,过于死板,还需要具体问题具体分析。
来源:blog.csdn.net/m0_60353039/
article/details/131810657
相关推荐
- Whoosh,纯python编写轻量级搜索工具
-
引言在许多应用程序中,搜索功能是至关重要的。Whoosh是一个纯Python编写的轻量级搜索引擎库,可以帮助我们快速构建搜索功能。无论是在网站、博客还是本地应用程序中,Whoosh都能提供高效的全文搜...
- 如何用Python实现二分搜索算法(python二分法查找代码)
-
如何用Python实现二分搜索算法二分搜索(BinarySearch)是一种高效的查找算法,适用于在有序数组中快速定位目标值。其核心思想是通过不断缩小搜索范围,每次将问题规模减半,时间复杂度为(O...
- 路径扫描 -- dirsearch(路径查找器怎么使用)
-
外表干净是尊重别人,内心干净是尊重自己,干净,在今天这个时代,应该是一种极高的赞美和珍贵。。。----网易云热评一、软件介绍Dirsearch是一种命令行工具,可以强制获取web服务器中的目录和文件...
- 78行Python代码帮你复现微信撤回消息!
-
来源:悟空智能科技本文约700字,建议阅读5分钟。本文基于python的微信开源库itchat,教你如何收集私聊撤回的信息。...
- 从零开始学习 Python!2《进阶知识》 Python进阶之路
-
欢迎来到Python学习的进阶篇章!如果你说已经掌握了基础语法,那么这篇就是你开启高手之路的大门。我们将一起探讨面向对象编程...
- 白帽黑客如何通过dirsearch脚本工具扫描和收集网站敏感文件
-
一、背景介绍...
- Python之txt数据预定替换word预定义定位标记生成word报告(四)
-
续接Python之txt数据预定替换word预定义定位标记生成word报告(一)https://mp.toutiao.com/profile_v4/graphic/preview?pgc_id=748...
- Python——字符串和正则表达式中的反斜杠('\')问题详解
-
在本篇文章里小编给大家整理的是关于Python字符串和正则表达式中的反斜杠('\')问题以及相关知识点,有需要的朋友们可以学习下。在Python普通字符串中在Python中,我们用'\'来转义某些普通...
- Python re模块:正则表达式综合指南
-
Python...
- python之re模块(python re模块sub)
-
re模块一.re模块的介绍1.什么是正则表达式"定义:正则表达式是一种对字符和特殊字符操作的一种逻辑公式,从特定的字符中,用正则表达字符来过滤的逻辑。(也是一种文本模式;)2、正则表达式可以帮助我们...
- MySQL、PostgreSQL、SQL Server 数据库导入导出实操全解
-
在数字化时代,数据是关键资产,数据库的导入导出操作则是连接数据与应用场景的桥梁。以下是常见数据库导入导出的实用方法及代码,包含更多细节和特殊情况处理,助你应对各种实际场景。一、MySQL数据库...
- Zabbix监控系统系列之六:监控 mysql
-
zabbix监控mysql1、监控规划在创建监控项之前要尽量考虑清楚要监控什么,怎么监控,监控数据如何存储,监控数据如何展现,如何处理报警等。要进行监控的系统规划需要对Zabbix很了解,这里只是...
- mysql系列之一文详解Navicat工具的使用(二)
-
本章内容是系列内容的第二部分,主要介绍Navicat工具的使用。若查看第一部分请见:...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- Whoosh,纯python编写轻量级搜索工具
- 如何用Python实现二分搜索算法(python二分法查找代码)
- 路径扫描 -- dirsearch(路径查找器怎么使用)
- 78行Python代码帮你复现微信撤回消息!
- 从零开始学习 Python!2《进阶知识》 Python进阶之路
- 白帽黑客如何通过dirsearch脚本工具扫描和收集网站敏感文件
- Python之txt数据预定替换word预定义定位标记生成word报告(四)
- 假期苦短,我用Python!这有个自动回复拜年信息的小程序
- Python——字符串和正则表达式中的反斜杠('\')问题详解
- Python re模块:正则表达式综合指南
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- node卸载 (33)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- exceptionininitializererror (33)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)