ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring AOP核心原理与实战:从动态代理到统一日志与事务管理

Spring AOP核心原理与实战:从动态代理到统一日志与事务管理 1. 项目概述为什么AOP是框架设计的“瑞士军刀”如果你写过一段时间Java尤其是在Spring生态里摸爬滚打过那你肯定对AOPAspect-Oriented Programming面向切面编程这个词不陌生。它经常和“解耦”、“非侵入式”、“横切关注点”这些听起来有点玄乎的词绑在一起。但说句实在话我第一次接触AOP的时候感觉就像在看一本天书——什么前置通知、后置通知、环绕通知还有那些XML配置或者注解看得人头大。直到后来在项目中真正用它解决了实际问题比如统一日志、事务管理和权限校验我才恍然大悟这玩意儿不是什么高深的理论而是一把极其趁手的“瑞士军刀”专门用来处理那些散落在业务代码各处、但又不得不做的“脏活累活”。简单来说AOP允许你把像日志记录、性能监控、安全控制、事务管理这些和核心业务逻辑关系不大但又必须存在的功能从业务代码中剥离出来单独进行定义和管理。想象一下你有一个用户服务类里面有注册、登录、修改信息等方法。按照传统写法你不得不在每个方法的开头写日志记录“开始执行XXX”在结尾写“执行完毕耗时XXms”在异常捕获里写错误日志如果涉及数据库操作还得加上Transactional注解或者手动控制事务。这些代码会像“样板代码”一样重复地出现在每一个方法里不仅让核心业务逻辑变得臃肿不堪而且一旦要修改日志格式或者事务传播行为你就得把所有相关方法都改一遍简直是维护的噩梦。AOP就是为了终结这个噩梦而生的。它的核心思想是“横向切割”。我们把应用程序想象成一个多层蛋糕每一层代表一个核心业务模块比如用户模块、订单模块。而像日志、事务这些功能就像一把刀横向地切过所有这些层。AOP就是定义这把“刀”切面以及“切”的规则切入点然后由框架在运行时自动把“刀”应用到该“切”的地方连接点执行额外的逻辑通知。这样业务模块就只需要关心自己的核心逻辑变得干净、纯粹。这就是所谓的“横切关注点”的分离。对于开发者而言尤其是构建和维护中大型应用的架构师和资深开发掌握AOP意味着你拥有了更强大的代码组织能力和架构设计武器能显著提升代码的可读性、可维护性和可复用性。2. AOP核心概念深度拆解不只是几个术语要玩转AOP光知道它能干什么还不够必须把它的几个核心概念吃透。这些概念是理解其工作原理和正确使用的基石很多人在配置时出错根源就在于对这些概念的理解似是而非。2.1 连接点、切入点、通知与切面一套精准的外科手术流程我们可以把AOP的执行过程类比成一次精准的外科手术这样理解起来就直观多了。连接点手术的潜在位置连接点Joinpoint指的是程序执行过程中明确定义的点比如方法调用、异常抛出、字段修改等。在Spring AOP中连接点特指方法的执行。你可以把整个应用程序看成一个人体每一个方法如UserService.register()就是一个潜在的“手术部位”。连接点就是所有可能被“动刀”的地方的集合。切入点手术的具体定位切入点Pointcut是一个谓词表达式用于匹配和筛选连接点。如果说连接点是所有可能的穴位那么切入点就是医生根据病情精确选择要下针的那几个穴位。它决定了“在什么地方执行切面逻辑”。切入点表达式是AOP的核心配置之一常用的有execution、annotation等。例如execution(* com.example.service.*.*(..))这个表达式就匹配了com.example.service包下所有类的所有方法。如果匹配成功这些方法就成了“被增强”的目标。注意切入点表达式的编写需要格外小心。过于宽泛如execution(* *.*(..))会匹配到大量不需要的方法影响性能甚至产生副作用过于狭窄又可能漏掉本该增强的方法。通常建议从业务包名开始限定这是实践中最重要的经验之一。通知手术的具体动作通知Advice定义了在切入点“处”要执行的逻辑也就是“切面”的具体行为。它回答了“什么时候做什么”的问题。根据执行时机的不同通知主要分为以下几类前置通知在目标方法执行之前执行。比如记录方法入参、进行权限校验。后置通知在目标方法执行之后执行无论是否抛出异常。比如记录方法执行结束。返回通知在目标方法成功执行并返回结果之后执行。可以获取到方法的返回值用于记录结果或进行后续处理。异常通知在目标方法抛出异常时执行。用于记录异常信息、进行异常转换或告警。环绕通知功能最强大的通知类型。它包围了目标方法的执行可以在方法调用前后执行自定义逻辑甚至可以决定是否继续执行目标方法、修改参数或返回值。它是实现事务、缓存等功能的常用手段。切面完整的手术方案切面Aspect是通知和切入点的结合体。它定义了“是什么”逻辑通知以及“在何处”应用该逻辑切入点。一个切面类就是一个模块化的单元它封装了某个横切关注点的所有行为。例如你可以创建一个LoggingAspect切面里面包含一个切入点表达式匹配所有Service方法和若干个通知前置、后置、环绕等共同完成了整个日志记录的功能。2.2 织入魔法发生的时刻织入Weaving是将切面应用到目标对象并创建新的代理对象的过程。这是AOP框架如Spring AOP的核心工作。你可以把它理解为“实施手术”的过程。根据织入发生的时机可以分为编译时织入在源代码编译期间通过特殊的编译器如AspectJ编译器将切面代码直接织入到字节码中。类加载时织入在JVM加载类文件时通过类加载器动态地将切面代码织入。运行时织入在应用运行过程中通过动态代理技术创建代理对象。Spring AOP默认采用的就是运行时织入。Spring AOP的运行时织入其本质是基于代理的设计模式。它不会直接修改原有的目标类字节码而是在运行时为目标对象创建一个“替身”代理对象。当客户端调用目标对象的方法时实际上调用的是这个代理对象的方法。代理对象在调用真实目标方法的前后会插入切面中定义的通知逻辑。这样对于客户端来说它感知不到代理的存在但切面的效果已经生效了。3. Spring AOP的两种代理机制JDK与CGLIB的抉择这是面试常问也是实际使用中容易混淆的点。Spring AOP底层实现动态代理主要依靠两种技术JDK动态代理和CGLIB代理。理解它们的区别对于性能调优和问题排查至关重要。3.1 JDK动态代理基于接口的“契约”工作原理JDK动态代理是Java标准库java.lang.reflect.Proxy提供的能力。它要求目标类必须实现至少一个接口。代理机制会基于这个接口在运行时动态生成一个实现了相同接口的代理类。怎么工作当你调用代理对象的方法时调用会被转发到一个统一的InvocationHandler的invoke方法中。在这个invoke方法里Spring AOP框架会执行你定义的切面通知并决定是否以及如何调用真实目标对象的方法。优点无需引入第三方库是Java标准。生成速度快。缺点强制要求目标对象有接口。对于没有接口的普通类POJO无能为力。只能代理接口中声明的方法。如果目标类有自己的非接口方法这些方法无法被代理。3.2 CGLIB代理基于继承的“模仿”工作原理CGLIBCode Generation Library是一个强大的字节码生成库。它通过继承目标类的方式来创建代理。运行时CGLIB会生成目标类的一个子类并重写其中的非final方法。怎么工作子类即代理对象重写了父类的方法在重写的方法中加入了方法拦截器MethodInterceptor的逻辑从而植入切面通知。优点不需要目标类实现接口。可以代理普通的类。可以代理更多的方法除了final、private等。缺点因为是基于继承所以无法代理final类或final方法。生成代理类的速度通常比JDK动态代理慢一些但现代JVM优化下差距已不明显。需要引入额外的CGLIB库依赖。3.3 实战中的选择与Spring的默认行为在Spring Boot 2.x及之后的版本中默认的代理策略是如果目标对象实现了接口则使用JDK动态代理如果没有实现任何接口则使用CGLIB代理。但是你可以在配置中强制指定使用CGLIB这在某些场景下是有利的# application.properties spring.aop.proxy-target-classtrue或者在Java配置类上使用EnableAspectJAutoProxy(proxyTargetClass true)。为什么有时要强制使用CGLIB类型转换问题这是最常见的坑。如果你将代理对象强制转换为目标类的具体类型而不是其接口类型在使用JDK代理时会抛出ClassCastException因为JDK代理生成的是一个实现了接口的新类并非目标类的实例。而CGLIB生成的是目标类的子类可以安全地转换为父类目标类类型。需要代理非接口方法如果你的切面需要应用到目标类独有的、未在接口中声明的方法上就必须使用CGLIB。性能考量在大量代理的情况下一些基准测试显示CGLIB调用可能稍快因为它是直接的方法调用而JDK代理需要通过反射调用InvocationHandler。但这通常不是决定性因素。实操心得在大多数基于Spring Boot的现代应用中我倾向于直接配置spring.aop.proxy-target-classtrue统一使用CGLIB代理。这样可以避免因疏忽而导致的类型转换异常也让代码更简单——不必为了AOP而特意去为每个类创建接口。除非你有明确的理由必须使用基于接口的JDK代理例如与某些严格依赖接口的旧框架集成。4. 从零到一Spring Boot中AOP的完整实操理论讲得再多不如动手做一遍。我们以一个最经典的“统一日志记录”场景为例在Spring Boot项目中完整实现一个AOP切面。4.1 环境准备与依赖引入首先创建一个新的Spring Boot项目或者在你现有的项目中确保pom.xmlMaven或build.gradleGradle中包含了AOP的依赖。对于Maven项目dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency这个starter已经包含了Spring AOP所需的全部核心依赖包括AspectJ的织入器。注意我们使用的是Spring AOP它依赖于AspectJ的注解和表达式语言但运行时织入是Spring自己通过动态代理实现的并非完整的AspectJ编译器。4.2 定义业务目标类我们先创建一个简单的服务类作为被代理的目标。为了对比我们创建一个有接口的和一个没有接口的。// 1. 接口和其实现类可用于JDK代理 public interface UserService { String register(String username, String password); boolean login(String username, String password); } Service public class UserServiceImpl implements UserService { Override public String register(String username, String password) { // 模拟业务逻辑 System.out.println(执行用户注册: username); return 注册成功ID: 1001; } Override public boolean login(String username, String password) { System.out.println(执行用户登录: username); if (admin.equals(username) 123456.equals(password)) { return true; } throw new RuntimeException(用户名或密码错误); } // 一个非接口方法 public void profile() { System.out.println(查看用户资料); } } // 2. 一个没有接口的普通服务类必须用CGLIB代理 Service public class ProductService { public String getProductById(Long id) { System.out.println(查询产品信息ID: id); return 产品名称: 手机; } }4.3 创建并配置切面类这是AOP的核心。我们创建一个LoggingAspect类使用Aspect注解标记它是一个切面并用Component让Spring管理它。package com.example.demo.aspect; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import org.springframework.util.StopWatch; import java.util.Arrays; Aspect // 1. 声明这是一个切面类 Component // 2. 交由Spring容器管理 public class LoggingAspect { // 3. 定义切入点Pointcut匹配com.example.demo.service包下所有类的所有方法 Pointcut(execution(* com.example.demo.service..*.*(..))) public void serviceLayer() { // 方法体通常为空仅作为切入点定义的载体 } // 4. 前置通知在匹配的方法执行前运行 Before(serviceLayer()) public void logBefore(JoinPoint joinPoint) { // JoinPoint对象包含了连接点的信息如方法名、参数等 String methodName joinPoint.getSignature().toShortString(); Object[] args joinPoint.getArgs(); System.out.println([前置通知] 准备执行方法: methodName , 参数: Arrays.toString(args)); } // 5. 后置通知在匹配的方法执行后运行无论成功或异常 After(serviceLayer()) public void logAfter(JoinPoint joinPoint) { System.out.println([后置通知] 方法执行结束: joinPoint.getSignature().toShortString()); } // 6. 返回通知在方法成功执行并返回后运行可以获取返回值 AfterReturning(pointcut serviceLayer(), returning result) public void logAfterReturning(JoinPoint joinPoint, Object result) { System.out.println([返回通知] 方法: joinPoint.getSignature().toShortString() 执行成功返回值: result); } // 7. 异常通知在方法抛出异常时运行可以获取异常对象 AfterThrowing(pointcut serviceLayer(), throwing ex) public void logAfterThrowing(JoinPoint joinPoint, Exception ex) { System.out.println([异常通知] 方法: joinPoint.getSignature().toShortString() 抛出异常: ex.getMessage()); } // 8. 环绕通知功能最强大可以控制方法是否执行、修改参数和返回值 Around(serviceLayer()) public Object logAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { String methodName proceedingJoinPoint.getSignature().toShortString(); System.out.println([环绕通知-开始] 进入方法: methodName); // 计时开始 StopWatch stopWatch new StopWatch(); stopWatch.start(); Object result; try { // 执行目标方法这是可选的你可以不执行或者执行多次 result proceedingJoinPoint.proceed(); } catch (Throwable throwable) { System.out.println([环绕通知-异常] 方法: methodName 执行异常: throwable.getMessage()); throw throwable; // 通常需要将异常原样抛出 } finally { // 计时结束 stopWatch.stop(); System.out.println([环绕通知-结束] 退出方法: methodName , 耗时: stopWatch.getTotalTimeMillis() ms); } // 你可以在这里修改返回值但需谨慎 // if (result instanceof String) { // result 被切面修改后的: result; // } return result; } }4.4 编写测试控制器并观察效果创建一个简单的REST控制器来触发我们的服务方法。RestController RequestMapping(/api) public class TestController { Autowired private UserService userService; // 注入接口实际是代理对象 Autowired private ProductService productService; // 注入具体类实际是代理对象 GetMapping(/register) public String register() { return userService.register(testUser, password123); } GetMapping(/login) public boolean login() { // 这里会抛出异常触发异常通知 return userService.login(wrong, wrong); } GetMapping(/product) public String getProduct() { return productService.getProductById(1L); } }启动Spring Boot应用访问/api/register观察控制台输出你会看到类似这样的日志清晰地展示了各个通知的执行顺序[环绕通知-开始] 进入方法: UserServiceImpl.register(..) [前置通知] 准备执行方法: UserServiceImpl.register(..), 参数: [testUser, password123] 执行用户注册: testUser [返回通知] 方法: UserServiceImpl.register(..) 执行成功返回值: 注册成功ID: 1001 [后置通知] 方法执行结束: UserServiceImpl.register(..) [环绕通知-结束] 退出方法: UserServiceImpl.register(..), 耗时: 15ms访问/api/login会触发异常通知[环绕通知-开始] 进入方法: UserServiceImpl.login(..) [前置通知] 准备执行方法: UserServiceImpl.login(..), 参数: [wrong, wrong] 执行用户登录: wrong [异常通知] 方法: UserServiceImpl.login(..) 抛出异常: 用户名或密码错误 [后置通知] 方法执行结束: UserServiceImpl.login(..) [环绕通知-异常] 方法: UserServiceImpl.login(..) 执行异常: 用户名或密码错误 [环绕通知-结束] 退出方法: UserServiceImpl.login(..), 耗时: 1ms5. 高级用法与最佳实践让AOP更强大、更稳健掌握了基础用法后我们来看看如何更精准、更安全地使用AOP以及一些能极大提升效率的“骚操作”。5.1 精细化切入点表达式execution是最常用的切入点指示符其语法为execution(修饰符? 返回类型 声明类型? 方法名(参数列表) 异常类型?)其中?表示可选部分。精准匹配execution(public String com.example.service.UserService.register(String, String))匹配包下所有类execution(* com.example.service.*.*(..))(注意这里不包含子包)匹配包及其子包下所有类execution(* com.example.service..*.*(..))(两个点..代表当前包及子包)匹配特定注解的方法annotation(com.example.annotation.MyLog)。这是非常有用的一种方式可以实现声明式切面。匹配带有特定注解的类within(org.springframework.stereotype.Service)或target(org.springframework.stereotype.Service)。within匹配当前类不包括父类的注解target匹配目标类包括其父类的注解。组合使用可以使用与、||或、!非来组合多个表达式。例如Pointcut(execution(* com.example.service..*.*(..)) within(org.springframework.stereotype.Service))匹配service包及其子包下所有被Service注解的类的方法。5.2 基于注解的声明式切面我们经常希望只有某些特定的方法才被增强而不是一个包下的所有方法。这时自定义注解配合annotation切入点就非常优雅。定义自定义注解Target(ElementType.METHOD) // 注解可以用在方法上 Retention(RetentionPolicy.RUNTIME) // 运行时保留 public interface OperationLog { String value() default ; String operator() default system; }修改切面使用annotation切入点Aspect Component public class OperationLogAspect { // 切入点匹配所有被OperationLog注解的方法 Pointcut(annotation(com.example.demo.annotation.OperationLog)) public void operationLogPointcut() {} Around(operationLogPointcut()) public Object logOperation(ProceedingJoinPoint pjp) throws Throwable { MethodSignature signature (MethodSignature) pjp.getSignature(); Method method signature.getMethod(); OperationLog annotation method.getAnnotation(OperationLog.class); String operationName annotation.value(); String operator annotation.operator(); System.out.println(【操作日志】操作员: operator , 开始执行: operationName); Object result pjp.proceed(); System.out.println(【操作日志】操作员: operator , 执行完成: operationName); return result; } }在业务方法上使用注解Service public class OrderService { OperationLog(value 创建订单, operator #{T(com.example.demo.util.SecurityUtil).getCurrentUser()}) public Order createOrder(OrderDTO dto) { // ... 业务逻辑 } }这样只有标注了OperationLog的方法才会被记录日志控制粒度更细代码的意图也更清晰。注解中的operator甚至可以使用Spring EL表达式动态获取值需要稍复杂的处理。5.3 切面执行顺序与优先级当一个方法匹配多个切面的切入点时通知的执行顺序就变得重要了。例如你可能有日志切面、事务切面、缓存切面等。Spring AOP默认的执行顺序是不确定的。控制顺序的方法实现Ordered接口让切面类实现org.springframework.core.Ordered接口getOrder()方法返回值越小优先级越高越先执行。Aspect Component public class TransactionAspect implements Ordered { Override public int getOrder() { return 1; // 高优先级先执行 } // ... 通知定义 }使用Order注解更简洁的方式是在切面类上添加Order(1)注解。重要规则进入方法时优先级高的切面的前置通知先执行。退出方法时优先级高的切面的后置/返回/异常通知后执行。环绕通知优先级高的切面的Around通知的proceed()方法之前的逻辑先执行但其proceed()方法之后的逻辑后执行。可以把环绕通知想象成一个“洋葱”高优先级的切面在外层。实操心得对于事务和缓存这类切面顺序至关重要。事务切面Transactional的优先级必须高于其他大多数切面。因为事务需要在最外层开启和提交/回滚。如果日志切面先执行并且在事务内部抛异常日志切面可能记录了“操作成功”但事务却回滚了导致数据不一致。通常通过Order(Ordered.LOWEST_PRECEDENCE - 1)或一个较小的值来确保事务切面在最外层。在Spring中Transactional的默认顺序是Ordered.LOWEST_PRECEDENCE即最低优先级这通常需要根据实际情况调整。5.4 性能考量与陷阱规避AOP虽好但滥用或使用不当也会带来问题。性能开销动态代理会带来轻微的方法调用开销因为每次调用都要经过代理拦截器。对于性能极度敏感的核心循环每秒数百万次调用需要谨慎评估。但在绝大多数Web应用场景下这个开销可以忽略不计。自调用失效问题这是AOP最大的一个“坑”。在同一个类中一个方法调用另一个方法被调用的方法上的AOP增强会失效。Service public class ProblemService { public void methodA() { System.out.println(执行 methodA); this.methodB(); // 这里调用methodB其上的切面不会生效 } MyAnnotation // 假设有一个自定义注解被切面拦截 public void methodB() { System.out.println(执行 methodB); } }原因AOP增强是通过代理对象实现的。当外部调用problemService.methodA()时调用的是代理对象代理对象会触发切面。但在methodA内部this.methodB()中的this指的是目标对象本身ProblemService实例而不是它的代理对象因此绕过了代理切面自然失效。解决方案重构将methodB抽取到另一个Bean中通过注入调用。使用AspectJ切换为编译时或类加载时织入的AspectJ模式可以解决自调用问题但配置更复杂。从Spring容器中获取代理不推荐通过AopContext.currentProxy()获取当前代理但需要暴露代理EnableAspectJAutoProxy(exposeProxy true)且代码侵入性强。切入点表达式过于宽泛execution(* *..*.*(..))这样的表达式会匹配Spring容器内所有的Bean方法包括框架自身的方法可能导致意想不到的行为和性能下降。务必精确限定到你的业务包路径。异常处理在环绕通知中如果捕获了异常并处理了但没有再次抛出那么外层的调用者将感知不到这个异常可能导致业务逻辑错误。除非你明确要在切面中消化这个异常否则通常应该throw throwable。6. 经典应用场景剖析AOP在真实项目中的用武之地理解了原理和细节我们来看看AOP在真实项目中是如何大放异彩的。这些场景几乎成了现代Java后端服务的标配。6.1 统一日志与监控这是我们最开始演示的例子也是最常见的用途。通过一个切面可以无侵入地为所有服务层方法自动记录入参、出参、执行时间、异常信息。这比在每个方法里手动写log.info要优雅和高效得多。结合MDCMapped Diagnostic Context还可以在日志中自动嵌入请求ID、用户ID等链路追踪信息。6.2 声明式事务管理Spring的Transactional注解本身就是基于AOP实现的典范。它通过在方法上添加一个注解就自动完成了事务的开启、提交或回滚。其背后的TransactionInterceptor就是一个强大的环绕通知。你可以通过Transactional(propagation Propagation.REQUIRES_NEW, rollbackFor Exception.class)等属性精细控制事务行为而业务代码完全不用关心Connection、commit、rollback这些底层细节。6.3 权限校验与安全控制在Web层我们可以用拦截器Interceptor做权限校验。但对于服务层方法的细粒度权限控制AOP是更好的选择。例如可以自定义一个PreAuthorize(hasRole(ADMIN))这样的注解Spring Security已经提供了然后通过切面来解析注解中的SpEL表达式判断当前用户是否有权限执行该方法。这样权限规则就以声明式的方式与业务代码解耦。6.4 缓存抽象Spring CacheCacheable,CacheEvict,CachePut也是基于AOP实现的。通过在方法上添加缓存注解切面会在方法执行前检查缓存命中则直接返回未命中则执行方法并将结果存入缓存。这极大地简化了缓存代码的编写。6.5 接口限流与熔断在微服务架构中可以使用AOP为某些关键接口自动添加限流如基于Guava RateLimiter或Sentinel或熔断如Resilience4j的逻辑。定义一个RateLimit(permitsPerSecond 100)注解切面在方法执行前进行令牌桶检查超过速率则快速失败避免系统被突发流量打垮。6.6 数据脱敏与格式化在返回给前端的数据中经常需要对手机号、身份证号、邮箱等敏感信息进行脱敏。可以在DAO层或Service层的方法上使用DataMask注解在返回通知AfterReturning中对结果对象进行递归扫描根据注解规则对字段进行脱敏处理如138****1234。6.7 参数校验与预处理虽然JSR-303的Valid通常在Controller层使用但有时我们希望在Service层也进行参数校验。可以自定义一个ServiceValid注解在切面中使用Hibernate Validator对方法的参数进行校验如果校验失败则抛出统一的异常。7. 常见问题排查与调试技巧即使理解了所有概念在实际开发中你还是会遇到各种奇怪的问题。这里记录一些我踩过的坑和解决方法。问题1切面不生效检查点1Bean是否被Spring管理切面类本身必须是一个Spring Bean被Component,Service等注解标记。同时目标对象也必须是由Spring容器管理的Bean。如果你用new关键字创建的对象AOP是无效的。检查点2切入点表达式是否正确这是最常见的原因。使用调试技巧在切面方法里打印joinPoint.getSignature()和joinPoint.getTarget().getClass()看看是否真的匹配到了你期望的方法和目标类。检查点3配置是否开启在Spring Boot中只要引入了spring-boot-starter-aop依赖AOP默认是开启的。但在纯Spring项目中需要在配置类上添加EnableAspectJAutoProxy。检查点4是否存在自调用问题参考5.4节。问题2事务不生效或回滚异常检查点1异常类型是否正确Transactional默认只对RuntimeException和Error回滚对受检异常Exception不回滚。如果需要使用Transactional(rollbackFor Exception.class)。检查点2方法修饰符是否为publicTransactional注解在非public方法上无效因为Spring AOP基于代理无法代理非public方法。检查点3是否在同一个类中自调用事务也是基于AOP的同样存在自调用失效问题。检查点4数据库引擎是否支持事务如MySQL的MyISAM引擎不支持事务。问题3循环依赖因AOP而加剧Spring Bean的循环依赖本身就是一个复杂问题。当Bean被AOP代理后问题可能变得更隐蔽。因为Spring解决循环依赖是通过“提前暴露早期引用”实现的而代理对象的创建时机可能导致依赖注入的不是最终代理对象。通常的解决方法是重构代码消除循环依赖或者使用Lazy注解进行懒加载。调试技巧查看代理类在调试时你可以打印注入的Bean的类名Autowired private UserService userService; PostConstruct public void init() { System.out.println(userService class: userService.getClass().getName()); }如果输出是com.sun.proxy.$ProxyXX说明是JDK动态代理如果输出是com.example.service.UserServiceImpl$$EnhancerBySpringCGLIB$$...说明是CGLIB代理。这能帮你确认AOP是否生效以及使用了哪种代理方式。性能分析工具如果你怀疑AOP带来了性能问题可以使用Arthas、JProfiler等工具进行方法追踪查看代理拦截带来的额外耗时。在绝大多数情况下这个开销是完全可以接受的。真正的性能瓶颈往往出现在数据库查询、远程调用或算法复杂度上而不是AOP这一层薄薄的代理。
返回列表