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
接口,这样我们可以将方面的优先级设置得高于事务建议(我们每次都想进行新的事务)。maxRetries
和 order
属性均由 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)
}