Spring AOP-正确配置重试建议 [英] Spring AOP - Properly Configuring Retry Advice
问题描述
我是Spring AOP的新手,已经做了一些实验.
I am new to Spring AOP and have been experimenting a bit.
我正在尝试设置Retry&我的一个项目通过Spring AOP的Rate Limiter. 用例是这样的:-
I am trying to setup Retry & Rate Limiter through Spring AOP for one of my project. The use case is like this:-
- 检查TPS是否可用.如果不是,则抛出
ThrottledException
- 如果抛出
ThrottledException
,则Retry
.
- Check if TPS is available. If not, throw
ThrottledException
- If a
ThrottledException
is thrown,Retry
.
我遇到的问题是:这种限制和限制.重试组合运行到无限循环(如果TPS = 0).也就是说,尝试"x"次后重试不会停止.
The issue I am running into is: This throttling & retry combo is running into an infinite loop (if TPS = 0). That is, retry is not stopping after 'x' attempts.
我的节流拦截器(在较高级别上)是这样的:
My Throttling Interceptor is (at a high level) like this:
@Before("<pointcut>")
public void invoke() throws ThrottlingException {
if (throttler.isThrottled(throttleKey)) {
throw new ThrottlingException("Call Throttled");
}
}
我的重试拦截器是这样的:
My Retry Interceptor is like this:
@AfterThrowing(pointcut="execution(* com.company.xyz.method())", throwing="exception")
public Object invoke(JoinPoint jp, ThrottlingException exception) throws Throwable {
return RetryingCallable.newRetryingCallable(new Callable<Object>() {
@Override
public Object call() throws Exception {
MethodSignature signature = (MethodSignature) p.getSignature();
Method method = signature.getMethod();
return method.invoke(jp.getThis(), (Object[]) null);
}
}, retryPolicy).call();
}
这里RetryingCallable
是一个简单的实现(由我公司的某人编写的内部库),它接受一个RetryAdvice
并将其应用.
Here RetryingCallable
is a simple implementation (internal library written by someone in my company) that takes in a RetryAdvice
and applies that.
我相关的spring-config如下:
My relevant spring-config is as follows:
<bean id="retryInterceptor" class="com.company.xyz.RetryInterceptor">
<constructor-arg index="0"><ref bean="retryPolicy"/></constructor-arg>
</bean>
<bean id="throttlingInterceptor" class="com.company.xyz.ThrottlingInterceptor">
<constructor-arg><value>throttleKey</value></constructor-arg>
</bean>
<context:component-scan base-package="com.company.xyz">
<context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>
</context:component-scan>
<aop:aspectj-autoproxy/>
据我所见,这里的问题是,在每个ThrottlingException
上都应用了新的Retry Advice
,而不是前一个生效.
The issue here, as I see, is that on each ThrottlingException
a new Retry Advice
is being applied instead of the previous one being coming into affect.
有关如何解决此问题的任何投入?
Any inputs on how to fix this?
推荐答案
免责声明:我不是Spring用户,因此我将提出一个纯粹的AspectJ解决方案这里.不过,在Spring AOP中,它应该以相同的方式工作.您唯一需要更改的是按照
Disclaimer: I am not a Spring user, thus I am going to present a pure AspectJ solution here. It should work the same way in Spring AOP though. The only thing you need to change is switch from @DeclarePresedence
to @Order
for aspect precedence configuration as described in the Spring AOP manual.
驱动程序应用程序:
package de.scrum_master.app;
public class Application {
public static void main(String[] args) {
new Application().doSomething();
}
public void doSomething() {
System.out.println("Doing something");
}
}
节流异常类:
package de.scrum_master.app;
public class ThrottlingException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ThrottlingException(String arg0) {
super(arg0);
}
}
节流拦截器:
为了模拟节流情况,我创建了一个辅助方法isThrottled()
,该方法在3种情况中有2种随机返回了true
.
In order to emulate the throttling situation I created a helper method isThrottled()
which returns true
randomly in 2 out of 3 cases.
package de.scrum_master.aspect;
import java.util.Random;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import de.scrum_master.app.ThrottlingException;
@Aspect
public class ThrottlingInterceptor {
private static final Random RANDOM = new Random();
@Before("execution(* doSomething())")
public void invoke(JoinPoint thisJoinPoint) throws ThrottlingException {
System.out.println(getClass().getSimpleName() + " -> " + thisJoinPoint);
if (isThrottled()) {
throw new ThrottlingException("call throttled");
}
}
private boolean isThrottled() {
return RANDOM.nextInt(3) > 0;
}
}
重试拦截器:
请注意,AspectJ注释@DeclarePrecedence("RetryInterceptor, *")
表示此拦截器将在其他任何拦截器之前执行.请在两个拦截器类上将其替换为@Order
批注.否则,@Around
通知将无法捕获节流拦截器引发的异常.
Please note that the AspectJ annotation @DeclarePrecedence("RetryInterceptor, *")
says that this interceptor is to be executed before any other ones. Please replace it with @Order
annotations on both interceptor classes. Otherwise the @Around
advice cannot catch exceptions thrown by the throttling interceptor.
还值得一提的是,此拦截器不需要任何反射即可实现重试逻辑,它直接在重试循环中使用连接点来重试thisJoinPoint.proceed()
.可以轻松地将其分解为实现不同种类的重试行为的辅助方法或辅助类.只需确保使用ProceedingJoinPoint
作为参数而不是Callable
.
Also worth mentioning is that this interceptor does not need any reflection in order to implement the retry logic, it directly uses the joinpoint within a retry loop in order to retry thisJoinPoint.proceed()
. This can easily be factored out into a helper method or helper class implementing different kinds of retry behaviour. Just make sure to use the ProceedingJoinPoint
as a parameter instead of a Callable
.
package de.scrum_master.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclarePrecedence;
import de.scrum_master.app.ThrottlingException;
@Aspect
@DeclarePrecedence("RetryInterceptor, *")
public class RetryInterceptor {
private static int MAX_TRIES = 5;
private static int WAIT_MILLIS_BETWEEN_TRIES = 1000;
@Around("execution(* doSomething())")
public Object invoke(ProceedingJoinPoint thisJoinPoint) throws Throwable {
System.out.println(getClass().getSimpleName() + " -> " + thisJoinPoint);
ThrottlingException throttlingException = null;
for (int i = 1; i <= MAX_TRIES; i++) {
try {
return thisJoinPoint.proceed();
}
catch (ThrottlingException e) {
throttlingException = e;
System.out.println(" Throttled during try #" + i);
Thread.sleep(WAIT_MILLIS_BETWEEN_TRIES);
}
}
throw throttlingException;
}
}
控制台日志以成功重试:
RetryInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
Throttled during try #1
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
Throttled during try #2
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
Doing something
控制台日志以重试失败:
RetryInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
Throttled during try #1
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
Throttled during try #2
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
Throttled during try #3
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
Throttled during try #4
ThrottlingInterceptor -> execution(void de.scrum_master.app.Application.doSomething())
Throttled during try #5
Exception in thread "main" de.scrum_master.app.ThrottlingException: call throttled
at de.scrum_master.aspect.ThrottlingInterceptor.invoke(ThrottlingInterceptor.aj:19)
at de.scrum_master.app.Application.doSomething_aroundBody0(Application.java:9)
at de.scrum_master.app.Application.doSomething_aroundBody1$advice(Application.java:22)
at de.scrum_master.app.Application.doSomething(Application.java:1)
at de.scrum_master.app.Application.main(Application.java:5)
可以随时提出与我的答案有关的任何后续问题.
Feel free to ask any follow-up questions related to my answer.
更新:我不知道您的RetryingCallable
和RetryPolicy
类/接口如何工作,您没有告诉我太多.但是我整理了一些东西,使它像这样工作:
Update: I have no idea how your RetryingCallable
and RetryPolicy
classes/interfaces work, you did not tell me much about it. But I made up something and got it working like this:
package de.scrum_master.app;
import java.util.concurrent.Callable;
public interface RetryPolicy<V> {
V apply(Callable<V> callable) throws Exception;
}
package de.scrum_master.app;
import java.util.concurrent.Callable;
public class DefaultRetryPolicy<V> implements RetryPolicy<V> {
private static int MAX_TRIES = 5;
private static int WAIT_MILLIS_BETWEEN_TRIES = 1000;
@Override
public V apply(Callable<V> callable) throws Exception {
Exception throttlingException = null;
for (int i = 1; i <= MAX_TRIES; i++) {
try {
return callable.call();
}
catch (ThrottlingException e) {
throttlingException = e;
System.out.println(" Throttled during try #" + i);
Thread.sleep(WAIT_MILLIS_BETWEEN_TRIES);
}
}
throw throttlingException;
}
}
package de.scrum_master.app;
import java.util.concurrent.Callable;
public class RetryingCallable<V> {
private RetryPolicy<V> retryPolicy;
private Callable<V> callable;
public RetryingCallable(Callable<V> callable, RetryPolicy<V> retryPolicy) {
this.callable = callable;
this.retryPolicy = retryPolicy;
}
public static <V> RetryingCallable<V> newRetryingCallable(Callable<V> callable, RetryPolicy<V> retryPolicy) {
return new RetryingCallable<V>(callable, retryPolicy);
}
public V call() throws Exception {
return retryPolicy.apply(callable);
}
}
现在像这样更改重试拦截器:
Now change the retry interceptor like this:
package de.scrum_master.aspect;
import java.util.concurrent.Callable;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclarePrecedence;
import de.scrum_master.app.DefaultRetryPolicy;
import de.scrum_master.app.RetryPolicy;
import de.scrum_master.app.RetryingCallable;
@Aspect
@DeclarePrecedence("RetryInterceptor, *")
public class RetryInterceptor {
private RetryPolicy<Object> retryPolicy = new DefaultRetryPolicy<>();
@Around("execution(* doSomething())")
public Object invoke(ProceedingJoinPoint thisJoinPoint) throws Throwable {
System.out.println(getClass().getSimpleName() + " -> " + thisJoinPoint);
return RetryingCallable.newRetryingCallable(
new Callable<Object>() {
@Override
public Object call() throws Exception {
return thisJoinPoint.proceed();
}
},
retryPolicy
).call();
}
}
日志输出将与您之前看到的非常相似.对我来说,这很好.
The log output will be pretty much similar to what you saw before. For me this works nicely.
这篇关于Spring AOP-正确配置重试建议的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!