AOP 综合示例 (An AOP Example)
既然你已经了解了所有组成部分的工作原理,我们可以将它们结合起来做一些有用的事情。
由于并发问题(例如:死锁竞争失败),业务服务的执行有时可能会失败。如果重试该操作,很可能在下一次尝试时成功。对于在这种情况下适合重试的业务服务(不需要通过用户解决冲突的幂等操作),我们希望透明地重试该操作,以避免客户端看到 PessimisticLockingFailureException。这是一个明显横跨服务层多个服务的需求,因此非常适合通过切面来实现。
因为我们需要重试操作,所以必须使用环绕通知(Around advice),以便可以多次调用 proceed。以下清单显示了基本的切面实现:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.CommonPointcuts.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}@Aspect
class ConcurrentOperationExecutor : Ordered {
companion object {
private const val DEFAULT_MAX_RETRIES = 2
}
var maxRetries = DEFAULT_MAX_RETRIES
private var order = 1
override fun getOrder(): Int {
return this.order
}
fun setOrder(order: Int) {
this.order = order
}
@Around("com.xyz.CommonPointcuts.businessService()")
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any? {
var numAttempts = 0
var lockFailureException: PessimisticLockingFailureException? = null
do {
numAttempts++
try {
return pjp.proceed()
} catch (ex: PessimisticLockingFailureException) {
lockFailureException = ex
}
} while (numAttempts <= this.maxRetries)
throw lockFailureException!!
}
}上面的 @Around("com.xyz.CommonPointcuts.businessService()") 引用了在共享命名的切入点定义中定义的 businessService 命名切入点。
请注意,该切面实现了 Ordered 接口,以便我们可以将切面的优先级设置为高于事务通知(我们希望每次重试时都有一个新的事务)。maxRetries 和 order 属性都由 Spring 配置。核心逻辑发生在 doConcurrentOperation 环绕通知中。目前,我们将重试逻辑应用于每个 businessService。我们尝试执行,如果因 PessimisticLockingFailureException 失败,我们会再次尝试,除非我们已经用尽了所有重试次数。
相应的 Spring 配置如下:
@Configuration
@EnableAspectJAutoProxy
public class ApplicationConfiguration {
@Bean
public ConcurrentOperationExecutor concurrentOperationExecutor() {
ConcurrentOperationExecutor executor = new ConcurrentOperationExecutor();
executor.setMaxRetries(3);
executor.setOrder(100);
return executor;
}
}@Configuration
@EnableAspectJAutoProxy
class ApplicationConfiguration {
@Bean
fun concurrentOperationExecutor() = ConcurrentOperationExecutor().apply {
maxRetries = 3
order = 100
}
}<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:aspectj-autoproxy />
<bean id="concurrentOperationExecutor"
class="com.xyz.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
</beans>为了进一步优化切面,使其仅重试幂等(Idempotent)操作,我们可以定义如下的 @Idempotent 注解:
@Retention(RetentionPolicy.RUNTIME)
// 标记注解
public @interface Idempotent {
}@Retention(AnnotationRetention.RUNTIME)
// 标记注解
annotation class Idempotent然后,我们可以使用该注解来标注服务操作的实现。修改切面以仅重试幂等操作涉及优化切入点表达式,使只有标注了 @Idempotent 的操作才会匹配,如下所示:
@Around("execution(* com.xyz..service.*.*(..)) && " +
"@annotation(com.xyz.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// ... 逻辑同上
return pjp.proceed(pjp.getArgs());
}@Around("execution(* com.xyz..service.*.*(..)) && " +
"@annotation(com.xyz.service.Idempotent)")
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any? {
// ... 逻辑同上
return pjp.proceed(pjp.args)
}补充教学
1. 为什么 Ordered 接口在这里至关重要?
在 Spring 中,事务(Transaction)本身也是通过 AOP 切面实现的。
- 重试切面 vs 事务切面:如果我们要在事务失败(如数据库死锁)时进行重试,重试必须发生在事务开启之前。
- 优先级逻辑:如果重试切面的
order值更小(优先级更高),它就会包裹住事务切面。这样当事务因为异常回滚并抛出时,重试切面才能捕获到该异常,并触发下一次新的事务尝试。 - 如果顺序反了(重试在事务内部),重试只会导致在同一个已经失败报错的事务链接中不断重复,最终还是会失败。
2. 什么是幂等性(Idempotency)?
在分布式和高并发场景下,这个概念极度重要:
- 定义:一个操作无论执行 1 次还是执行 100 次,其结果(以及对系统的副作用)都是一样的。
- 示例:
setAge(25):幂等。updateBalance(balance - 100):不幂等(执行多次会导致余额被扣多次)。
- 案例启示:我们在 AOP 示例中引入了
@Idempotent注解,就是为了确保那些敏感的、非幂等的操作不会被意外重试,从而保证数据的绝对准确性。
3. AOP 赋能:透明化处理
该案例充分展示了 AOP 的威力:
- 解耦:业务代码(Service 内部)完全不需要关心“重试”逻辑。它只需要写业务逻辑。
- 复用:一行切点配置,就能为成百上千个方法提供重试能力。
- 可插拔:如果某天不需要重试逻辑了,直接删除该切面 Bean 即可,业务代码无需修改一行。
4. 现代替代方案:Spring Retry
虽然手动写 AOP 实现重试很经典,但在现代 Spring 开发中,我们通常推荐使用专门的 Spring Retry 库:
@Service
public class MyService {
@Retryable(value = PessimisticLockingFailureException.class, maxAttempts = 3)
public void doSomething() {
// ...
}
}它底层同样是基于 Spring AOP,但提供了更丰富的指数退避(Backoff)、监听器和配置能力。
5. ProceedingJoinPoint.proceed() 的并发安全
切记,ProceedingJoinPoint 对象是非线程安全的。
- 但在拦截普通 Singleton 服务时,每次方法调用都会创建一个新的
JoinPoint上下文,因此在通知内部(如doConcurrentOperation方法内)的操作是线程安全的。 - 示例中的
do-while循环是标准的处理模式。