文章目录
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@EnableRetrypublicclassRetryApp{publicstaticvoidmain(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@Slf4jpublicclassRetryService{@Retryable(value= IllegalAccessException.class)publicvoidservice1()throws IllegalAccessException{
log.info("do something... {}", LocalDateTime.now());thrownewIllegalAccessException("manual exception");}}
默认情况下,会重试3次,间隔1秒
我们可以从注解@Retryable
中看到
@Target({ ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interfaceRetryable{/**
* Retry interceptor bean name to be applied for retryable method. Is mutually
* exclusive with other attributes.
* @return the retry interceptor bean name
*/
Stringinterceptor()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<?extendsThrowable>[]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<?extendsThrowable>[]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<?extendsThrowable>[]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
*/
Stringlabel()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
*/booleanstateful()defaultfalse;/**
* @return the maximum number of attempts (including the first failure), defaults to 3
*/intmaxAttempts()default3;//默认重试次数3次/**
* @return an expression evaluated to the maximum number of attempts (including the first failure), defaults to 3
* Overrides {@link #maxAttempts()}.
* @since 1.2
*/
StringmaxAttemptsExpression()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
*/
Backoffbackoff()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
*/
StringexceptionExpression()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)@Documentedpublic @interfaceBackoff{/**
* Synonym for {@link #delay()}.
*
* @return the delay in milliseconds (default 1000)
*/longvalue()default1000;//默认的重试间隔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)
*/longdelay()default0;/**
* 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)
*/longmaxDelay()default0;/**
* 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)
*/doublemultiplier()default0;/**
* 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
*/
StringdelayExpression()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
*/
StringmaxDelayExpression()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
*/
StringmultiplierExpression()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)
*/booleanrandom()defaultfalse;}
我们来运行测试代码
package org.example;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTestclassRetryServiceTest{@Autowiredprivate RetryService retryService;@TestvoidtestService1()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.invokeTestMethod(TestMethodTestDescriptor.java:198)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:229)
at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:197)
at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:211)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
可以看到重新执行了3次service1()
方法,然后间隔是1秒,然后最后还是重试失败,所以抛出了异常
既然我们看到了注解@Retryable
中有这么多参数可以设置,那我们就来介绍几个常用的配置。
@Retryable(include= IllegalAccessException.class, maxAttempts=5)publicvoidservice2()throws IllegalAccessException{
log.info("do something... {}", LocalDateTime.now());thrownewIllegalAccessException("manual exception");}
首先是maxAttempts
,用于设置重试次数
2021-01-06 09:30:11.263 INFO 15612 ---[ main] org.example.RetryService:do something... 2021-01-06T09:30:11.263621900
2021-01-06 09:30:12.265 INFO 15612 ---[ main] org.example.RetryService:do something... 2021-01-06T09:30:12.265629100
2021-01-06 09:30:13.265 INFO 15612 ---[ main] org.example.RetryService:do something... 2021-01-06T09:30:13.265701
2021-01-06 09:30:14.266 INFO 15612 ---[ main] org.example.RetryService:do something... 2021-01-06T09:30:14.266705400
2021-01-06 09:30:15.266 INFO 15612 ---[ main] org.example.RetryService:do something... 2021-01-06T09:30:15.266733200
java.lang.IllegalAccessException: manual exception....
从运行结果可以看到,方法执行了5次。
下面来介绍maxAttemptsExpression
的设置
@Retryable(value= IllegalAccessException.class, maxAttemptsExpression="${maxAttempts}")publicvoidservice3()throws IllegalAccessException{
log.info("do something... {}", LocalDateTime.now());thrownewIllegalAccessException("manual exception");}
maxAttemptsExpression
则可以使用表达式,比如上述就是通过获取配置中maxAttempts的值,我们可以在application.yml设置。上述其实省略掉了SpEL表达式#{....}
,运行结果的话可以发现方法执行了4次…
maxAttempts:4
我们可以使用SpEL表达式
@Retryable(value= IllegalAccessException.class, maxAttemptsExpression="#{1+1}")publicvoidservice3_1()throws IllegalAccessException{
log.info("do something... {}", LocalDateTime.now());thrownewIllegalAccessException("manual exception");}@Retryable(value= IllegalAccessException.class, maxAttemptsExpression="#{${maxAttempts}}")//效果和上面的一样publicvoidservice3_2()throws IllegalAccessException{
log.info("do something... {}", LocalDateTime.now());thrownewIllegalAccessException("manual exception");}
接着我们下面来看看exceptionExpression
, 一样也是写SpEL表达式
@Retryable(value= IllegalAccessException.class, exceptionExpression="message.contains('test')")publicvoidservice4(String exceptionMessage)throws IllegalAccessException{
log.info("do something... {}", LocalDateTime.now());thrownewIllegalAccessException(exceptionMessage);}@Retryable(value= IllegalAccessException.class, exceptionExpression="#{message.contains('test')}")publicvoidservice4_3(String exceptionMessage)throws IllegalAccessException{
log.info("do something... {}", LocalDateTime.now());thrownewIllegalAccessException(exceptionMessage);}
上面的表达式exceptionExpression = "message.contains('test')"
的作用其实是获取到抛出来exception的message(调用了getMessage()
方法),然后判断message的内容里面是否包含了test
字符串,如果包含的话就会执行重试。所以如果调用方法的时候传入的参数exceptionMessage
中包含了test
字符串的话就会执行重试。
但这里值得注意的是, Spring Retry 1.2.5之后exceptionExpression
是可以省略掉#{...}
Since Spring Retry 1.2.5, for
exceptionExpression
, templated expressions (#{...}
) are deprecated in favor of simple expression strings (message.contains('this can be retried')
).
使用1.2.5之后的版本运行是没有问题的
<dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId><version>1.3.0</version></dependency>
但是如果使用1.2.5版本之前包括1.2.5版本的话,运行的时候会报错如下:
2021-01-06 09:52:45.209 INFO 23220 ---[ main] org.example.RetryService:do something... 2021-01-06T09:52:45.209178200
org.springframework.expression.spel.SpelEvaluationException: EL1001E: Type conversion problem, cannot convert from java.lang.String to java.lang.Boolean
at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:75)
at org.springframework.expression.common.ExpressionUtils.convertTypedValue(ExpressionUtils.java:57)
at org.springframework.expression.common.LiteralExpression.getValue(LiteralExpression.java:106)
at org.springframework.retry.policy.ExpressionRetryPolicy.canRetry(ExpressionRetryPolicy.java:113)
at org.springframework.retry.support.RetryTemplate.canRetry(RetryTemplate.java:375)
at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:304)
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:749)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
at org.example.RetryService$$EnhancerBySpringCGLIB$$d321a75e.service4(<generated>)
at org.example.RetryServiceTest.testService4_2(RetryServiceTest.java:46)
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:686)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
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:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
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:212)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:208)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:137)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:71