AOP 与 Spring 事务 学习笔记 关于 AOP 和 Spring 事务,我理解如下:
什么是 AOP? AOP ,全称 Aspect - Oriented Programming ,中文译为 “面向切面编程 ”。它是一种编程范式,旨在通过分离横切关注点 (Cross - Cutting Concerns) 来提高软件系统的模块化 程度。
想象一下,在我们的应用程序中,有很多功能模块(比如用户管理、订单管理、商品管理等)。除了这些核心业务逻辑之外,还存在一些通用的、分散在各个模块中的功能,例如:
日志记录 (Logging) :记录方法的调用、参数、返回值等。
权限校验 (Authorization) :检查用户是否有权限执行某个操作。
性能监控 (Performance Monitoring) :记录方法的执行时间。
事务管理 (Transaction Management) :确保一组数据库操作要么全部成功,要么全部失败。
这些功能就像 “切面” 一样,横向地 “切割” 了我们纵向的业务模块。如果我们在每个业务模块中都重复编写这些代码,会导致代码冗余、难以维护。
AOP 的核心思想就是将这些横切关注点从业务逻辑中剥离出来,形成独立的模块(即 “切面”),然后通过某种方式(通常是动态代理)将这些切面 “织入” 到业务逻辑的特定连接点上。
核心概念:
切面 (Aspect) :一个模块,它封装了特定的横切关注点。例如,一个日志切面、一个事务切面。
通知 (Advice) :切面在特定连接点上执行的动作。常见的通知类型有:
@Before:在目标方法执行之前执行。
@After:在目标方法执行之后执行(无论成功还是失败)。
@AfterReturning:在目标方法成功执行之后执行。
@AfterThrowing:在目标方法抛出异常之后执行。
@Around:环绕目标方法执行,可以控制目标方法的执行。
连接点 (Join Point) :程序执行过程中的某个特定点,例如方法的调用、异常的抛出等。在 Spring AOP 中,连接点通常指方法的执行。
切点 (Pointcut) :一个表达式,用于匹配一个或多个连接点。它定义了通知应该在哪些连接点上执行。
目标对象 (Target Object) :被一个或多个切面所通知的对象。也就是包含主要业务逻辑的类。
织入 (Weaving) :将切面应用到目标对象并创建新的代理对象的过程。织入可以在编译时、加载时或运行时进行。Spring AOP 在运行时 进行织入。
代理 (Proxy) :AOP 框架创建的对象,用于封装目标对象并实现切面逻辑。客户端直接与代理对象交互。
AOP 的优势:
降低耦合度 :将通用功能与业务逻辑分离,使代码更加清晰和易于维护。
提高代码复用性 :可以将通用的横切关注点封装成切面,在多个地方重复使用。
增强模块化 :每个模块专注于自身的核心功能。
你们项目中有没有使用到 AOP? 回答思路:
肯定回答 :大概率是有的,因为 Spring 框架本身就大量使用了 AOP。
举例说明 :结合项目实际情况,列举 1 - 2 个具体的应用场景,并给出简单的代码示例。
突出作用 :说明 AOP 在这些场景中解决了什么问题,带来了什么好处。
回答范例: “是的,面试官,我们项目中广泛地使用了 AOP 。
最典型的应用场景有两个:
统一日志记录 :我们通过 AOP 实现了一个全局的日志切面 。这个切面会拦截所有 Service 层方法的调用,在方法执行前后记录方法的入参、出参以及执行耗时。这样做的好处是,我们不需要在每个 Service 方法中手动编写日志记录代码,大大减少了代码冗余 ,并且使得日志格式和记录时机保持一致,方便后续的问题排查和性能分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 package com.example.aop.aspect;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import java.util.Arrays;@Aspect @Component public class LogAspect { private static final Logger logger = LoggerFactory.getLogger(LogAspect.class); @Pointcut("execution(* com.example.aop.service..*.*(..))") public void serviceLayerPointcut () { } @Before("serviceLayerPointcut()") public void logBefore (JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); logger.info("===> @Before: Method [{}] execution starts, Args: {}" , methodName, Arrays.toString(args)); } @After("serviceLayerPointcut()") public void logAfter (JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); logger.info("===> @After: Method [{}] execution ends." , methodName); } @AfterReturning(pointcut = "serviceLayerPointcut()", returning = "result") public void logAfterReturning (JoinPoint joinPoint, Object result) { String methodName = joinPoint.getSignature().getName(); logger.info("===> @AfterReturning: Method [{}] executed successfully, Result: {}" , methodName, result); } @AfterThrowing(pointcut = "serviceLayerPointcut()", throwing = "exception") public void logAfterThrowing (JoinPoint joinPoint, Throwable exception) { String methodName = joinPoint.getSignature().getName(); logger.error("===> @AfterThrowing: Method [{}] execution failed, Exception: {}" , methodName, exception.getMessage()); } @Around("serviceLayerPointcut()") public Object logAround (ProceedingJoinPoint proceedingJoinPoint) throws Throwable { String methodName = proceedingJoinPoint.getSignature().getName(); Object[] args = proceedingJoinPoint.getArgs(); long startTime = System.currentTimeMillis(); logger.info("====> @Around: Method [{}] execution starts with Args: {}" , methodName, Arrays.toString(args)); Object result = null ; try { result = proceedingJoinPoint.proceed(); long endTime = System.currentTimeMillis(); logger.info("====> @Around: Method [{}] executed successfully, Result: {}, Execution time: {} ms" , methodName, result, (endTime - startTime)); } catch (Throwable throwable) { long endTime = System.currentTimeMillis(); logger.error("====> @Around: Method [{}] execution failed, Exception: {}, Execution time: {} ms" , methodName, throwable.getMessage(), (endTime - startTime)); throw throwable; } return result; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.example.aop.service;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Service;@Service public class MyService { private static final Logger logger = LoggerFactory.getLogger(MyService.class); public String performAction (String userName, int actionType) { logger.info("MyService.performAction: Executing business logic for user: {}, action: {}" , userName, actionType); if (actionType < 0 ) { throw new IllegalArgumentException ("Action type cannot be negative" ); } return "Action performed for " + userName + " with type " + actionType; } public void anotherSimpleMethod () { logger.info("MyService.anotherSimpleMethod: Executing another simple method." ); } }
要使上述 AOP 生效,还需要在 Spring 配置类中开启 AOP 支持 :
1 2 3 4 5 6 7 8 import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration @EnableAspectJAutoProxy public class AppConfig { }
声明式事务管理 :Spring 框架提供的声明式事务管理 本身就是基于 AOP 实现的。我们在 Service 层的方法上使用@Transactional注解,Spring 就会自动为这些方法应用事务管理。当方法开始执行时,AOP 会开启事务;当方法执行完毕时,根据执行结果(成功或异常)自动提交或回滚事务。这使得我们的业务代码不再需要关注事务的开启、提交和回滚等细节 ,更加专注于业务逻辑本身,提高了开发效率和代码的可读性 。
概念性代码说明 (非完整运行示例,重点在 AOP 思想) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.example.aop.service;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional; @Service public class OrderService { @Transactional public void createOrder (String product, int quantity) { System.out.println("OrderService: Attempting to create order for " + product + " with quantity " + quantity); System.out.println("OrderService: Stock checked (simulated)." ); System.out.println("OrderService: Order record created (simulated)." ); if (quantity > 100 ) { throw new RuntimeException ("Cannot order more than 100 items in a single transaction." ); } System.out.println("OrderService: Stock decreased (simulated)." ); System.out.println("OrderService: Order created successfully for " + product); } }
在这个例子中,@Transactional注解就是一个 “切点”,告诉 Spring AOP 在哪里应用事务管理的 “切面”。事务的开启、提交、回滚等逻辑就是 “通知”。
除此之外,我们可能还在权限校验 、缓存处理 等方面考虑过或使用了 AOP 的思想,将这些非业务核心但又普遍存在的功能进行模块化管理。”
Spring 中的事务是如何实现的? Spring 中的事务管理,尤其是声明式事务 ,是基于 AOP 和 ThreadLocal 实现的。
核心原理:
AOP 代理 :
当一个 Bean 被声明为需要事务管理时(例如,通过@Transactional注解),Spring 容器在创建这个 Bean 的时候,并不会直接返回原始的 Bean 实例,而是会为它创建一个代理对象 (Proxy) 。
这个代理对象包裹 (wraps) 了原始的 Bean 实例。
当客户端调用 Bean 中被@Transactional注解标记的方法时,实际上是调用了代理对象的相应方法。
事务拦截器 (TransactionInterceptor) :
代理对象的方法内部会包含一个事务拦截器 (例如 TransactionInterceptor)。这个拦截器就是 AOP 中的通知 (Advice) 。
在目标业务方法执行之前 (类似@Before通知),事务拦截器会介入。
它会检查当前是否存在事务,如果不存在,则根据@Transactional注解的配置(如传播行为、隔离级别、只读属性等)开启一个新的数据库事务 。
ThreadLocal 存储数据库连接 (Connection) :
为了保证同一个事务中的所有数据库操作都使用同一个数据库连接,Spring 会将当前事务对应的数据库连接 (Connection) 存储在 ThreadLocal 变量中 。
ThreadLocal 为每个线程都提供了一个独立的变量副本,这意味着在一个线程的执行过程中,无论调用多少个方法,只要它们在同一个事务内,就能获取到同一个数据库连接。这解决了多线程环境下事务管理的问题,保证了事务的线程安全性。
业务方法执行 :
开启事务后,代理对象会调用原始 Bean 实例 的业务方法,执行实际的业务逻辑和数据库操作。
由于数据库连接是从ThreadLocal中获取的,所以业务方法中的所有 DAO 操作都会在同一个事务上下文中执行。
事务提交或回滚 :
当业务方法执行完毕后,事务拦截器会再次介入 (类似@AfterReturning 或 @AfterThrowing通知)。
如果业务方法正常执行完成 ,没有抛出任何(未被捕获的、符合回滚规则的)异常,事务拦截器会提交事务 。
如果业务方法抛出了异常 (通常是RuntimeException或其子类,或者在@Transactional中配置了rollbackFor指定的异常),事务拦截器会回滚事务 。
关闭连接与清理 :
事务提交或回滚之后,事务拦截器会负责关闭数据库连接 (通常是将其归还给连接池),并清理 ThreadLocal 中存储的连接信息 。
总结 Spring 事务实现的关键点:
基于 AOP :通过动态代理和拦截器机制,在业务代码执行前后动态地添加事务管理逻辑。
@Transactional 注解 :提供了一种声明式的方式来定义事务的边界和属性,简化了事务管理。
PlatformTransactionManager 接口 :Spring 事务管理的核心接口,定义了事务操作(获取事务、提交、回滚)的规范。具体的实现类(如 DataSourceTransactionManager 对应 JDBC,JpaTransactionManager 对应 JPA)负责与底层数据访问技术交互。
TransactionDefinition 接口 :定义事务的属性,如传播行为 (Propagation Behavior)、隔离级别 (Isolation Level)、超时时间 (Timeout)、只读状态 (Read - only) 等。@Transactional注解的属性最终会转换为TransactionDefinition的配置。
TransactionStatus 接口 :表示当前事务的状态,可以用来控制事务的提交和回滚。
ThreadLocal :确保了在单个线程中,事务操作使用的是同一个数据库连接,从而保证了事务的原子性和一致性。
简单案例思路 (伪代码): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public interface UserService { void A () ; void B () ; } @Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override @Transactional public void A () { userDao.updateA(); userDao.updateB(); } @Override public void B () { userDao.updateC(); } }
Spring AOP 和事务管理大致流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 Proxy$UserService.A() { TransactionInterceptor interceptor = ...; Connection conn = null ; TransactionStatus status = null ; try { status = platformTransactionManager.getTransaction(transactionDefinition); conn = dataSource.getConnection(); conn.setAutoCommit(false ); ThreadLocal<Connection> currentConnection.set(conn); targetUserServiceImpl.A(); platformTransactionManager.commit(status); conn.commit(); } catch (RuntimeException e) { if (status!= null ) { platformTransactionManager.rollback(status); if (conn!= null ) conn.rollback(); } throw e; } finally { if (conn!= null ) { conn.setAutoCommit(true ); conn.close(); } ThreadLocal<Connection> currentConnection.remove(); } }