An AOP Example

现在,您已经了解了所有组成部分的工作原理,我们可以将它们组合在一起,以执行一些有用的操作。

Now that you have seen how all the constituent parts work, we can put them together to do something useful.

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

The execution of business services can sometimes fail due to concurrency issues (for example, a deadlock loser). If the operation is retried, it is likely to succeed on the next try. For business services where it is appropriate to retry in such conditions (idempotent operations that do not need to go back to the user for conflict resolution), we want to transparently retry the operation to avoid the client seeing a PessimisticLockingFailureException. This is a requirement that clearly cuts across multiple services in the service layer and, hence, is ideal for implementing through an aspect.

由于希望重试操作,因此需要使用建议,以便我们可以多次调用 proceed。以下列表显示了基本方面实现:

Because we want to retry the operation, we need to use around advice so that we can call proceed multiple times. The following listing shows the basic aspect implementation:

  • Java

  • Kotlin

@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?
		do {
			numAttempts++
			try {
				return pjp.proceed()
			} catch (ex: PessimisticLockingFailureException) {
				lockFailureException = ex
			}
		} while (numAttempts <= this.maxRetries)
		throw lockFailureException!!
	}

@Around("com.xyz.CommonPointcuts.businessService()") 引用 Sharing Named Pointcut Definitions 中定义的 businessService 命名切点。

@Around("com.xyz.CommonPointcuts.businessService()") references the businessService named pointcut defined in Sharing Named Pointcut Definitions.

请注意,该方面实现了 Ordered 接口,这样我们可以将方面的优先级设置得高于事务建议(我们每次都想进行新的事务)。maxRetriesorder 属性均由 Spring 进行配置。主要操作在 around 建议 doConcurrentOperation 中发生。请注意,就目前而言,我们会将重试逻辑应用到每个 businessService。我们尝试继续,如果出现 PessimisticLockingFailureException 失败,我们会再次尝试,除非我们已用尽所有重试尝试。

Note that the aspect implements the Ordered interface so that we can set the precedence of the aspect higher than the transaction advice (we want a fresh transaction each time we retry). The maxRetries and order properties are both configured by Spring. The main action happens in the doConcurrentOperation around advice. Notice that, for the moment, we apply the retry logic to each businessService. We try to proceed, and if we fail with a PessimisticLockingFailureException, we try again, unless we have exhausted all of our retry attempts.

相应的 Spring 配置如下:

The corresponding Spring configuration follows:

  • Java

  • Kotlin

  • Xml

@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 批注:

To refine the aspect so that it retries only idempotent operations, we might define the following Idempotent annotation:

  • Java

  • Kotlin

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

然后,我们可以使用该批注为服务操作实现做批注。对该方面进行更改以仅重试幂等操作涉及改进切入点表达式,以便仅匹配 @Idempotent 操作,如下所示:

We can then use the annotation to annotate the implementation of service operations. The change to the aspect to retry only idempotent operations involves refining the pointcut expression so that only @Idempotent operations match, as follows:

  • Java

  • Kotlin

@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)
}