基于SpringAOP面向切面编程的一些实践(日志记录、权限控制、统一异常处理)

前言

Spring框架中的AOP(面向切面编程)

        通过上面的文章我们了解到了AOP面向切面编程的思想,接下来通过一些实践,去更加深入的了解我们所学到的知识。


简单回顾一下AOP的常见应用场景

  • 日志记录:记录方法入参、返回值、执行性能等日志信息。

  • 权限控制:通过自定义注解检查用户权限,进行基本的权限控制。

  • 统一异常处理:通过捕获Controller层的异常可以已经统一的异常响应处理。

        接下来,将对上述场景分别进行实践。


准备工作

1、基础依赖

  • JDK17

  • lombok

2、梳理项目结构

aop-demo
├── pom.xml
├── aop-demo-logging
├── aop-demo-permission
└── aop-demo-exception

一、日志记录

1、梳理一下需要记录的信息

  • 记录当前执行方法的线程信息。

  • 记录方法参数(可选)。

  • 记录方法返回值(可选)。

  • 记录方法执行时间。

  • 记录方法执行是否超出阈值,若超出阈值进行一定提示。

  • 数据脱敏。

2、实现注解

        实现注解,通过给方法加上注解的方式进行日志记录。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {/** 是否记录参数(默认开启) */boolean logParams() default true;/** 是否记录返回值(默认开启) */boolean logResult() default true;/** 超时警告阈值(单位:毫秒) */long warnThreshold() default 1000;
}

3、实现切面类

        通过环绕通知的方式,记录方法信息,并收集上面整理的信息。

@Aspect
@Component
@Slf4j
public class LoggingAspect {// 线程信息格式化模板private static final String THREAD_INFO_TEMPLATE = "Thread[ID=%d, Name=%s]";@Pointcut("@annotation(com.djhhh.annotation.Loggable)")public void loggableMethod() {}@Around("loggableMethod()")public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable {// 获取当前线程信息Thread currentThread = Thread.currentThread();String threadInfo = String.format(THREAD_INFO_TEMPLATE,currentThread.getId(),currentThread.getName());MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();String methodName = method.getDeclaringClass().getSimpleName() + "#" + method.getName();Loggable loggable = method.getAnnotation(Loggable.class);boolean logParams = loggable == null || loggable.logParams();boolean logResult = loggable == null || loggable.logResult();long warnThreshold = loggable != null ? loggable.warnThreshold() : 1000;// 记录开始日志(添加线程信息)if (logParams) {log.info("{} - Method [{}] started with params: {}",threadInfo, methodName, formatParams(joinPoint.getArgs()));} else {log.info("{} - Method [{}] started", threadInfo, methodName);}long start = System.currentTimeMillis();Object result = null;try {result = joinPoint.proceed();return result;} catch (Exception e) {// 异常日志添加线程信息log.error("{} - Method [{}] failed: {} - {}",threadInfo, methodName, e.getClass().getSimpleName(), e.getMessage());throw e;} finally {long duration = System.currentTimeMillis() - start;String durationMsg = String.format("%s - Method [%s] completed in %d ms",threadInfo, methodName, duration);if (duration > warnThreshold) {log.warn("{} (超过阈值{}ms)", durationMsg, warnThreshold);} else {log.info(durationMsg);}if (logResult && result != null) {// 结果日志添加线程信息log.info("{} - Method [{}] result: {}",threadInfo, methodName, formatResult(result));}}}// 参数格式化(保持不变)private String formatParams(Object[] args) {return Arrays.stream(args).map(arg -> {if (arg instanceof String) return "String[****]";if (arg instanceof Password) return "Password[PROTECTED]";return Objects.toString(arg);}).collect(Collectors.joining(", "));}// 结果格式化优化:集合类型显示大小private String formatResult(Object result) {return result.toString();}
}

4、实现测试服务

        通过下列的五个的服务进行测试,详细测试情况看下文。

@Service
@Slf4j
public class TestServiceImpl implements TestService {@Override@Loggablepublic Integer sum(ArrayList<Integer> arr) {return arr.stream().mapToInt(Integer::intValue).sum();}@Override@Loggable(warnThreshold = 5)public Integer sumMx(ArrayList<Integer> arr) {try{Thread.sleep(5000);}catch (Exception e){log.error(e.getMessage());}return arr.stream().mapToInt(Integer::intValue).sum();}@Override@Loggablepublic Boolean login(String username, Password password) {return "djhhh".equals(username)&&"123456".equals(password.getPassword());}@Override@Loggable(logResult = false,logParams = false)public void logout() {log.info("登出成功");}
}

5、测试

@SpringBootTest
@ExtendWith({SpringExtension.class, OutputCaptureExtension.class})
class LoggingAspectTest {@Autowiredprivate TestServiceImpl testService;//---- 测试业务逻辑正确性 ----@Test@DisplayName("测试sum方法-正常计算")void testSum_NormalCalculation() {ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));int result = testService.sum(list);assertEquals(6, result);}@Test@DisplayName("测试login方法-正确凭证")void testLogin_CorrectCredentials() {Password password = new Password("123456");boolean result = testService.login("djhhh", password);assertTrue(result);}@Test@DisplayName("测试login方法-错误凭证")void testLogin_WrongCredentials() {Password password = new Password("wrong");boolean result = testService.login("djhhh", password);assertFalse(result);}@Test@DisplayName("测试logout方法-无参数无返回值")void testLogout() {assertDoesNotThrow(() -> testService.logout());}//---- 验证日志切面功能 ----@Test@DisplayName("验证sum方法-参数和结果日志")void testSum_Logging(CapturedOutput output) {ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));testService.sum(list);// 验证日志内容String logs = output.toString();assertTrue(logs.contains("Method [TestServiceImpl#sum] started with params: [1, 2, 3]"));assertTrue(logs.contains("Method [TestServiceImpl#sum] result: 6"));}@Test@DisplayName("验证login方法-敏感参数脱敏")void testLogin_SensitiveParamMasking(CapturedOutput output) {Password password = new Password("123456");testService.login("djhhh", password);// 验证参数脱敏String logs = output.toString();assertTrue(logs.contains("String[****], Password[PROTECTED]"), "未正确脱敏敏感参数");assertFalse(logs.contains("123456"), "密码明文泄露");}@Test@DisplayName("验证logout方法-关闭参数和结果日志")void testLogout_NoParamNoResultLog(CapturedOutput output) {testService.logout();String logs = output.toString();assertTrue(logs.contains("Method [TestServiceImpl#logout] started"));assertFalse(logs.contains("started with params"));assertFalse(logs.contains("result:"));}@Test@DisplayName("验证sumMx方法-超时告警")void testSumMx_ThresholdExceeded(CapturedOutput output) throws InterruptedException {// 构造大数据量延长执行时间(根据实际性能调整)ArrayList<Integer> bigList = new ArrayList<>();for (int i = 0; i < 10; i++) {bigList.add(i);}testService.sumMx(bigList);// 验证超时警告String logs = output.toString();assertTrue(logs.contains("(超过阈值5ms)"), "未触发超时警告");}
}

        测试结果如下:

        至此通过Spring AOP实现日志记录的实践完毕。

实践总结

        通过SpringAOP实现日志记录的解耦,将日志逻辑从业务代码中剥离,提升了代码的可维护性和系统运行状态的可观测性。


二、权限校验

        本实践只进行基础的权限身份校验,想要更加详细的权限校验权限可以参考下面的文章。

权限系统设计方案实践(Spring Security + RBAC 模型)

1、线程工具

        用于保存用户信息。

public class UserContext {private static final ThreadLocal<Set<String>> permissionsHolder = new ThreadLocal<>();// 设置当前用户权限public static void setCurrentPermissions(Set<String> permissions) {permissionsHolder.set(permissions);}// 获取当前用户权限public static Set<String> getCurrentPermissions() {return permissionsHolder.get();}// 清除上下文public static void clear() {permissionsHolder.remove();}
}

2、实现注解和常量类

        实现注解和常量类,为后续权限校验进行准备工作。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Role {/** 需要的权限标识 */String[] value();/** 校验逻辑:AND(需全部满足)或 OR(满足其一) */Logical logical() default Logical.AND;
}
enum class Logical {AND, OR
}

3、定义切面类

        通过定义切面类进行权限校验。

        通过@Before注解,在进入方法之前进行权限校验。

@Aspect
@Component
public class PermissionAspect {@Pointcut("@annotation(role)")public void rolePointcut(Role role) {}/*** 定义切入点:拦截所有带 @RequiresPermission 注解的方法*/@Before("rolePointcut(role)")public void checkPermission(Role role){// 获取当前用户权限列表(需自行实现用户权限获取逻辑)Set<String> userPermissions = UserContext.getCurrentPermissions();// 校验权限boolean hasPermission;String[] requiredPermissions = role.value();Logical logical = role.logical();if (logical == Logical.AND) {hasPermission = Arrays.stream(requiredPermissions).allMatch(userPermissions::contains);} else {hasPermission = Arrays.stream(requiredPermissions).anyMatch(userPermissions::contains);}if (!hasPermission) {throw new RuntimeException("权限不足,所需权限: " + Arrays.toString(requiredPermissions));}}
}

4、实现测试服务

        两个方法,分别测试满足权限和不满足权限。

@Service
public class TestService {@Role(value = {"order:read", "order:write"}, logical = Logical.OR)public void query(Long id) {}@Role("order:admin")public void delete(Long id) {}
}

5、测试

        对两种情况分别进行测试。

@SpringBootTest
public class PermissionAspectTest {@Autowiredprivate TestService testService;@Test@DisplayName("测试AND逻辑-权限满足")void testAndLogicSuccess() {// 模拟用户有全部权限UserContext.setCurrentPermissions(Set.of("order:read", "order:write"));assertDoesNotThrow(() -> testService.query(1L));}@Test@DisplayName("测试OR逻辑-权限不足")void testOrLogicFailure() {// 模拟用户只有部分权限UserContext.setCurrentPermissions(Set.of("order:read"));assertThrows(RuntimeException.class,() -> testService.delete(1L),"应检测到权限不足");}
}

        测试结果如下:

实践总结

        通过AOP可以进行简单的权限校验工作,若项目中对权限的颗粒度需求没有那么细的情况下,可以使用该方法进行权限校验。


三、异常统一处理

1、准备工作

响应类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {private int code;    // 业务状态码private String msg;  // 错误描述private T data;      // 返回数据// 快速创建成功响应public static <T> ApiResponse<T> success(T data) {return new ApiResponse<>(200, "success", data);}// 快速创建错误响应public static ApiResponse<?> error(int code, String msg) {return new ApiResponse<>(code, msg, null);}
}

自定义异常类

@Getter
public class BusinessException extends RuntimeException {private final int code;  // 自定义错误码public BusinessException(int code, String message) {super(message);this.code = code;}
}

2、全局异常捕捉

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** 处理业务异常(返回HTTP 200,通过code区分错误)*/@ExceptionHandler(BusinessException.class)public ApiResponse<?> handleBusinessException(BusinessException e) {log.error("业务异常: code={}, msg={}", e.getCode(), e.getMessage());return ApiResponse.error(e.getCode(), e.getMessage());}/*** 处理参数校验异常(返回HTTP 400)*/@ResponseStatus(HttpStatus.BAD_REQUEST)@ExceptionHandler(BindException.class)public ApiResponse<?> handleValidationException(BindException e) {String errorMsg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();log.error("参数校验失败: {}", errorMsg);return ApiResponse.error(400, errorMsg);}/*** 处理其他所有异常(返回HTTP 500)*/@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)@ExceptionHandler(Exception.class)public ApiResponse<?> handleGlobalException(Exception e) {log.error("系统异常: ", e);return ApiResponse.error(500, "系统繁忙,请稍后重试");}
}

3、测试类

@RestController
@RequestMapping("/api")
public class TestController {@GetMapping("/test/get")public ApiResponse<String> test(@RequestParam String id){if(id==null){throw new RuntimeException("id为空");}return ApiResponse.success(id);}
}

        测试结果如下:

实践总结

在项目中比较常用的一个异常捕获方式,我们可以通过该方式,统一捕获项目中的异常,便于项目的异常处理。


总结

         通过上面的三个实践,可以加深我们对于AOP的理解和应用。通过Spring AOP对我们的服务进行抽象处理,简化我们的开发和维护成本,写出更加高质量的代码。


github链接:https://github.com/Djhhhhhh/aop-demo

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

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

相关文章

Rust 语言语法糖深度解析:优雅背后的编译器魔法

之前介绍了语法糖的基本概念和在C/Python/JavaScript中的使用&#xff0c;今天和大家讨论语法糖在Rust中的表现形式。 程序语言中的语法糖&#xff1a;让代码更优雅的甜味剂 引言&#xff1a;语法糖的本质与价值 语法糖(Syntactic Sugar) 是编程语言中那些并不引入新功能&…

【56】数组指针:指针穿梭数组间

【56】数组指针&#xff1a;指针穿梭数组间 引言 在嵌入式系统开发中&#xff0c;指针操作是优化内存管理和数据交互的核心技术。本文以STC89C52单片机为平台&#xff0c;通过一维指针强制转换、二维指针结构化操作和**return返回指针**三种方法&#xff0c;系统讲解指针操作二…

C语言【指针二】

引言 介绍&#xff1a;const修饰指针&#xff0c;野指针 应用&#xff1a;指针的使用&#xff08;strlen的模拟实现&#xff09;&#xff0c;传值调用和传指调用 一、const修饰指针 1.const修饰变量 简单回顾一下前面学过的const修饰变量&#xff1a;在变量前面加上const&…

学习记录-软件测试基础

一、软件测试分类 1.按阶段&#xff1a;单元测试&#xff08;一般开发自测&#xff09;、集成测试、系统测试、验收测试 2.按代码可见度测试&#xff1a;黑盒测试、灰盒测试、白盒测试 3.其他&#xff1a;冒烟测试(冒烟测试主要是在开发提测后进行&#xff0c;主要是测试主流…

RAG系统实战:当检索为空时,如何实现生成模块的优雅降级(Fallback)?

目录 RAG系统实战&#xff1a;当检索为空时&#xff0c;如何实现生成模块的优雅降级&#xff08;Fallback&#xff09;&#xff1f; 一、为什么需要优雅降级&#xff08;Fallback&#xff09;&#xff1f; 二、常用的优雅降级策略 策略一&#xff1a;预设后备提示&#xff0…

spring boot前后端开发上传文件时报413(Request Entity Too Large)错误的可能原因及解决方案

可能原因及解决方案 1. Spring Boot默认文件大小限制 原因&#xff1a;Spring Boot默认单文件最大为1MB&#xff0c;总请求体限制为10MB。解决方案&#xff1a; 在application.properties中配置&#xff1a;spring.servlet.multipart.max-file-size10MB # 单文件最大 spring…

Qt - findChild

findChild 1. 函数原型2. 功能描述3. 使用场景4. 示例代码5. 注意事项6. 总结 在 Qt 中&#xff0c;每个 QObject 都可以拥有子对象&#xff0c;而 QObject 提供的模板函数 findChild 就是用来在对象树中查找满足特定条件的子对象的工具。下面我们详细介绍一下它的使用和注意事…

Sink Token

论文&#xff1a;ICLR 2025 MLLM视觉VAR方法Attention重分配 Sink Token 是一种在语言模型(LLM)和多模态模型(MLLM)中用于优化注意力分配的关键机制&#xff0c;通过吸收模型中冗余的注意力权重&#xff0c;确保注意力资源不被无效或无关信息占用。以下是对这一概念的系统性解…

Spring Event 观察者模型及事件和消息队列之间的区别笔记

Spring Event观察者模型&#xff1a;基于内置事件实现自定义监听 在Spring框架中&#xff0c;观察者模式通过事件驱动模型实现&#xff0c;允许组件间通过事件发布与监听进行解耦通信。这一机制的核心在于ApplicationEvent、ApplicationListener和ApplicationEventPublisher等接…

【复活吧,我的爱机!】Ideapad300-15isk拆机升级:加内存条 + 换固态硬盘 + 换电源

写在前面&#xff1a;本博客仅作记录学习之用&#xff0c;部分图片来自网络&#xff0c;如需引用请注明出处&#xff0c;同时如有侵犯您的权益&#xff0c;请联系删除&#xff01; 文章目录 前言升级成本升级流程电池健康度加内存条和换内存条光驱位加装机械硬盘更换电池重装系…

基于PyQt5的自动化任务管理软件:高效、智能的任务调度与执行管理

基于PyQt5的自动化任务管理软件&#xff1a;高效、智能的任务调度与执行管理 相关资源文件已经打包成EXE文件&#xff0c;可双击直接运行程序&#xff0c;且文章末尾已附上相关源码&#xff0c;以供大家学习交流&#xff0c;博主主页还有更多Python相关程序案例&#xff0c;秉着…

JavaScript 库:全面解析与推荐

JavaScript 库:全面解析与推荐 引言 JavaScript 作为当今最流行的前端开发语言之一,拥有丰富的库和框架。这些库和框架极大地简化了开发工作,提高了开发效率。本文将全面解析 JavaScript 库,并推荐一些优秀的库,帮助开发者更好地掌握 JavaScript。 JavaScript 库概述 …

C#从入门到精通(5)

目录 第十二章 其他基础知识 &#xff08;1&#xff09;抽象类和方法 &#xff08;2&#xff09;接口 &#xff08;3&#xff09;集合与索引器 &#xff08;4&#xff09;委托和匿名方法 &#xff08;5&#xff09;事件 &#xff08;6&#xff09;迭代器 &#xff08;7…

【区块链安全 | 第十四篇】类型之值类型(一)

文章目录 值类型布尔值整数运算符取模运算指数运算 定点数地址&#xff08;Address&#xff09;类型转换地址成员balance 和 transfersendcall&#xff0c;delegatecall 和 staticcallcode 和 codehash 合约类型&#xff08;Contract Types&#xff09;固定大小字节数组&#x…

Windows 系统下多功能免费 PDF 编辑工具详解

IceCream PDF Editor是一款极为实用且操作简便的PDF文件编辑工具&#xff0c;它完美适配Windows操作系统。其用户界面设计得十分直观&#xff0c;哪怕是初次接触的用户也能快速上手。更为重要的是&#xff0c;该软件具备丰富多样的强大功能&#xff0c;能全方位满足各类PDF编辑…

vue3相比于vue2的提升

性能提升&#xff1a; Vue3的页面渲染速度更快、性能更好。特别是在处理大量数据和复杂组件时&#xff0c;优势更加明显。Vue3引入了编译时优化&#xff0c;如静态节点提升&#xff08;hoistStatic&#xff09;、补丁标志&#xff08;patchflag&#xff09;等&#xff0c;这些…

Redis 梳理汇总目录

Redis 哨兵集群&#xff08;Sentinel&#xff09;与 Cluster 集群对比-CSDN博客 如何快速将大规模数据保存到Redis集群-CSDN博客 Redis的一些高级指令-CSDN博客 Redis 篇-CSDN博客

【奇点时刻】GPT-4o新生图特性深度洞察报告

以下报告围绕最新推出的「GPT4o」最新图像生成技术展开&#xff0c;旨在让读者从整体层面快速了解其技术原理、功能亮点&#xff0c;以及与其他常见图像生成或AI工具的对比分析&#xff0c;同时也会客观探讨该技术在应用过程中可能遇到的挑战与限制。 1. 技术背景概述 GPT4o新…

【算法day28】解数独——编写一个程序,通过填充空格来解决数独问题

37. 解数独 编写一个程序&#xff0c;通过填充空格来解决数独问题。 数独的解法需 遵循如下规则&#xff1a; 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。&#xff08;请参考示例图&#xff…

【已解决】Javascript setMonth跨月问题;2025-03-31 setMonth后变成 2025-05-01

文章目录 bug重现解决方法&#xff1a;用第三方插件来实现&#xff08;不推荐原生代码来实现&#xff09;。项目中用的有dayjs。若要自己实现&#xff0c;参考 AI给出方案&#xff1a; bug重现 今天&#xff08;2025-04-01&#xff09;遇到的一个问题。原代码逻辑大概是这样的…