Spring AOP - 正确配置重试建议

时间:2016-04-23 13:10:16

标签: java spring aop spring-aop

我是Spring AOP的新手并且已经进行了一些实验。

我正在尝试设置Retry&通过Spring AOP对我的一个项目进行速率限制。 用例是这样的: -

  1. 检查TPS是否可用。如果没有,请抛出ThrottledException
  2. 如果抛出ThrottledExceptionRetry
  3. 我遇到的问题是:这个节流&重试组合正在进入无限循环(如果TPS = 0)。也就是说,尝试“x”后重试不会停止。

    我的节流拦截器(高水平)是这样的:

    @Before("<pointcut>")
    public void invoke() throws ThrottlingException {
            if (throttler.isThrottled(throttleKey)) {
                throw new ThrottlingException("Call Throttled");
        }
    }
    

    我的重试拦截器是这样的:

    @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并应用它。

    我的相关spring-config如下:

    <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,而不是之前正在生效的.properties

    有关如何解决这个问题的任何意见?

1 个答案:

答案 0 :(得分:3)

免责声明:不是一个Spring用户,因此我将在这里展示一个纯粹的AspectJ解决方案。它应该在Spring AOP中以相同的方式工作。您需要更改的唯一方法是从@DeclarePresedence切换到@Order进行方面优先配置,如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

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建议无法捕获限制拦截器抛出的异常。

另外值得一提的是,这个拦截器不需要任何反射来实现重试逻辑,它直接在重试循环中使用连接点来重试thisJoinPoint.proceed()。这可以很容易地分解为实现不同类型的重试行为的辅助方法或辅助类。只需确保使用ProceedingJoinPoint作为参数,而不是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)

随时提出与我的回答相关的任何后续问题。

更新:我不知道你的RetryingCallableRetryPolicy类/接口是如何工作的,你没有告诉我很多。但我做了一些事情并让它像这样工作:

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

现在更改重试拦截器:

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();
    }
}

日志输出与您之前看到的非常相似。对我来说这很好用。