一. AOP 核心概念
- 作用:在不修改原始代码的前提下增强方法功能,通过代理模式实现。
- 核心术语:
- 连接点(JoinPoint):程序执行的任意位置(Spring AOP 中特指方法执行)。
- 切入点(Pointcut):匹配连接点的表达式(如
execution(* com.itheima.service.*.*(..))
)。 - 通知(Advice):增强逻辑(如前置、后置、环绕等)。
- 切面(Aspect):通知与切入点的绑定关系。
- 目标对象(Target):被代理的原始对象。
- 代理(Proxy):Spring 自动生成的增强对象。
以下将逐步讲解上述代码示例中使用 Spring AOP 统计 SQL 执行时间和接口执行时间的实现原理和代码细节。
1. 开启 AOP 功能
package com.example.demo;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration
@EnableAspectJAutoProxy
public class AspectConfig {}
@Configuration
:这是 Spring 的注解,用于将AspectConfig
类标记为配置类。配置类类似于传统的 XML 配置文件,它可以定义 Bean 以及其他配置信息。@EnableAspectJAutoProxy
:该注解用于开启 Spring AOP 的自动代理功能。开启后,Spring 会自动检测并创建代理对象,以便在目标方法执行前后插入切面逻辑。
2. 统计接口执行时间
@Aspect
:将InterfaceTimeAspect
类标记为切面类。切面类中定义了切入点和通知,用于在目标方法执行前后插入额外的逻辑。@Component
:将该类注册为 Spring 的组件,这样 Spring 容器才能管理它。@Around
:这是一个环绕通知注解,它可以在目标方法执行前后都执行额外的逻辑。"execution(* com.example.demo.controller.*.*(..))"
是切入点表达式,表示匹配com.example.demo.controller
包下的所有类的所有方法。ProceedingJoinPoint
:在环绕通知中,ProceedingJoinPoint
用于调用目标方法。pjp.proceed()
会执行目标方法,并返回目标方法的返回值。- 统计时间:在调用目标方法前后分别记录时间,计算差值得到执行时间,并使用
Logger
输出日志。
3. 统计 SQL 执行时间
package com.example.demo;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;@Aspect
@Component
public class SqlTimeAspect {private static final Logger logger = LoggerFactory.getLogger(SqlTimeAspect.class);@Around("execution(* com.example.demo.dao.*.*(..))")public Object aroundSqlMethod(ProceedingJoinPoint pjp) throws Throwable {long startTime = System.currentTimeMillis();Object result = pjp.proceed();long endTime = System.currentTimeMillis();logger.info("SQL 方法 {} 执行时间: {} ms", pjp.getSignature().toShortString(), endTime - startTime);return result;}
}
- 该类的结构和
InterfaceTimeAspect
类类似,只是切入点表达式不同。"execution(* com.example.demo.dao.*.*(..))"
表示匹配com.example.demo.dao
包下的所有类的所有方法,用于统计 SQL 方法的执行时间。
4. 示例控制器
package com.example.demo.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class DemoController {@GetMapping("/test")public String test() {try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}return "Test Success";}
}
@RestController
:将该类标记为 RESTful 风格的控制器,用于处理 HTTP 请求。@GetMapping("/test")
:定义一个 GET 请求的映射路径/test
。当客户端访问该路径时,会执行test
方法。Thread.sleep(200)
:模拟接口处理的耗时操作。
5. 示例 DAO 类
@Repository
:将该类标记为数据访问对象(DAO),Spring 会自动处理该类的异常转换。executeSql
方法:模拟 SQL 执行的耗时操作。
6. 示例服务类
package com.example.demo.service;import com.example.demo.dao.DemoDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class DemoService {@Autowiredprivate DemoDao demoDao;public void callDaoMethod() {demoDao.executeSql();}
}
@Service
:将该类标记为服务层组件,用于处理业务逻辑。@Autowired
:自动注入DemoDao
实例,实现依赖注入。callDaoMethod
方法:调用DemoDao
的executeSql
方法。
7. Spring Boot 启动类
package com.example.demo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}
}
@SpringBootApplication
:这是一个组合注解,包含了 @Configuration
、@EnableAutoConfiguration
和 @ComponentScan
,用于启动 Spring Boot 应用。
SpringApplication.run(DemoApplication.class, args)
:启动 Spring Boot 应用。
总结
通过上述代码,我们使用 Spring AOP 实现了对接口和 SQL 方法执行时间的统计。核心是定义切面类,使用 @Around
环绕通知在目标方法执行前后记录时间,并输出日志。切入点表达式用于指定要拦截的方法。这种方式可以在不修改原有业务逻辑的前提下,添加额外的统计功能。
2. AOP 配置与实现
2.1 切入点表达式
- 语法:
execution(修饰符? 返回值 包名.类名.方法名(参数) 异常?)
。 - 通配符:
*
:匹配任意符号(如* com.itheima.dao.*.*(..)
)。..
:匹配多级路径或任意参数(如com..*Service.*(..)
)。+
:匹配子类类型(如*Service+
)。
- 示例:
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
2.2 通知类型
类型 | 注解 | 执行时机 | 参数 / 返回值 |
---|---|---|---|
前置通知 | @Before | 方法执行前 | JoinPoint 获取参数 |
后置通知 | @After | 方法执行后(无论是否异常) | JoinPoint 获取参数 |
环绕通知 | @Around | 方法前后,需调用 proceed() | ProceedingJoinPoint 控制原始方法调用 |
返回后通知 | @AfterReturning | 方法正常返回后 | returning 属性获取返回值 |
异常后通知 | @AfterThrowing | 方法抛出异常后 | throwing 属性获取异常 |
查缺补漏:
- 环绕通知必须依赖
ProceedingJoinPoint
,否则无法调用原始方法。 - 返回后通知和异常后通知的参数名需与注解属性一致(如
@AfterReturning(returning = "ret")
)。
2.3 通知数据获取
- 参数:
- 非环绕通知:
JoinPoint.getArgs()
。 - 环绕通知:
ProceedingJoinPoint.getArgs()
(可修改参数)。
- 非环绕通知:
- 返回值:
- 环绕通知:
Object ret = pjp.proceed();
。 - 返回后通知:
@AfterReturning(returning = "ret")
。
- 环绕通知:
- 异常:
- 环绕通知:通过
try-catch
捕获。 - 异常后通知:
@AfterThrowing(throwing = "ex")
。
- 环绕通知:通过
3. 事务管理
3.1 核心组件
@Transactional
:标记业务方法,支持事务属性配置。PlatformTransactionManager
:事务管理器(如DataSourceTransactionManager
)。
3.2 事务属性
属性 | 说明 | 示例 |
---|---|---|
readOnly | 是否为只读事务(默认 false ) | @Transactional(readOnly = true) |
timeout | 超时时间(秒),-1 表示永不超时 | @Transactional(timeout = 5) |
isolation | 隔离级别(如 Isolation.DEFAULT 、Isolation.REPEATABLE_READ ) | @Transactional(isolation = Isolation.REPEATABLE_READ) |
propagation | 传播行为(如 REQUIRED (默认)、REQUIRES_NEW ) | @Transactional(propagation = Propagation.REQUIRES_NEW) |
rollbackFor | 指定回滚的异常类型(默认仅 RuntimeException 和 Error ) | @Transactional(rollbackFor = IOException.class) |
3.3 传播行为
类型 | 说明 | 场景 |
---|---|---|
REQUIRED | 若当前有事务则加入,否则新建(默认) | 转账操作(减钱与加钱同一事务) |
REQUIRES_NEW | 强制新建事务,与当前事务独立 | 日志记录(不随转账失败回滚) |
NESTED | 在当前事务中嵌套保存点,回滚到保存点而非完全回滚 | 部分回滚场景 |
查缺补漏:
- 事务未生效:确保方法被 Spring 管理,且未被 final/private 修饰。
- 异常回滚:默认仅回滚
RuntimeException
和Error
,需通过rollbackFor
配置其他异常。
4. 案例应用
4.1 业务层效率监控
- 实现:使用环绕通知统计万次方法执行时间。
- 关键代码:
@Around("servicePt()") public Object runSpeed(ProceedingJoinPoint pjp) throws Throwable {long start = System.currentTimeMillis();for (int i = 0; i < 10000; i++) {pjp.proceed();}System.out.println("万次执行时间: " + (System.currentTimeMillis() - start) + "ms");return null; }
4.2 密码空格处理
- 实现:通过环绕通知过滤参数中的空格。
- 关键代码:
@Around("pt()") public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {Object[] args = pjp.getArgs();for (int i = 0; i < args.length; i++) {if (args[i] instanceof String) {args[i] = ((String) args[i]).trim();}}return pjp.proceed(args); }
4.3 转账事务管理
- 实现:通过
@Transactional
确保转账原子性,结合REQUIRES_NEW
实现日志独立事务。 - 关键配置:
@Service public class AccountServiceImpl {@Transactionalpublic void transfer(String out, String in, Double money) {accountDao.outMoney(out, money);accountDao.inMoney(in, money);} }@Service public class LogServiceImpl {@Transactional(propagation = Propagation.REQUIRES_NEW)public void log(String info) {logDao.log(info);} }
5. 查缺补漏
-
AOP 代理机制:
- JDK 动态代理:基于接口,性能较高。
- CGLIB 代理:基于子类,适用于无接口的类。
- 默认选择:优先 JDK 动态代理,无接口时使用 CGLIB。
-
事务隔离级别:
READ_UNCOMMITTED
:最低级别,可能读到未提交数据。READ_COMMITTED
:解决脏读(Oracle 默认)。REPEATABLE_READ
:解决不可重复读(MySQL 默认)。SERIALIZABLE
:最高级别,完全串行化。
-
事务超时:
- 单位为秒,超时后自动回滚。
- 需根据业务复杂度合理设置(如数据库慢查询阈值)。
-
常见问题:
- AOP 不生效:检查
@EnableAspectJAutoProxy
是否开启,包扫描路径是否正确。 - 事务不回滚:确保异常类型为
RuntimeException
或通过rollbackFor
显式配置。
- AOP 不生效:检查
6. 总结
- AOP 通过代理模式实现非侵入式增强,核心是切入点与通知的绑定。
- 事务管理 通过
@Transactional
和事务管理器确保数据一致性,传播行为和属性配置是关键。 - 实践 中需结合业务场景选择合适的通知类型和事务策略,避免性能问题和数据不一致。
AOP(面向切面编程)的工作流程
AOP(面向切面编程)的工作流程可以分为以下几个关键步骤,下面为你详细介绍:
1. 定义切面和通知
- 切面(Aspect):切面是通知和切入点的结合,它定义了在哪些连接点上执行何种通知。在 Spring AOP 中,通常使用 Java 类来定义切面,并使用
@Aspect
注解标记该类。 - 通知(Advice):通知是切面在特定连接点执行的操作。Spring AOP 支持多种类型的通知,如前置通知(
@Before
)、后置通知(@After
)、环绕通知(@Around
)、返回后通知(@AfterReturning
)和异常后通知(@AfterThrowing
)。 - 示例代码:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;@Aspect
@Component
public class LoggingAspect {@Pointcut("execution(* com.example.service.*.*(..))")public void serviceMethods() {}@Before("serviceMethods()")public void beforeServiceMethod() {System.out.println("Before service method execution");}
}
在上述代码中,LoggingAspect
类是一个切面,serviceMethods()
是切入点,beforeServiceMethod()
是前置通知。
2. 定义切入点
- 切入点(Pointcut):切入点定义了哪些连接点会被拦截。连接点是程序执行过程中的某个特定位置,如方法调用、异常抛出等。在 Spring AOP 中,通常使用切入点表达式来定义切入点。
- 切入点表达式:常用的切入点表达式类型是
execution
,它可以匹配方法的执行。例如,execution(* com.example.service.*.*(..))
表示匹配com.example.service
包下所有类的所有方法。 - 示例
在上述代码中,repositoryMethods()
定义了一个切入点,匹配 com.example.repository
包下所有类的所有方法。
3. Spring 容器启动
- 创建容器:当 Spring 应用程序启动时,会创建 Spring 容器。容器负责管理所有的 Bean,包括切面和目标对象。
- 扫描组件:Spring 容器会扫描配置类或 XML 配置文件中指定的包,查找带有
@Component
、@Service
、@Repository
等注解的类,并将它们注册为 Bean。 - 开启 AOP 自动代理:需要在配置类中使用
@EnableAspectJAutoProxy
注解开启 AOP 自动代理功能,这样 Spring 容器会自动为目标对象创建代理。 - 示例代码:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration
@ComponentScan(basePackages = "com.example")
@EnableAspectJAutoProxy
public class AppConfig {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);// 其他操作context.close();}
}
在上述代码中,AppConfig
是配置类,@ComponentScan
用于扫描指定包下的组件,@EnableAspectJAutoProxy
开启 AOP 自动代理。
4. 创建代理对象
- 代理类型:Spring AOP 支持两种代理类型,JDK 动态代理和 CGLIB 代理。
- JDK 动态代理:基于接口实现,当目标对象实现了接口时,Spring 会使用 JDK 动态代理。
- CGLIB 代理:基于继承实现,当目标对象没有实现接口时,Spring 会使用 CGLIB 代理。
- 创建代理:Spring 容器在创建目标对象时,会根据切入点表达式判断是否需要为该对象创建代理。如果需要,会根据代理类型创建相应的代理对象。
- 示例:假设
UserService
是一个实现了UserServiceInterface
的类,Spring 会为UserService
创建一个 JDK 动态代理对象。
5. 目标方法调用
- 调用代理对象:当客户端调用目标对象的方法时,实际上调用的是代理对象的方法。
- 执行通知:代理对象会根据切入点表达式和通知类型,在目标方法执行前后或抛出异常时执行相应的通知。
- 示例:当调用
UserService
的addUser
方法时,代理对象会先执行LoggingAspect
中的前置通知beforeServiceMethod()
,然后再调用目标方法addUser()
。
6. 通知执行
- 前置通知(
@Before
):在目标方法执行之前执行。 - 后置通知(
@After
):在目标方法执行之后执行,无论目标方法是否抛出异常。 - 环绕通知(
@Around
):在目标方法执行前后都可以执行额外的逻辑,并且可以控制目标方法的执行。 - 返回后通知(
@AfterReturning
):在目标方法正常返回后执行。 - 异常后通知(
@AfterThrowing
):在目标方法抛出异常后执行。 - 示例代码:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;@Aspect
@Component
public class AroundLoggingAspect {@Around("execution(* com.example.service.*.*(..))")public Object aroundServiceMethod(ProceedingJoinPoint pjp) throws Throwable {System.out.println("Before method execution");Object result = pjp.proceed();System.out.println("After method execution");return result;}
}
在上述代码中,aroundServiceMethod()
是一个环绕通知,在目标方法执行前后分别输出日志。
7. 返回结果
- 目标方法返回:目标方法执行完毕后,将结果返回给代理对象。
- 代理对象返回:代理对象将结果返回给客户端。
总结
AOP 的工作流程主要包括定义切面和通知、定义切入点、Spring 容器启动、创建代理对象、目标方法调用、通知执行和返回结果等步骤。通过这些步骤,AOP 可以在不修改原有业务逻辑的前提下,在目标方法的特定位置插入额外的逻辑,实现诸如日志记录、事务管理等功能。
Spring 事务
1. 事务的基本概念
- 事务(Transaction):数据库操作的最小逻辑单元,确保一系列操作要么全部成功,要么全部失败。
- ACID 特性:
- 原子性(Atomicity):事务中的操作要么全部完成,要么全部不完成。
- 一致性(Consistency):事务执行前后,数据的完整性约束未被破坏。
- 隔离性(Isolation):多个事务并发执行时,彼此不可见(不同隔离级别有不同表现)。
- 持久性(Durability):事务提交后,数据永久保存到数据库。
2. Spring 事务管理
Spring 提供两种事务管理方式:声明式事务(推荐)和编程式事务。
2.1 声明式事务
- 通过注解实现:使用
@Transactional
注解标记业务方法。 - 优点:
- 解耦业务逻辑与事务管理。
- 无需手动编写事务提交 / 回滚代码。
- 示例:
@Service public class AccountService {@Autowiredprivate AccountDao accountDao;@Transactional // 标记事务public void transfer(String from, String to, double amount) {accountDao.withdraw(from, amount);accountDao.deposit(to, amount);} }
2.2 编程式事务
- 通过 API 手动控制:使用
TransactionTemplate
或PlatformTransactionManager
。 - 适用场景:复杂的事务控制逻辑(如嵌套事务)。
- 示例:
@Service public class OrderService {@Autowiredprivate TransactionTemplate transactionTemplate;public void processOrder() {transactionTemplate.execute(status -> {// 业务逻辑if (shouldRollback) {status.setRollbackOnly();}return null;});} }
3. 事务管理器(PlatformTransactionManager)
- 作用:协调事务的提交与回滚。
- 常用实现类:
DataSourceTransactionManager
:适用于 JDBC/MyBatis 等基于 JDBC 的持久化技术。HibernateTransactionManager
:适用于 Hibernate。
- 配置示例(Spring Boot):
@Configuration @EnableTransactionManagement public class TransactionConfig {@Beanpublic PlatformTransactionManager transactionManager(DataSource dataSource) {return new DataSourceTransactionManager(dataSource);} }
4. 事务传播行为(Propagation)
定义事务协调员对事务管理员携带事务的处理态度。
传播行为 | 说明 |
---|---|
REQUIRED | 默认值。若当前有事务则加入,否则新建事务。 |
REQUIRES_NEW | 强制新建事务,当前事务挂起。 |
NESTED | 在当前事务中嵌套保存点,回滚时仅回滚到保存点。 |
SUPPORTS | 若当前有事务则加入,否则以非事务方式执行。 |
NOT_SUPPORTED | 以非事务方式执行,挂起当前事务。 |
示例:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOperation() {// 日志记录(独立事务)
}
5. 事务隔离级别(Isolation)
定义事务之间的可见性。
隔离级别 | 说明 |
---|---|
DEFAULT | 使用数据库默认隔离级别(MySQL 默认 REPEATABLE_READ )。 |
READ_UNCOMMITTED | 允许脏读、不可重复读、幻读。 |
READ_COMMITTED | 避免脏读,但仍有不可重复读、幻读。 |
REPEATABLE_READ | 避免脏读、不可重复读,但仍有幻读。 |
SERIALIZABLE | 最高级别,完全串行化,避免所有并发问题。 |
示例:
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void queryData() {// 查询操作
}
6. 事务属性配置
timeout
:事务超时时间(秒),超时自动回滚(默认-1
表示永不超时)。readOnly
:标记为只读事务(默认false
),可优化数据库性能。rollbackFor
:指定触发回滚的异常类型(默认仅RuntimeException
和Error
)。
示例
@Transactional(timeout = 5, readOnly = true, rollbackFor = IOException.class)
public void readData() throws IOException {// 业务逻辑
}
7. 声明式事务实现步骤
- 添加依赖:
<dependency><groupId>org.springframework</groupId><artifactId>spring-tx</artifactId> </dependency>
- 启用事务管理:
@Configuration @EnableTransactionManagement public class AppConfig {// 配置事务管理器 }
- 标记业务方法:
@Service public class UserService {@Transactionalpublic void updateUser(User user) {// 业务逻辑} }
8. 注意事项
- 事务未生效场景:
- 方法非
public
。 - 方法内部调用(非通过代理对象调用)。
- 异常被捕获未抛出。
- 方法非
- 事务范围:
- 避免事务方法过大(降低并发性能)。
- 避免在事务中执行耗时操作(如文件读写)。
9. 总结
- 声明式事务是 Spring 事务管理的核心,通过
@Transactional
注解实现解耦。 - 事务管理器负责协调事务的提交与回滚。
- 传播行为和隔离级别需根据业务场景合理选择,避免数据不一致或性能问题。
Spring 事务角色详解
在 Spring 事务管理中,事务角色分为 事务管理员(Transaction Administrator) 和 事务协调员(Transaction Coordinator),二者通过协作保证数据一致性。
1. 事务管理员
- 定义:发起事务的方法,通常位于业务层(Service 层)。
- 职责:
- 开启事务。
- 控制事务的提交或回滚。
- 管理事务的传播行为和隔离级别。
- 示例:
@Service public class AccountService {@Autowiredprivate AccountDao accountDao;@Transactional // 事务管理员public void transfer(String from, String to, double amount) {accountDao.withdraw(from, amount); // 事务协调员accountDao.deposit(to, amount); // 事务协调员} }
- 关键注解:
@Transactional
标注在业务方法上。
2. 事务协调员
- 定义:加入事务管理员发起的事务的方法,通常位于数据层(DAO 层)。
- 职责:
- 参与事务管理员的事务。
- 执行具体的数据操作(如 SQL 增删改)。
- 示例:
@Repository public class AccountDao {@Transactional // 事务协调员(默认传播行为为 REQUIRED)public void withdraw(String account, double amount) {// 执行扣款 SQL} }
- 传播行为:默认使用
Propagation.REQUIRED
,即加入当前事务。
3. 核心协作流程
- 事务管理员开启事务:
- 业务层方法标注
@Transactional
,Spring 自动开启事务。
- 业务层方法标注
- 事务协调员加入事务:
- 数据层方法被调用时,若当前存在事务(管理员开启的),则加入该事务。
- 异常处理:
- 若业务层方法抛出
RuntimeException
或Error
,事务管理员触发回滚,所有协调员操作均回滚。 - 若协调员抛出受检异常且未被捕获,事务管理员也会回滚。
- 若业务层方法抛出
4. 事务传播行为与角色关系
传播行为 | 事务管理员 | 事务协调员 | 示例场景 |
---|---|---|---|
REQUIRED | 开启新事务 | 加入当前事务(默认) | 转账操作(扣款与存款同一事务) |
REQUIRES_NEW | 开启新事务 | 强制新建事务,挂起当前事务 | 日志记录(独立于主事务) |
NESTED | 开启新事务 | 在当前事务中创建保存点,回滚到保存点 | 部分回滚(如订单支付与库存扣减) |
5. 事务角色配置
- 事务管理员:
- 标注
@Transactional
在业务方法上,设置传播行为、隔离级别等。
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ) public void processOrder() {// 业务逻辑 }
- 标注
- 事务协调员:
- 数据层方法无需显式标注
@Transactional
(默认加入当前事务)。 - 如需独立事务,显式设置
@Transactional(propagation = Propagation.REQUIRES_NEW)
。
- 数据层方法无需显式标注
6. 最佳实践
- 事务应尽可能放在业务层:
- 业务层方法作为事务管理员,控制多个数据层操作的原子性。
- 避免数据层单独开启事务:
- 数据层方法默认加入业务层事务,减少事务嵌套。
- 合理选择传播行为:
- 日志记录、缓存更新等非核心操作使用
REQUIRES_NEW
,避免主事务回滚影响。
- 日志记录、缓存更新等非核心操作使用
7. 总结
- 事务管理员是事务的发起者和管理者,位于业务层。
- 事务协调员是事务的参与者,位于数据层,默认加入管理员的事务。
- 通过
@Transactional
和传播行为配置,确保复杂业务操作的数据一致性。
Spring 事务失效场景与解决方案
在 Spring 声明式事务中,@Transactional
注解可能因配置或代码逻辑问题导致事务失效。以下是常见失效场景及解决方案:
1. 方法非 public
- 原因:Spring AOP 默认仅对
public
方法应用事务。 - 示例:
@Service public class UserService {// 事务失效(非 public)@Transactionalvoid updateUser(User user) {// 业务逻辑} }
- 解决方案:将方法声明为
public
。
2. 异常被捕获未抛出
- 原因:事务默认仅对
RuntimeException
和Error
回滚,若异常被捕获且未抛出,事务不会回滚。 - 示例:
@Transactional public void transfer() {try {// 业务逻辑int i = 1 / 0; // 抛出异常} catch (Exception e) {// 异常被捕获,未抛出e.printStackTrace();} }
- 解决方案:
- 抛出
RuntimeException
或Error
。 - 使用
@Transactional(rollbackFor = Exception.class)
强制回滚所有异常。
- 抛出
3. 传播行为配置错误
- 原因:若事务协调员配置了
REQUIRES_NEW
但未正确处理异常,可能导致主事务回滚而协调员事务提交。 - 示例:
@Service public class OrderService {@Autowiredprivate PaymentService paymentService;@Transactionalpublic void createOrder() {paymentService.charge(); // 若 charge 异常且未抛出,主事务回滚,但 charge 的事务已提交} }@Service public class PaymentService {@Transactional(propagation = Propagation.REQUIRES_NEW)public void charge() {// 业务逻辑throw new RuntimeException("Payment failed");} }
- 解决方案:确保异常传播到事务管理员,或在协调员中显式回滚。
4. 类内部方法调用
- 原因:通过
this
调用类内部方法时,未经过 Spring 代理,事务不生效。 - 示例:
@Service public class UserService {public void updateUser() {this.saveUser(); // 内部调用,事务失效}@Transactionalpublic void saveUser() {// 业务逻辑} }
- 解决方案:
- 通过依赖注入自己(代理对象)。
- 使用
AopContext.currentProxy()
获取代理对象(需配置exposeProxy=true
)。
5. 未正确配置事务管理器
- 原因:未在 Spring 容器中注册
PlatformTransactionManager
。 - 示例:
@Configuration public class TransactionConfig {// 缺少 @Bean PlatformTransactionManager }
- 解决方案:
@Bean public PlatformTransactionManager transactionManager(DataSource dataSource) {return new DataSourceTransactionManager(dataSource); }
6. 注解使用错误
- 原因:错误地在接口或非 Spring 管理的类上使用
@Transactional
。 - 示例:
public interface UserService {@Transactional // 无效(接口非 Spring 组件)void updateUser(); }
- 解决方案:仅在 Spring 管理的组件(如
@Service
、@Component
)中使用@Transactional
。
7. 数据库引擎不支持事务
- 原因:例如 MySQL 的
MyISAM
引擎不支持事务。 - 解决方案:使用
InnoDB
引擎。
8. 方法使用 final
或 static
- 原因:Spring 代理无法覆盖
final
或static
方法。 - 示例:
@Service public class UserService {@Transactionalpublic final void updateUser() { // 事务失效// 业务逻辑} }
- 解决方案:移除
final
或static
修饰符。
9. 事务超时设置不合理
- 原因:事务执行时间超过
timeout
设置,自动回滚。 - 示例:
@Transactional(timeout = 1) // 1 秒超时 public void longRunningMethod() {// 执行耗时操作 }
- 解决方案:根据业务需求调整
timeout
值。
10. 事务未被 Spring 管理
- 原因:目标对象未被 Spring 容器管理(如手动 new 对象)。
- 示例:
public class UserService {@Transactionalpublic void updateUser() {// 业务逻辑} }// 手动创建对象,事务失效 UserService userService = new UserService(); userService.updateUser();
- 解决方案:通过 Spring 容器获取对象(如
@Autowired
)。
避免事务失效的最佳实践
- 方法声明为
public
。 - 避免捕获异常不抛出,或明确配置
rollbackFor
。 - 合理使用传播行为(如
REQUIRED
、REQUIRES_NEW
)。 - 通过代理对象调用方法,避免类内部直接调用。
- 正确配置事务管理器。
- 确保数据库引擎支持事务(如
InnoDB
)。 - 避免使用
final
/static
修饰事务方法。
总结
事务失效的核心原因通常是代理机制未生效、异常未正确传播或配置错误。通过遵循最佳实践和仔细检查代码逻辑,可有效避免这些问题。
1. Spring AOP 代理机制的限制
Spring 通过 AOP 实现声明式事务,默认使用 JDK 动态代理 或 CGLIB 代理:
- JDK 动态代理:
- 基于接口实现,只能代理接口中的
public
方法。 - 非
public
方法(如protected
、private
)无法被代理,事务注解失效。
- 基于接口实现,只能代理接口中的
- CGLIB 代理:
- 基于继承,可代理类的
public
方法。 - 非
public
方法(如final
、static
)无法被覆盖,事务注解失效。
- 基于继承,可代理类的
2. 为什么非 public
方法事务失效?
- 访问控制限制:
- JDK 代理只能调用接口中声明的
public
方法。 - CGLIB 代理默认仅处理
public
方法,非public
方法无法被增强。
- JDK 代理只能调用接口中声明的
- Spring 设计哲学:
- Spring 推荐面向接口编程,业务逻辑应通过接口暴露,而非直接操作实现类。
- 非
public
方法违背了这一原则,导致事务无法通过代理生效。