尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

SpringBoot AOP切面编程实战:从核心概念到生产级应用

SpringBoot AOP切面编程实战:从核心概念到生产级应用 1. 为什么我们需要切面编程从“打补丁”到“优雅织入”如果你写过一段时间的Java Web应用尤其是基于SpringBoot的下面这个场景你一定不陌生产品经理跑过来说我们需要给所有Controller接口都加上操作日志记录方便排查问题。你看了看代码几十个Controller上百个方法难道要一个一个去加log.info(...)吗这还不算完过两天安全团队又提需求要对某些敏感接口进行权限校验再过一阵性能团队要求监控所有Service层方法的执行耗时……如果每次都靠手动修改业务代码你的代码很快就会变成一锅“意大利面”各种非核心的逻辑日志、安全、事务和核心业务逻辑下单、支付、查询纠缠在一起难以维护更别提单一职责原则了。这就是面向切面编程AOP要解决的核心痛点。AOP允许我们将这些遍布应用多个模块的“横切关注点”Cross-cutting Concerns从业务逻辑中剥离出来集中管理。想象一下你不再需要侵入性地修改每一个业务方法而是像有一个“魔法织布机”可以自动、统一地在所有需要的地方“织入”日志、安全、事务等逻辑。Spring AOP就是Spring框架提供的这套“织布机”而Aspect注解则是我们定义“织布图案”即切面逻辑的主要方式。在SpringBoot中得益于自动配置使用AOP变得异常简单几乎可以做到开箱即用。今天我们就来彻底搞懂如何在SpringBoot项目中使用Aspect实现功能强大且优雅的切面。2. 核心概念快速扫盲Pointcut, Advice, Aspect 和 Join Point在动手写代码之前我们必须统一语言。AOP有自己的术语体系理解它们是你灵活运用切面的基础。我用一个“保安巡逻”的类比来帮你快速建立直观印象Join Point连接点这是程序执行过程中一个明确的点比如方法调用、异常抛出、字段修改等。在Spring AOP中它特指方法的执行。你可以把它想象成大楼里每一个可能发生事件的“位置”例如每个办公室的门口、每个楼梯口。Pointcut切入点这是一个表达式它用来匹配我们想要拦截的那些 Join Point。如果说Join Point是所有可能的点那么Pointcut就是我们的“巡逻路线图”它定义了保安切面需要关注哪些特定的位置。例如“所有com.example.service包下以Impl结尾的类的public方法”就是一个Pointcut表达式。Advice通知这是在特定的 Join Point被Pointcut匹配到的上执行的动作。也就是“保安在巡逻点上具体要做什么”。Spring AOP提供了5种类型的AdviceBefore: 在目标方法执行之前执行。好比保安在进入办公室前先检查工牌。After: 在目标方法执行之后执行无论成功还是异常。好比保安在人员离开办公室后记录一下离开时间。AfterReturning: 在目标方法成功执行并返回结果后执行。好比保安确认人员办完事正常离开后做一个登记。AfterThrowing: 在目标方法抛出异常后执行。好比保安发现有人从办公室慌张跑出发生异常立即上前处理。Around:最强大的通知它包围了目标方法的整个执行过程。你可以在方法执行前、后自定义行为甚至决定是否执行目标方法。好比保安完全接管了办公室的进出他可以选择是否放行放行前后做什么都由他控制。Aspect切面这是Pointcut和Advice的结合体。它定义了“在什么地方Pointcut”和“做什么Advice”。一个切面类用Aspect注解标记可以包含多个Pointcut和Advice。它就是我们的“保安巡逻方案”本身。理解了这些我们就知道编写一个切面主要就是做两件事1. 用表达式定义好你要拦截哪些方法写Pointcut2. 为这些拦截到的方法编写前置、后置或环绕的处理逻辑写Advice。3. 环境搭建与基础配置SpringBoot中的极简AOP开启SpringBoot让AOP的集成变得极其简单。首先创建一个标准的SpringBoot项目这里我推荐使用 start.spring.io 或者IDE的创建向导。3.1 依赖引入对于大多数情况我们只需要引入SpringBoot的AOP Starter即可。它会自动引入Spring AOP及其依赖如AspectJ Weaver。在你的pom.xml文件中添加dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency如果你用的是Gradle则在build.gradle的dependencies块中添加implementation org.springframework.boot:spring-boot-starter-aop注意很多人会困惑是否需要额外引入aspectjweaver。spring-boot-starter-aop已经包含了它所以通常不需要单独引入。除非你需要使用更高级的AspectJ功能如编译时织入LTW才需要考虑额外配置。3.2 启用AspectJ自动代理这是关键一步SpringBoot的自动配置通常会帮我们做好。你只需要确保你的切面类能被Spring的组件扫描到。通常有两种方式在切面类上添加Component注解这是最常用、最推荐的方式。让Spring容器像管理普通Bean一样管理你的切面。在配置类中显式使用EnableAspectJAutoProxy如果你有一个Configuration类可以加上这个注解。但在SpringBoot项目中只要引入了spring-boot-starter-aop依赖这个注解是默认启用的所以通常你不需要手动添加。所以最简单的做法就是引入依赖然后创建一个类加上Aspect和Component注解它就会生效。3.3 创建你的第一个切面日志切面让我们从一个最实用的例子开始记录所有Controller层方法的入参、出参和执行时间。首先创建一个普通的Controller用于测试RestController RequestMapping(/api/user) public class UserController { GetMapping(/{id}) public UserInfo getUser(PathVariable Long id) { // 模拟业务逻辑 return new UserInfo(id, 用户 id, System.currentTimeMillis()); } PostMapping public ResponseEntityString createUser(RequestBody UserInfo user) { // 模拟创建逻辑 return ResponseEntity.ok(用户创建成功ID: user.getId()); } } // 简单的数据对象 Data AllArgsConstructor NoArgsConstructor class UserInfo { private Long id; private String name; private Long createTime; }现在创建我们的日志切面类LoggingAspectpackage com.example.demo.aspect; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Arrays; Aspect // 声明这是一个切面类 Component // 让Spring管理这个Bean Slf4j // 使用Lombok的日志注解 public class LoggingAspect { /** * 定义一个切入点Pointcut匹配所有Controller包下的所有公共方法 * 表达式详解execution(public * com.example.demo.controller..*.*(..)) * - execution: 表示匹配方法执行 * - public: 匹配公共方法 * - 第一个 *: 匹配任意返回类型 * - com.example.demo.controller..*: 匹配controller包及其所有子包下的任意类 * - 第二个 *: 匹配类中的任意方法 * - (..): 匹配任意参数列表 */ Pointcut(execution(public * com.example.demo.controller..*.*(..))) public void controllerLog() {} /** * 环绕通知功能最强大的通知类型。 * param joinPoint 连接点对象包含了目标方法的信息 * return 目标方法的执行结果 * throws Throwable */ Around(controllerLog()) public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { long startTime System.currentTimeMillis(); // 获取请求相关信息适用于Web环境 ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); String requestURI ; if (attributes ! null) { HttpServletRequest request attributes.getRequest(); requestURI request.getRequestURI(); String httpMethod request.getMethod(); log.info( 请求开始: [{}] {} , httpMethod, requestURI); } // 记录类名、方法名、入参 String className joinPoint.getTarget().getClass().getName(); String methodName joinPoint.getSignature().getName(); Object[] args joinPoint.getArgs(); log.info(调用方法: {}.{}, className, methodName); log.info(方法入参: {}, Arrays.toString(args)); Object result; try { // 执行目标方法 result joinPoint.proceed(); long endTime System.currentTimeMillis(); long elapsedTime endTime - startTime; // 记录出参和执行时间 log.info(方法返回: {}, result); log.info(方法执行耗时: {} ms, elapsedTime); log.info( 请求结束: {} (耗时{}ms) \n, requestURI, elapsedTime); } catch (Throwable e) { long endTime System.currentTimeMillis(); log.error(方法执行异常: {}, 耗时: {} ms, e.getMessage(), endTime - startTime); log.error(异常堆栈: , e); // 记录完整堆栈方便排查 // 通常这里我们选择将异常继续抛出让全局异常处理器处理 throw e; } return result; } }启动你的SpringBoot应用调用GET /api/user/1接口你会在控制台看到类似这样的日志 请求开始: [GET] /api/user/1 调用方法: com.example.demo.controller.UserController.getUser 方法入参: [1] 方法返回: UserInfo(id1, name用户1, createTime1712345678901) 方法执行耗时: 12 ms 请求结束: /api/user/1 (耗时12ms) 恭喜你的第一个全功能的AOP切面已经成功运行了。它自动拦截了Controller方法记录了请求的完整生命周期而你没有修改任何一行业务代码。4. 深入切入点表达式如何精准定位你的目标方法上面的例子中我们使用了execution表达式。这是最常用也是最强大的切入点指示器。它的语法结构需要熟练掌握execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)其中带?的是可选部分。我们主要关注核心部分execution(返回类型 包名.类名.方法名(参数列表))4.1 通配符的使用*匹配任意数量的字符除了包分隔符.。例如*Service匹配UserService,OrderService。..两个用途在包名中匹配当前包及其所有子包。如com.example..匹配com.example,com.example.service,com.example.dao等。在参数列表中匹配任意数量、任意类型的参数。如(..)匹配无参、一个参数、多个参数的所有情况。4.2 常用表达式示例匹配特定包下所有方法execution(* com.example.service.*.*(..))匹配com.example.service包下所有类的所有方法但不包括子包。匹配特定包及其子包下所有方法execution(* com.example.service..*.*(..))匹配com.example.service包及其所有子包下所有类的所有方法。匹配特定类所有方法execution(* com.example.service.UserService.*(..))匹配UserService类的所有方法。匹配特定方法名execution(* com.example.service.*Service.save*(..))匹配service包下以Service结尾的类中所有以save开头的方法。匹配特定返回类型和参数execution(public String com.example.controller.*Controller.*(String, int))匹配controller包下以Controller结尾的类中返回String类型且第一个参数为String、第二个参数为int的公共方法。4.3 其他切入点指示器除了execution还有几个常用的指示器可以组合使用实现更精细的匹配annotation匹配带有指定注解的方法。这是非常常用且推荐的方式因为它对代码侵入性最小意图最明确。// 定义一个自定义注解 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface OperateLog { String value() default ; } // 在切面中使用 Pointcut(annotation(com.example.demo.annotation.OperateLog)) public void operateLogPointcut() {}然后你只需要在需要记录日志的方法上加上OperateLog注解即可无需关心它在哪个包哪个类。within匹配指定类型类或包内的所有方法。Pointcut(within(com.example.service..*))等价于execution(* com.example.service..*.*(..))但within通常用于匹配类型而不是具体的方法签名。args匹配参数类型符合指定类型的方法。例如args(java.lang.String, ..)匹配第一个参数是String的方法。within匹配持有指定注解的类里的所有方法。args匹配参数类型持有指定注解的方法。4.4 组合表达式可以使用(与),||(或),!(非) 来组合多个切入点表达式。// 匹配在Service包下并且方法上带有Transactional注解的方法 Pointcut(within(com.example.service..*) annotation(org.springframework.transaction.annotation.Transactional)) public void transactionalServiceMethod() {} // 匹配在Controller包下但排除GetMapping注解的方法 Pointcut(within(com.example.controller..*) !annotation(org.springframework.web.bind.annotation.GetMapping)) public void controllerNonGetMethods() {}实操心得在项目初期为了快速实现功能可能会使用比较宽泛的execution表达式如匹配整个包。但随着项目发展强烈建议逐渐转向使用自定义注解annotation的方式。理由有三第一意图清晰只有标记了注解的方法才被切面影响避免了误拦截第二灵活度高可以轻松地对不同业务场景使用不同的注解和切面第三维护方便当你需要调整切面逻辑时影响范围一目了然。5. 五种通知类型详解与实战场景我们已经用过了功能最强大的Around。现在我们来详细看看其他四种通知类型以及它们各自最适合的应用场景。5.1 Before前置通知在目标方法执行之前运行。无法阻止目标方法的执行除非抛出异常也无法获取方法的返回值。典型场景参数校验、权限检查、记录操作开始标记。Aspect Component Slf4j public class SecurityAspect { Before(execution(* com.example.demo.service.*.*(..))) public void checkPermission(JoinPoint joinPoint) { // 模拟从ThreadLocal或Session中获取当前用户 User currentUser getCurrentUser(); if (currentUser null || !ADMIN.equals(currentUser.getRole())) { log.warn(用户无权限访问方法: {}, joinPoint.getSignature()); throw new SecurityException(权限不足); } log.info(权限校验通过用户: {} 即将执行: {}, currentUser.getUsername(), joinPoint.getSignature()); } // ... getCurrentUser() 方法实现 }5.2 AfterReturning返回后通知在目标方法成功执行并返回后运行。可以获取到方法的返回值。典型场景对方法的返回结果进行后处理、记录成功的操作日志、缓存结果。Aspect Component Slf4j public class CacheAspect { AfterReturning( pointcut execution(* com.example.demo.service.UserService.findUserById(..)), returning user // 必须与通知方法参数名一致 ) public void cacheUserResult(JoinPoint joinPoint, User user) { if (user ! null) { Long userId user.getId(); // 将查询到的用户对象放入缓存如Redis // redisTemplate.opsForValue().set(user: userId, user, 10, TimeUnit.MINUTES); log.info(已将用户ID: {} 的查询结果缓存, userId); } } }注意returning属性的值user必须与通知方法的参数名user完全一致。5.3 AfterThrowing异常通知在目标方法抛出异常后运行。可以获取到抛出的异常对象。典型场景记录异常日志、发送异常告警通知、进行异常转换或统一的异常处理但注意它不会吞掉异常异常依然会向上抛出。Aspect Component Slf4j public class ExceptionLogAspect { AfterThrowing( pointcut execution(* com.example.demo.service..*.*(..)), throwing ex ) public void logServiceException(JoinPoint joinPoint, Exception ex) { String methodName joinPoint.getSignature().toShortString(); Object[] args joinPoint.getArgs(); log.error(业务服务异常 - 方法: {}, 参数: {}, 异常: {}, methodName, Arrays.toString(args), ex.getMessage(), ex); // 可以在这里集成告警平台如发送邮件、钉钉、Slack消息 // alertService.sendAlert(业务异常, methodName, ex); } }5.4 After后置通知在目标方法执行之后运行无论方法是成功返回还是抛出异常。它类似于finally块。它无法获取到方法的返回值和异常。如果你既需要处理正常返回又需要处理异常但又不需要返回值或异常对象可以用它。典型场景释放资源、清理临时数据、记录方法执行结束无论成败。Aspect Component public class ResourceCleanupAspect { After(execution(* com.example.demo.dao.*.*(..))) public void cleanup(JoinPoint joinPoint) { // 模拟清理线程局部变量或关闭某些资源 // ThreadLocalHolder.clear(); log.debug(DAO层方法 {} 执行完毕已执行清理操作。, joinPoint.getSignature().getName()); } }5.5 Around环绕通知再深入我们已经用过它是功能核心。它需要显式调用joinPoint.proceed()来执行目标方法并可以修改传入的参数、包装返回值、甚至完全阻止目标方法的执行。一个更复杂的环绕通知示例接口限流Aspect Component Slf4j public class RateLimitAspect { // 使用Guava的RateLimiter做简单演示生产环境建议用Redis等分布式限流器 private final RateLimiter rateLimiter RateLimiter.create(10.0); // 每秒10个令牌 Around(annotation(rateLimit)) public Object doRateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { double permits rateLimit.permits(); // 从注解获取每次请求需要的令牌数 long timeout rateLimit.timeout(); TimeUnit unit rateLimit.timeUnit(); if (!rateLimiter.tryAcquire(permits, timeout, unit)) { log.warn(接口限流触发拒绝访问: {}, joinPoint.getSignature()); throw new RateLimitException(请求过于频繁请稍后再试); } log.debug(通过限流检查开始执行目标方法...); return joinPoint.proceed(); } } // 自定义限流注解 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RateLimit { double permits() default 1.0; long timeout() default 0; TimeUnit timeUnit() default TimeUnit.MILLISECONDS; }注意事项与避坑指南通知执行顺序如果同一个切面有多个通知匹配同一个连接点执行顺序是Around-Before- 目标方法 -AfterReturning/AfterThrowing-After-Around的剩余部分。不同切面间的顺序可以通过Order注解或实现Ordered接口来控制。Around必须调用proceed()忘记调用joinPoint.proceed()是一个常见错误这会导致目标方法根本不会执行。异常处理在Around通知中如果捕获了异常并处理了但没有再次抛出那么外层的AfterThrowing通知将不会触发调用方也感知不到异常。需要根据业务谨慎决定是吞掉异常还是抛出。性能影响AOP会创建代理对象对方法调用有轻微的性能开销。避免定义过于宽泛的切入点如execution(* *..*.*(..))匹配所有方法这会给系统带来不必要的负担。6. 获取连接点信息与参数处理技巧在通知方法中JoinPoint对象Around中用ProceedingJoinPoint它是JoinPoint的子类是我们的信息宝库。熟练使用它能让你写出更强大的切面逻辑。6.1 常用信息获取Before(controllerLog()) public void beforeAdvice(JoinPoint joinPoint) { // 1. 获取方法签名包含大量信息 Signature signature joinPoint.getSignature(); String methodName signature.getName(); // 方法名 String declaringTypeName signature.getDeclaringTypeName(); // 类全限定名 String shortString signature.toShortString(); // 简短表示如 UserController.saveUser(...) // 2. 获取目标对象被代理的原始对象和代理对象 Object target joinPoint.getTarget(); // 目标对象如 UserServiceImpl 实例 Object aThis joinPoint.getThis(); // 代理对象如 Spring CGLIB 代理实例 // 3. 获取方法参数 Object[] args joinPoint.getArgs(); for (int i 0; i args.length; i) { log.info(参数 {}: 类型{}, 值{}, i, args[i].getClass().getName(), args[i]); } // 4. 获取静态部分较少用 StaticPart staticPart joinPoint.getStaticPart(); }6.2 在 AfterReturning 和 AfterThrowing 中获取返回值和异常AfterReturning(pointcut serviceMethod(), returning result) public void afterReturning(JoinPoint joinPoint, Object result) { // result 就是目标方法的返回值可以对其进行判断、修改注意修改返回值只在某些情况下生效且需谨慎 if (result instanceof List) { log.info(方法返回了一个列表大小: {}, ((List?) result).size()); } } AfterThrowing(pointcut serviceMethod(), throwing ex) public void afterThrowing(JoinPoint joinPoint, Exception ex) { // ex 就是目标方法抛出的异常 log.error(方法抛出异常类型: {}, 消息: {}, ex.getClass().getName(), ex.getMessage()); }6.3 在 Around 中修改参数和返回值ProceedingJoinPoint允许你修改传入的参数并包装返回值。Around(execution(* com.example.service.*.*(..))) public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable { Object[] args pjp.getArgs(); // 1. 修改参数例如对字符串参数进行trim处理 for (int i 0; i args.length; i) { if (args[i] instanceof String) { args[i] ((String) args[i]).trim(); } } // 2. 执行目标方法传入可能修改过的参数 Object result pjp.proceed(args); // 3. 修改返回值例如对返回的字符串结果进行大写转换仅作演示实际需谨慎 if (result instanceof String) { result ((String) result).toUpperCase(); } // 或者包装成统一响应体 // result ResponseEntity.ok().body(new ApiResponse(0, success, result)); return result; }重要提示修改参数和返回值是强大但危险的操作。它破坏了方法的透明性可能导致意想不到的副作用。务必确保这种修改是跨切面一致的并且所有相关开发人员都清楚这一行为。在大多数日志、监控、安全等场景下应避免修改业务数据。7. 高级话题解决AOP失效的常见场景与原理在实际项目中你可能会遇到切面不生效的情况。这通常与Spring AOP的实现机制有关。Spring AOP默认使用基于动态代理的运行时织入。7.1 失效场景一同类内部方法调用这是最常见的坑。看下面的代码Service public class UserServiceImpl implements UserService { public void outerMethod() { log.info(调用外部方法...); this.innerMethod(); // 这里切面会失效 } OperateLog // 假设我们有一个自定义注解被切面拦截 public void innerMethod() { log.info(执行内部方法...); } }当你调用userService.outerMethod()时innerMethod()上的OperateLog切面不会触发。为什么原理Spring AOP通过代理对象工作。当从容器中获取UserService时你拿到的是一个代理对象比如UserService$$EnhancerBySpringCGLIB。调用outerMethod()时是通过代理对象调用的所以代理逻辑切面会执行。但在outerMethod()内部this指向的是目标对象本身UserServiceImpl而不是它的代理对象。因此this.innerMethod()调用绕过了代理切面自然失效。解决方案推荐将内部方法抽取到另一个Bean这是最清晰的方式符合单一职责。Service public class InnerService { OperateLog public void innerMethod() { ... } } Service public class UserServiceImpl { Autowired private InnerService innerService; public void outerMethod() { innerService.innerMethod(); // 通过代理调用切面生效 } }从AopContext获取当前代理对象不推荐强依赖AOP配置EnableAspectJAutoProxy(exposeProxy true) // 首先需要在配置中开启 public class UserServiceImpl { public void outerMethod() { UserService proxy (UserService) AopContext.currentProxy(); proxy.innerMethod(); // 通过代理调用 } }自我注入有点HackService public class UserServiceImpl { Autowired private UserService self; // 注入自己 public void outerMethod() { self.innerMethod(); // 通过注入的代理调用 } }7.2 失效场景二静态方法、final方法、private方法Spring AOP无法为静态方法、final方法和private方法创建代理。因此针对这些方法的切面定义是无效的。确保你的切入点表达式不要匹配到这些方法。7.3 失效场景三未被Spring容器管理的对象只有Spring容器管理的Bean才能被AOP代理。如果你直接new出来的对象或者来自第三方库且未声明为Component的类其方法上的切面不会生效。7.4 检查与调试当切面不生效时可以按以下步骤排查确认切面类已被Spring扫描并实例化有Component等注解。确认spring-boot-starter-aop依赖已正确引入。检查切入点表达式是否正确匹配到了目标方法。可以在切面方法开始加一行日志看是否执行。检查是否属于“同类内部调用”等失效场景。在调试模式下查看注入的Bean类型。如果是有接口的JDK动态代理Bean类型会是com.sun.proxy.$ProxyXXX如果是CGLIB代理类型会是YourClass$$EnhancerBySpringCGLIB$$...。如果看到的是原始类名说明没有被代理。8. 生产级实践构建可维护的切面体系在真实的企业级项目中切面会越来越多。如何优雅地组织和管理它们8.1 使用自定义注解进行标记这是最重要的最佳实践。为不同的横切关注点定义专用的注解使切面逻辑与业务代码解耦意图更明确。// 操作日志注解 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface OpLog { String module() default ; String operation() default ; boolean recordParams() default true; boolean recordResult() default false; } // 在切面中使用 Aspect Component public class OperationLogAspect { Around(annotation(opLog)) public Object around(ProceedingJoinPoint pjp, OpLog opLog) throws Throwable { String module opLog.module(); String operation opLog.operation(); // ... 根据注解属性记录日志 return pjp.proceed(); } } // 在业务方法上使用 Service public class OrderService { OpLog(module 订单, operation 创建订单, recordParams true) public Order createOrder(OrderDTO dto) { // ... } }8.2 切面模块化与职责分离不要试图在一个切面类里做所有事情。根据关注点分离切面。LoggingAspect: 负责通用的请求/响应日志。SecurityAspect: 负责权限校验。CacheAspect: 负责缓存操作。TransactionAspect: 负责特定的事务管理虽然Spring已有Transactional但你可能需要自定义行为。PerformanceAspect: 负责监控方法耗时并推送到监控系统。IdempotentAspect: 负责接口幂等性校验。8.3 将Pointcut表达式集中管理如果一个复杂的表达式被多个切面复用或者为了更好的可读性可以定义一个专门的类来存放Pointcut定义。Component public class SystemPointcuts { // Controller层所有公共方法 Pointcut(execution(public * com.example.web..*.*(..))) public void webLayer() {} // Service层所有方法 Pointcut(execution(* com.example.service..*.*(..))) public void serviceLayer() {} // DAO层所有方法 Pointcut(execution(* com.example.dao..*.*(..))) public void dataAccessLayer() {} // 所有标记了MyAnnotation的方法 Pointcut(annotation(com.example.common.annotation.MyAnnotation)) public void annotatedWithMyAnnotation() {} } // 在其他切面中引用 Aspect Component public class MyAspect { Around(SystemPointcuts.serviceLayer()) public Object aroundService(ProceedingJoinPoint pjp) throws Throwable { // ... } }8.4 处理异步方法如果目标方法是Async的切面逻辑会在调用线程执行而目标方法在另一个线程执行。对于Around你需要小心处理线程上下文信息如MDC日志跟踪ID的传递。Aspect Component public class AsyncAspect { Around(annotation(org.springframework.scheduling.annotation.Async)) public Object aroundAsync(ProceedingJoinPoint pjp) throws Throwable { // 在异步任务执行前捕获当前线程的上下文如MDC中的traceId MapString, String context MDC.getCopyOfContextMap(); try { // 异步任务开始执行时将上下文设置到新线程中 if (context ! null) { MDC.setContextMap(context); } return pjp.proceed(); } finally { // 清理新线程的MDC MDC.clear(); } } }8.5 性能考量与开关在生产环境全量日志记录可能会影响性能。可以为切面添加开关通过配置中心或环境变量控制其启停。Aspect Component ConditionalOnProperty(name aspect.logging.enabled, havingValue true, matchIfMissing false) public class ConditionalLoggingAspect { // 只有当 aspect.logging.enabledtrue 时这个切面才会被创建 }或者在切面内部判断Around(controllerLog()) public Object doAround(ProceedingJoinPoint pjp) throws Throwable { if (!loggingEnabled) { // loggingEnabled 可从 Value 注入 return pjp.proceed(); } // ... 原有的日志记录逻辑 }通过以上这些实践你就能在SpringBoot项目中构建出一套清晰、强大、可维护的AOP体系让那些横跨整个系统的“公共事务”变得井井有条真正实现业务逻辑与非业务逻辑的优雅分离。
返回列表