Spring Boot中使用Spring-Retry重试框架
Spring Retry提供了自动重新调用失败的操作的功能。这在错误可能是暂时的(例如瞬时网络故障)的情况下很有用。 从2.2.0版本开始,重试功能已从Spring Batch中撤出,成为一个独立的新库:Spring Retry
Maven依赖
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> <!-- also need to add Spring AOP into our project--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> </dependency>
注解使用
开启Retry功能
在启动类中使用@EnableRetry注解
package org.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.retry.annotation.EnableRetry; @SpringBootApplication @EnableRetry public class RetryApp { public static void main(String[] args) { SpringApplication.run(RetryApp.class, args); } }
注解 @Retryable
需要在重试的代码中加入重试注解 @Retryable
package org.example; import lombok.extern.slf4j.Slf4j; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Recover; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; import java.time.LocalDateTime; @Service @Slf4j public class RetryService { @Retryable(value = IllegalAccessException.class) public void service1() throws IllegalAccessException { log.info("do something... {}", LocalDateTime.now()); throw new IllegalAccessException("manual exception"); } }
默认情况下,会重试3次,间隔1秒
我们可以从注解 @Retryable
中看到
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Retryable { /** * Retry interceptor bean name to be applied for retryable method. Is mutually * exclusive with other attributes. * @return the retry interceptor bean name */ String interceptor() default ""; /** * Exception types that are retryable. Synonym for includes(). Defaults to empty (and * if excludes is also empty all exceptions are retried). * @return exception types to retry */ Class<? extends Throwable>[] value() default {}; /** * Exception types that are retryable. Defaults to empty (and if excludes is also * empty all exceptions are retried). * @return exception types to retry */ Class<? extends Throwable>[] include() default {}; /** * Exception types that are not retryable. Defaults to empty (and if includes is also * empty all exceptions are retried). * If includes is empty but excludes is not, all not excluded exceptions are retried * @return exception types not to retry */ Class<? extends Throwable>[] exclude() default {}; /** * A unique label for statistics reporting. If not provided the caller may choose to * ignore it, or provide a default. * * @return the label for the statistics */ String label() default ""; /** * Flag to say that the retry is stateful: i.e. exceptions are re-thrown, but the * retry policy is applied with the same policy to subsequent invocations with the * same arguments. If false then retryable exceptions are not re-thrown. * @return true if retry is stateful, default false */ boolean stateful() default false; /** * @return the maximum number of attempts (including the first failure), defaults to 3 */ int maxAttempts() default 3; //默认重试次数3次 /** * @return an expression evaluated to the maximum number of attempts (including the first failure), defaults to 3 * Overrides {@link #maxAttempts()}. * @since 1.2 */ String maxAttemptsExpression() default ""; /** * Specify the backoff properties for retrying this operation. The default is a * simple {@link Backoff} specification with no properties - see it's documentation * for defaults. * @return a backoff specification */ Backoff backoff() default @Backoff(); //默认的重试中的退避策略 /** * Specify an expression to be evaluated after the {@code SimpleRetryPolicy.canRetry()} * returns true - can be used to conditionally suppress the retry. Only invoked after * an exception is thrown. The root object for the evaluation is the last {@code Throwable}. * Other beans in the context can be referenced. * For example: * <pre class=code> * {@code "message.contains('you can retry this')"}. * </pre> * and * <pre class=code> * {@code "@someBean.shouldRetry(#root)"}. * </pre> * @return the expression. * @since 1.2 */ String exceptionExpression() default ""; /** * Bean names of retry listeners to use instead of default ones defined in Spring context * @return retry listeners bean names */ String[] listeners() default {}; }
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Backoff { /** * Synonym for {@link #delay()}. * * @return the delay in milliseconds (default 1000) */ long value() default 1000; //默认的重试间隔1秒 /** * A canonical backoff period. Used as an initial value in the exponential case, and * as a minimum value in the uniform case. * @return the initial or canonical backoff period in milliseconds (default 1000) */ long delay() default 0; /** * The maximimum wait (in milliseconds) between retries. If less than the * {@link #delay()} then the default of * {@value org.springframework.retry.backoff.ExponentialBackOffPolicy#DEFAULT_MAX_INTERVAL} * is applied. * * @return the maximum delay between retries (default 0 = ignored) */ long maxDelay() default 0; /** * If positive, then used as a multiplier for generating the next delay for backoff. * * @return a multiplier to use to calculate the next backoff delay (default 0 = * ignored) */ double multiplier() default 0; /** * An expression evaluating to the canonical backoff period. Used as an initial value * in the exponential case, and as a minimum value in the uniform case. Overrides * {@link #delay()}. * @return the initial or canonical backoff period in milliseconds. * @since 1.2 */ String delayExpression() default ""; /** * An expression evaluating to the maximimum wait (in milliseconds) between retries. * If less than the {@link #delay()} then the default of * {@value org.springframework.retry.backoff.ExponentialBackOffPolicy#DEFAULT_MAX_INTERVAL} * is applied. Overrides {@link #maxDelay()} * * @return the maximum delay between retries (default 0 = ignored) * @since 1.2 */ String maxDelayExpression() default ""; /** * Evaluates to a vaule used as a multiplier for generating the next delay for * backoff. Overrides {@link #multiplier()}. * * @return a multiplier expression to use to calculate the next backoff delay (default * 0 = ignored) * @since 1.2 */ String multiplierExpression() default ""; /** * In the exponential case ({@link #multiplier()} > 0) set this to true to have the * backoff delays randomized, so that the maximum delay is multiplier times the * previous delay and the distribution is uniform between the two values. * * @return the flag to signal randomization is required (default false) */ boolean random() default false; }
我们来运行测试代码
package org.example; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class RetryServiceTest { @Autowired private RetryService retryService; @Test void testService1() throws IllegalAccessException { retryService.service1(); } }
运行结果如下:
2021-01-05 19:40:41.221 INFO 3548 --- [ main] org.example.RetryService : do something... 2021-01-05T19:40:41.221763300 2021-01-05 19:40:42.224 INFO 3548 --- [ main] org.example.RetryService : do something... 2021-01-05T19:40:42.224436500 2021-01-05 19:40:43.225 INFO 3548 --- [ main] org.example.RetryService : do something... 2021-01-05T19:40:43.225189300 java.lang.IllegalAccessException: manual exception at org.example.RetryService.service1(RetryService.java:19) at org.example.RetryService$$FastClassBySpringCGLIB$$c0995ddb.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:769) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747) at org.springframework.retry.interceptor.RetryOperationsInterceptor$1.doWithRetry(RetryOperationsInterceptor.java:91) at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:287) at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:164) at org.springframework.retry.interceptor.RetryOperationsInterceptor.invoke(RetryOperationsInterceptor.java:118) at org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor.invoke(AnnotationAwareRetryOperationsInterceptor.java:153) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689) at org.example.RetryService$$EnhancerBySpringCGLIB$$499afa1d.service1(<generated>) at org.example.RetryServiceTest.testService1(RetryServiceTest.java:16) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:675) at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:125) at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:132) at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:124) at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:74) at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115) at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105) at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:104) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:62) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:43) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:35) at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104) at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:202) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMetho