目录
(5)回滚策略(rollbackFor/noRollbackFor)
在 Java 开发中,Spring 的 AOP(面向切面编程)和事务管理是提升代码模块化和数据一致性的核心技术。AOP 通过 “横向切入” 的方式解决日志、权限等跨模块问题,而事务管理则确保数据库操作的原子性、一致性。本文将从底层原理到实战应用,全面解析这两大技术,包含丰富代码示例和项目测试。
一、Spring AOP:面向切面编程的核心思想
1. 什么是 AOP?解决什么问题?
AOP(Aspect-Oriented Programming,面向切面编程)是一种通过 “横向抽取” 实现代码复用的技术。它弥补了 OOP(面向对象编程)在 “跨模块功能” 上的不足。
(1)传统 OOP 的局限性
在分层架构中,某些功能(如日志、权限校验、异常处理)会分散在各个业务方法中,导致代码冗余且难以维护。
示例:日志代码分散在业务方法中
@Service
public class UserService {
public void register(User user) {
// 日志代码(冗余)
System.out.println("开始执行register方法,参数:" + user);
// 核心业务
userDAO.save(user);
// 日志代码(冗余)
System.out.println("register方法执行结束");
}
public User query(String username) {
// 日志代码(冗余)
System.out.println("开始执行query方法,参数:" + username);
// 核心业务
User user = userDAO.findByUsername(username);
// 日志代码(冗余)
System.out.println("query方法执行结束,结果:" + user);
return user;
}
}
问题分析:
- 日志代码与业务代码混杂,违背 “单一职责原则”;
- 若需修改日志格式,需修改所有业务方法,维护成本高。
(2)AOP 的解决方案:横向抽取
AOP 将分散在各个方法中的 “跨模块功能”(如日志)抽取为独立的 “切面”,通过配置指定在哪些方法(“切入点”)的什么时机(“通知”)执行,从而实现 “业务代码与非业务代码分离”。
AOP 核心术语:
- 切面(Aspect):封装跨模块功能的类(如日志切面、权限切面);
- 切入点(Pointcut):指定切面作用于哪些方法(通过表达式匹配);
- 通知(Advice):切面的具体逻辑,包括执行时机(如方法前、方法后);
- 目标对象(Target):被切面增强的原始对象(如UserService);
- 代理对象(Proxy):AOP 生成的包含切面逻辑的对象(实际调用的对象);
- 连接点(Joinpoint):程序执行过程中可被切面拦截的点(如方法调用、异常抛出)。
AOP 执行流程:
客户端调用代理对象的方法;
代理对象先执行切面的通知逻辑(如日志);
再调用目标对象的核心业务方法;
最后返回结果给客户端。
2. AOP 的底层实现:动态代理
Spring AOP 的底层通过动态代理技术实现:在运行时动态生成目标对象的代理对象,将切面逻辑织入代理对象中。Spring 支持两种动态代理方式:
(1)JDK 动态代理(默认,针对接口)
当目标对象实现接口时,Spring 使用 JDK 的java.lang.reflect.Proxy生成代理对象,代理对象实现相同接口。
示例:手动实现 JDK 动态代理
// 1. 业务接口
public interface UserService {
void register(User user);
}
// 2. 目标对象(实现接口)
public class UserServiceImpl implements UserService {
@Override
public void register(User user) {
System.out.println("执行注册业务:" + user);
}
}
// 3. 日志切面(横切逻辑)
public class LogHandler implements InvocationHandler {
private Object target; // 目标对象
public LogHandler(Object target) {
this.target = target;
}
// 代理对象的方法执行时,会调用此方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置通知(方法执行前)
System.out.println("开始执行" + method.getName() + "方法,参数:" + Arrays.toString(args));
// 调用目标对象的方法
Object result = method.invoke(target, args);
// 后置通知(方法执行后)
System.out.println(method.getName() + "方法执行结束");
return result;
}
}
// 4. 测试JDK动态代理
public class TestProxy {
public static void main(String[] args) {
// 目标对象
UserService target = new UserServiceImpl();
// 创建代理对象(通过JDK Proxy)
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LogHandler(target)
);
// 调用代理对象的方法(会触发切面逻辑)
proxy.register(new User("test"));
// 输出:
// 开始执行register方法,参数:[User{username='test'}]
// 执行注册业务:User{username='test'}
// register方法执行结束
}
}
(2)CGLIB 动态代理(针对类)
当目标对象未实现接口时,Spring 使用 CGLIB(Code Generation Library)生成代理对象:通过继承目标类并重写方法,织入切面逻辑。
示例:CGLIB 代理的核心原理
// 1. 目标对象(未实现接口)
public class OrderService {
public void createOrder() {
System.out.println("创建订单");
}
}
// 2. CGLIB回调(类似InvocationHandler)
public class LogInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 前置通知
System.out.println("开始执行" + method.getName() + "方法");
// 调用目标对象的方法(通过CGLIB的MethodProxy)
Object result = proxy.invokeSuper(obj, args);
// 后置通知
System.out.println(method.getName() + "方法执行结束");
return result;
}
}
// 3. 测试CGLIB代理
public class TestCGLIB {
public static void main(String[] args) {
// 创建CGLIB增强器
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OrderService.class); // 设置父类(目标类)
enhancer.setCallback(new LogInterceptor()); // 设置回调
// 生成代理对象(继承OrderService)
OrderService proxy = (OrderService) enhancer.create();
// 调用代理对象的方法
proxy.createOrder();
// 输出:
// 开始执行createOrder方法
// 创建订单
// createOrder方法执行结束
}
}
(3)Spring AOP 的代理选择逻辑
- 若目标对象实现接口,默认使用JDK 动态代理;
- 若目标对象未实现接口,使用CGLIB 代理;
- 可通过配置强制使用 CGLIB(@EnableAspectJAutoProxy(proxyTargetClass = true))。
3. Spring AOP 实战:通过注解实现切面
Spring AOP 通过 AspectJ 注解简化切面开发,核心步骤:定义切面→配置切入点→编写通知。
(1)环境准备
导入 AOP 依赖(Maven):
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.20</version>
</dependency>
在配置类中启用 AOP:
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy // 启用AOP注解支持
public class SpringConfig {
}
(2)定义切面(包含切入点和通知)
// 日志切面(@Aspect标识为切面)
@Aspect
@Component // 需被Spring容器管理
public class LogAspect {
// 切入点:匹配com.example.service包下所有类的所有方法
@Pointcut("execution(* com.example.service..*.*(..))")
public void servicePointcut() {} // 切入点签名(无实际逻辑)
// 前置通知:目标方法执行前执行
@Before("servicePointcut()")
public void before(JoinPoint joinPoint) { // JoinPoint包含方法信息
String methodName = joinPoint.getSignature().getName(); // 获取方法名
Object[] args = joinPoint.getArgs(); // 获取方法参数
System.out.println("【前置通知】" + methodName + "方法开始执行,参数:" + Arrays.toString(args));
}
// 后置通知:目标方法执行后执行(无论是否异常)
@After("servicePointcut()")
public void after(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("【后置通知】" + methodName + "方法执行结束");
}
// 返回通知:目标方法正常返回后执行
@AfterReturning(value = "servicePointcut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("【返回通知】" + methodName + "方法返回结果:" + result);
}
// 异常通知:目标方法抛出异常后执行
@AfterThrowing(value = "servicePointcut()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("【异常通知】" + methodName + "方法抛出异常:" + ex.getMessage());
}
// 环绕通知:包围目标方法,可自定义执行逻辑
@Around("servicePointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().getName();
long start = System.currentTimeMillis();
// 执行目标方法(必须调用,否则目标方法不执行)
Object result = pjp.proceed();
long end = System.currentTimeMillis();
System.out.println("【环绕通知】" + methodName + "方法执行耗时:" + (end - start) + "ms");
return result;
}
}
(3)目标对象(业务类)
@Service
public class UserService {
public User register(User user) {
System.out.println("执行注册业务:" + user);
return user; // 返回用户对象
}
public void delete(String username) {
System.out.println("执行删除业务:" + username);
// 模拟异常
if ("admin".equals(username)) {
throw new RuntimeException("不允许删除管理员");
}
}
}
(4)测试 AOP 效果
public class TestAOP {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
UserService userService = context.getBean(UserService.class);
// 测试正常方法
System.out.println("=====测试register方法=====");
userService.register(new User("test"));
// 测试异常方法
System.out.println("\n=====测试delete方法=====");
try {
userService.delete("admin");
} catch (Exception e) {
// 捕获异常(不影响程序执行)
}
}
}
输出结果:
=====测试register方法=====
【前置通知】register方法开始执行,参数:[User{username='test'}]
执行注册业务:User{username='test'}
【返回通知】register方法返回结果:User{username='test'}
【后置通知】register方法执行结束
【环绕通知】register方法执行耗时:1ms
=====测试delete方法=====
【前置通知】delete方法开始执行,参数:[admin]
执行删除业务:admin
【异常通知】delete方法抛出异常:不允许删除管理员
【后置通知】delete方法执行结束
结果分析:
- 通知按@Around→@Before→目标方法→@AfterReturning/@AfterThrowing→@After的顺序执行;
- 异常通知仅在目标方法抛出异常时执行,返回通知仅在正常返回时执行。
(5)切入点表达式详解
切入点表达式用于匹配目标方法,Spring AOP 支持多种表达式,最常用的是execution():
execution(修饰符 返回值 包名.类名.方法名(参数) 异常)
示例:
- execution(* com.example.service.UserService.register(..)):匹配UserService的register方法(任意参数);
- execution(* com.example.service..*(..)):匹配com.example.service包及子包下的所有方法;
- execution(public * *(..)):匹配所有公共方法;
- execution(* *..*Service.*(..)):匹配类名以Service结尾的所有方法。
4. AOP 的实际应用场景
(1)日志记录
如上述示例,记录方法调用日志(参数、返回值、耗时)。
(2)权限校验
在方法执行前校验用户权限,无权限则抛出异常:
@Aspect
@Component
public class AuthAspect {
@Before("execution(* com.example.service..*.*(..)) && @annotation(RequireAuth)")
public void checkAuth() {
// 模拟权限校验
if (!isLogin()) {
throw new RuntimeException("未登录,无权访问");
}
}
private boolean isLogin() {
// 实际开发中从Session或Token获取登录状态
return false;
}
}
// 自定义权限注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface RequireAuth {
}
// 在需要权限的方法上标注
@Service
public class OrderService {
@RequireAuth // 需权限校验
public void pay() {
System.out.println("支付订单");
}
}
(3)异常统一处理
捕获方法抛出的异常,转换为友好提示:
@Aspect
@Component
public class ExceptionAspect {
@AfterThrowing(value = "execution(* com.example.service..*.*(..))", throwing = "ex")
public void handleException(Exception ex) {
// 记录异常日志
System.err.println("捕获异常:" + ex.getMessage());
// 可在此处转换异常类型(如将SQLException转为业务异常)
}
}
二、Spring 事务管理:保证数据一致性
在数据库操作中,事务(Transaction)是一组不可分割的操作单元,要么全部成功,要么全部失败(ACID 特性)。Spring 通过声明式事务管理,简化了传统 JDBC 事务的代码(conn.setAutoCommit(false)、commit()、rollback())。
1. 事务的 ACID 特性
- 原子性(Atomicity):事务中的操作要么全做,要么全不做(如转账时 “扣款” 和 “加款” 必须同时成功或失败);
- 一致性(Consistency):事务执行前后,数据状态保持一致(如转账前后总金额不变);
- 隔离性(Isolation):多个事务并发执行时,彼此不干扰(避免脏读、不可重复读、幻读);
- 持久性(Durability):事务提交后,数据修改永久保存(即使系统崩溃也不丢失)。
2. 传统 JDBC 事务的问题
手动管理事务需编写大量重复代码,且容易遗漏异常处理:
public void transfer(String from, String to, int amount) {
Connection conn = null;
try {
conn = JDBCUtils.getConnection();
conn.setAutoCommit(false); // 开启事务(关闭自动提交)
// 核心操作:扣款、加款
updateBalance(conn, from, -amount); // 转出账户扣款
updateBalance(conn, to, +amount); // 转入账户加款
conn.commit(); // 提交事务
} catch (SQLException e) {
if (conn != null) try { conn.rollback(); } catch (SQLException ex) {} // 回滚事务
e.printStackTrace();
} finally {
JDBCUtils.close(conn);
}
}
问题:每个事务方法都需编写try-catch、commit、rollback,代码冗余。
3. Spring 声明式事务:通过注解简化管理
Spring 声明式事务基于 AOP 实现:通过@Transactional注解标识事务方法,
容器自动织入事务管理逻辑(开启、提交、回滚),开发者无需手动操作。
4. 声明式事务的实现步骤
(1)环境准备
导入 Spring 事务和 JDBC 依赖:
<!-- Spring事务 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.20</version>
</dependency>
<!-- Spring JDBC(用于整合数据库) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.20</version>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
(2)配置数据源和事务管理器
@Configuration
@ComponentScan("com.example")
@EnableTransactionManagement // 启用声明式事务
public class SpringConfig {
// 配置数据源
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/test");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}
// 配置JdbcTemplate(简化数据库操作)
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
// 配置事务管理器(核心:负责事务的开启、提交、回滚)
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
(3)使用 @Transactional 注解标识事务方法
@Service
public class TransferService {
@Autowired
private JdbcTemplate jdbcTemplate;
// 声明式事务:该方法在事务中执行
@Transactional
public void transfer(String from, String to, int amount) {
// 1. 转出账户扣款
jdbcTemplate.update(
"UPDATE account SET balance = balance - ? WHERE username = ?",
amount, from
);
// 模拟异常(测试事务回滚)
// if (true) throw new RuntimeException("转账失败");
// 2. 转入账户加款
jdbcTemplate.update(
"UPDATE account SET balance = balance + ? WHERE username = ?",
amount, to
);
}
}
(4)测试事务效果
public class TestTransaction {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
TransferService transferService = context.getBean(TransferService.class);
// 初始化数据(假设账户初始余额均为1000)
JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class);
jdbcTemplate.update("INSERT INTO account VALUES ('张三', 1000) ON DUPLICATE KEY UPDATE balance = 1000");
jdbcTemplate.update("INSERT INTO account VALUES ('李四', 1000) ON DUPLICATE KEY UPDATE balance = 1000");
try {
// 执行转账(张三给李四转200)
transferService.transfer("张三", "李四", 200);
System.out.println("转账成功");
} catch (Exception e) {
System.out.println("转账失败:" + e.getMessage());
}
// 查询余额
int zhangsan = jdbcTemplate.queryForObject(
"SELECT balance FROM account WHERE username = '张三'", Integer.class
);
int lisi = jdbcTemplate.queryForObject(
"SELECT balance FROM account WHERE username = '李四'", Integer.class
);
System.out.println("张三余额:" + zhangsan + ",李四余额:" + lisi);
}
}
正常情况(无异常)输出:
转账成功
张三余额:800,李四余额:1200
异常情况(取消注释throw new RuntimeException)输出:
转账失败:转账失败
张三余额:1000,李四余额:1000
结果分析:
- 无异常时,事务正常提交,扣款和加款操作均生效;
- 有异常时,事务自动回滚,所有操作均不生效(余额恢复初始值)。
5. 事务属性详解
@Transactional注解支持多种属性,用于控制事务的行为:
(1)传播行为(propagation)
定义事务方法之间调用时的事务关系,常用传播行为:
- REQUIRED(默认):若当前有事务,则加入该事务;否则创建新事务;
- REQUIRES_NEW:无论当前是否有事务,都创建新事务(原事务暂停);
- SUPPORTS:若当前有事务,则加入;否则以非事务方式执行;
- NOT_SUPPORTED:以非事务方式执行,若当前有事务则暂停;
- NEVER:以非事务方式执行,若当前有事务则抛出异常;
- MANDATORY:必须在事务中执行,若当前无事务则抛出异常;
- NESTED:若当前有事务,则在嵌套事务中执行;否则创建新事务。
示例:REQUIRES_NEW的使用
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder() {
System.out.println("创建订单(开启事务)");
try {
paymentService.pay(); // 调用支付方法(新事务)
} catch (Exception e) {
// 支付失败不影响订单事务提交
System.out.println("支付失败,继续创建订单");
}
}
}
@Service
public class PaymentService {
// 无论createOrder是否有事务,pay都创建新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void pay() {
System.out.println("执行支付(新事务)");
throw new RuntimeException("支付超时"); // 仅回滚pay的事务
}
}
(2)隔离级别(isolation)
控制事务并发时的隔离程度,解决脏读、不可重复读、幻读问题:
- DEFAULT(默认):使用数据库默认隔离级别(MySQL 默认REPEATABLE_READ);
- READ_UNCOMMITTED:最低隔离级别,允许读取未提交的数据(可能脏读);
- READ_COMMITTED:只能读取已提交的数据(避免脏读);
- REPEATABLE_READ:保证多次读取同一数据结果一致(避免脏读、不可重复读);
- SERIALIZABLE:最高隔离级别,串行执行事务(避免所有并发问题,性能低)。
示例:设置隔离级别
@Transactional(isolation = Isolation.READ_COMMITTED)
public void queryBalance(String username) {
// 业务逻辑
}
(3)超时时间(timeout)
设置事务的最大执行时间(秒),超过则自动回滚:
@Transactional(timeout = 5) // 超时时间5秒
public void longTimeTask() {
// 若任务执行超过5秒,事务回滚
}
(4)只读(readOnly)
标识事务是否为只读(仅查询操作),数据库可优化性能:
@Transactional(readOnly = true) // 只读事务
public List<User> queryAll() {
return userDAO.findAll();
}
(5)回滚策略(rollbackFor/noRollbackFor)
指定哪些异常触发回滚或不回滚(默认:运行时异常回滚,受检异常不回滚):
// 仅当抛出RuntimeException或SQLException时回滚
@Transactional(rollbackFor = {RuntimeException.class, SQLException.class})
public void updateData() {
// 业务逻辑
}
// 当抛出BusinessException时不回滚
@Transactional(noRollbackFor = BusinessException.class)
public void process() {
// 业务逻辑
}
6. 事务的底层原理:AOP 的应用
Spring 事务管理基于 AOP 实现,核心是TransactionInterceptor(事务拦截器):
当调用@Transactional标注的方法时,AOP 生成代理对象;
代理对象先通过TransactionManager开启事务;
调用目标方法执行业务逻辑;
若目标方法正常返回,提交事务;
若目标方法抛出异常(符合回滚策略),回滚事务。
伪代码:事务拦截器的核心逻辑
public class TransactionInterceptor {
private PlatformTransactionManager txManager;
public Object invoke(MethodInvocation invocation) throws Throwable {
// 1. 获取事务属性(@Transactional的配置)
TransactionAttribute txAttr = getTransactionAttribute(invocation);
// 2. 开启事务
TransactionStatus status = txManager.getTransaction(txAttr);
try {
// 3. 调用目标方法
Object result = invocation.proceed();
// 4. 提交事务
txManager.commit(status);
return result;
} catch (Exception e) {
// 5. 符合回滚条件则回滚
if (txAttr.rollbackOn(e)) {
txManager.rollback(status);
} else {
txManager.commit(status); // 否则提交
}
throw e;
}
}
}
三、项目实战:整合 AOP 与事务
实现一个用户注册功能,包含:
- AOP 日志记录注册过程;
- 事务保证注册和日志表操作的一致性。
1. 数据库表设计
-- 用户表
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(50) NOT NULL
);
-- 操作日志表
CREATE TABLE operation_log (
id INT PRIMARY KEY AUTO_INCREMENT,
operation VARCHAR(100) NOT NULL,
create_time DATETIME NOT NULL
);
2. 代码实现
(1)实体类
public class User {
private Integer id;
private String username;
private String password;
// 省略getter、setter
}
(2)DAO 层
@Repository
public class UserDAO {
@Autowired
private JdbcTemplate jdbcTemplate;
public void save(User user) {
jdbcTemplate.update(
"INSERT INTO user (username, password) VALUES (?, ?)",
user.getUsername(), user.getPassword()
);
}
}
@Repository
public class LogDAO {
@Autowired
private JdbcTemplate jdbcTemplate;
public void saveLog(String operation) {
jdbcTemplate.update(
"INSERT INTO operation_log (operation, create_time) VALUES (?, NOW())",
operation
);
}
}
(3)Service 层(含事务)
@Service
public class UserService {
@Autowired
private UserDAO userDAO;
@Autowired
private LogDAO logDAO;
// 事务保证:用户注册和日志保存要么同时成功,要么同时失败
@Transactional
public void register(User user) {
// 1. 保存用户
userDAO.save(user);
// 2. 记录日志
logDAO.saveLog("用户注册:" + user.getUsername());
// 模拟异常(测试事务回滚)
// if (true) throw new RuntimeException("注册失败");
}
}
(4)AOP 日志切面
@Aspect
@Component
public class RegisterLogAspect {
@Before("execution(* com.example.service.UserService.register(..)) && args(user)")
public void beforeRegister(User user) {
System.out.println("开始注册用户:" + user.getUsername());
}
@AfterReturning("execution(* com.example.service.UserService.register(..)) && args(user)")
public void afterRegister(User user) {
System.out.println("用户注册成功:" + user.getUsername());
}
}
(5)测试类
public class TestProject {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
UserService userService = context.getBean(UserService.class);
JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class);
// 清理测试数据
jdbcTemplate.update("DELETE FROM user WHERE username = 'test'");
jdbcTemplate.update("DELETE FROM operation_log WHERE operation LIKE '用户注册:test'");
// 执行注册
User user = new User();
user.setUsername("test");
user.setPassword("123456");
try {
userService.register(user);
} catch (Exception e) {
e.printStackTrace();
}
// 验证结果
int userCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM user WHERE username = 'test'", Integer.class
);
int logCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM operation_log WHERE operation = '用户注册:test'", Integer.class
);
System.out.println("用户表记录数:" + userCount); // 1(成功)或0(失败回滚)
System.out.println("日志表记录数:" + logCount); // 1(成功)或0(失败回滚)
}
}
正常情况输出:
开始注册用户:test
用户注册成功:test
用户表记录数:1
日志表记录数:1
异常情况输出:
开始注册用户:test
java.lang.RuntimeException: 注册失败
...
用户表记录数:0
日志表记录数:0
四、总结:AOP 与事务的核心价值
- Spring AOP:通过横向抽取实现跨模块功能的复用,解决代码冗余问题,核心是动态代理技术。实际开发中常用于日志、权限、异常处理等场景。
- Spring 事务:基于 AOP 实现声明式事务管理,通过@Transactional注解简化事务操作,保证数据库操作的 ACID 特性,核心是事务管理器和传播行为。
两者结合能极大提升代码质量:AOP 让业务代码更纯净,事务保证数据一致性。理解其底层原理(动态代理、AOP 织入),能帮助更好地排查问题(如事务不生效的原因),写出更健壮的企业级应用。