对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

一个AOP示例

现在你已经了解了各个组成部分是如何工作的,我们可以将它们组合起来,完成一些有用的任务。spring-doc.cadn.net.cn

业务服务的执行有时会因并发问题(例如死锁导致的事务回滚)而失败。如果重试该操作,很可能在下一次尝试时成功。对于在这些情况下适合重试的业务服务(即幂等操作,且无需返回用户进行冲突解决),我们希望透明地重试该操作,以避免客户端看到 PessimisticLockingFailureException 异常。这一需求显然横切了服务层中的多个服务,因此非常适合通过切面(aspect)来实现。spring-doc.cadn.net.cn

由于我们希望重试该操作,因此需要使用环绕通知(around advice),以便可以多次调用 proceed 方法。以下代码清单展示了基本的切面实现:spring-doc.cadn.net.cn

@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()") (1)
	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;
	}
}
1 引用在共享命名切入点定义中定义的名为pointcuts.html#aop-common-pointcuts的切入点。
@Aspect
class ConcurrentOperationExecutor : Ordered {

	private val DEFAULT_MAX_RETRIES = 2
	private var maxRetries = DEFAULT_MAX_RETRIES
	private var order = 1

	fun setMaxRetries(maxRetries: Int) {
		this.maxRetries = maxRetries
	}

	override fun getOrder(): Int {
		return this.order
	}

	fun setOrder(order: Int) {
		this.order = order
	}

	@Around("com.xyz.CommonPointcuts.businessService()") (1)
	fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any? {
		var numAttempts = 0
		var lockFailureException: PessimisticLockingFailureException
		do {
			numAttempts++
			try {
				return pjp.proceed()
			} catch (ex: PessimisticLockingFailureException) {
				lockFailureException = ex
			}

		} while (numAttempts <= this.maxRetries)
		throw lockFailureException
	}
}
1 引用在共享命名切入点定义中定义的名为pointcuts.html#aop-common-pointcuts的切入点。

请注意,该切面实现了 Ordered 接口,以便我们可以将该切面的优先级设置得高于事务通知(我们希望每次重试时都开启一个全新的事务)。maxRetriesorder 属性均由 Spring 进行配置。主要逻辑发生在 doConcurrentOperation 环绕通知中。注意,目前我们将重试逻辑应用于每一个 businessService。我们尝试执行操作,如果因 PessimisticLockingFailureException 而失败,则会再次尝试,除非我们已经用尽了所有重试次数。spring-doc.cadn.net.cn

对应的 Spring 配置如下:spring-doc.cadn.net.cn

<aop:aspectj-autoproxy/>

<bean id="concurrentOperationExecutor"
		class="com.xyz.service.impl.ConcurrentOperationExecutor">
	<property name="maxRetries" value="3"/>
	<property name="order" value="100"/>
</bean>

为了细化切面,使其仅对幂等操作进行重试,我们可以定义如下 Idempotent 注解:spring-doc.cadn.net.cn

@Retention(RetentionPolicy.RUNTIME)
// marker annotation
public @interface Idempotent {
}
@Retention(AnnotationRetention.RUNTIME)
// marker annotation
annotation class Idempotent

然后,我们可以使用该注解来标注服务操作的实现。为了使切面仅对幂等操作进行重试,需要细化切入点表达式,使其仅匹配带有 @Idempotent 注解的操作,如下所示:spring-doc.cadn.net.cn

@Around("execution(* com.xyz..service.*.*(..)) && " +
		"@annotation(com.xyz.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
	// ...
}
@Around("execution(* com.xyz..service.*.*(..)) && " +
		"@annotation(com.xyz.service.Idempotent)")
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any? {
	// ...
}