Skip to content

AOP 综合示例 (An AOP Example)

既然你已经了解了所有组成部分的工作原理,我们可以将它们结合起来做一些有用的事情。

由于并发问题(例如:死锁竞争失败),业务服务的执行有时可能会失败。如果重试该操作,很可能在下一次尝试时成功。对于在这种情况下适合重试的业务服务(不需要通过用户解决冲突的幂等操作),我们希望透明地重试该操作,以避免客户端看到 PessimisticLockingFailureException。这是一个明显横跨服务层多个服务的需求,因此非常适合通过切面来实现。

因为我们需要重试操作,所以必须使用环绕通知(Around advice),以便可以多次调用 proceed。以下清单显示了基本的切面实现:

java
@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;
	}
}
kotlin
@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 接口,以便我们可以将切面的优先级设置为高于事务通知(我们希望每次重试时都有一个新的事务)。maxRetriesorder 属性都由 Spring 配置。核心逻辑发生在 doConcurrentOperation 环绕通知中。目前,我们将重试逻辑应用于每个 businessService。我们尝试执行,如果因 PessimisticLockingFailureException 失败,我们会再次尝试,除非我们已经用尽了所有重试次数。

相应的 Spring 配置如下:

java
@Configuration
@EnableAspectJAutoProxy
public class ApplicationConfiguration {

	@Bean
	public ConcurrentOperationExecutor concurrentOperationExecutor() {
		ConcurrentOperationExecutor executor = new ConcurrentOperationExecutor();
		executor.setMaxRetries(3);
		executor.setOrder(100);
		return executor;
	}
}
kotlin
@Configuration
@EnableAspectJAutoProxy
class ApplicationConfiguration {

	@Bean
	fun concurrentOperationExecutor() = ConcurrentOperationExecutor().apply {
		maxRetries = 3
		order = 100
	}
}
xml
<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 注解:

java
@Retention(RetentionPolicy.RUNTIME)
// 标记注解
public @interface Idempotent {
}
kotlin
@Retention(AnnotationRetention.RUNTIME)
// 标记注解
annotation class Idempotent

然后,我们可以使用该注解来标注服务操作的实现。修改切面以仅重试幂等操作涉及优化切入点表达式,使只有标注了 @Idempotent 的操作才会匹配,如下所示:

java
@Around("execution(* com.xyz..service.*.*(..)) && " +
		"@annotation(com.xyz.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
	// ... 逻辑同上
	return pjp.proceed(pjp.getArgs());
}
kotlin
@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 库:

java
@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 循环是标准的处理模式。

Based on Spring Framework.