Spring 不使用原型 bean 字段重试原型 bean 实现 Runnable 的 @Recover 方法

Spring Retry @Recover method for prototype-bean implementing Runnable not using field of prototype-bean

我有一个实现 Runnable 的原型 bean,它必须重试其 运行 方法,并在达到最大重试上限时执行某些操作。 现在我遇到的问题是 recover 方法似乎总是从同一个 spring bean 中调用,而不是从相应的实例中调用。

到目前为止,这是我的代码:

RetryableRunnable

@Slf4j
@AllArgsConstructor
public class RetryableRunnable implements Runnable {

   private final RetryDemoService retryDemoService;
   private final Status someStatus;

   @Override
   @Retryable(
         value = { RuntimeException.class },
         maxAttempts = 2,
         backoff = @Backoff(delay = 2000))
   public void run() {
      log.info( "+++ RetryableRunnable executed! +++" );
      retryDemoService.demoRun();
   }

   @Recover
   private void recover() {
      retryDemoService.demoRecover();
      log.info( String.valueOf( someStatus ) );
   }
}

配置

@Configuration
@AllArgsConstructor
@EnableRetry(proxyTargetClass = true)
public class RetryDemoConfig {

   private final RetryDemoService retryDemoService;

   @Bean
   @Scope( "prototype" )
   public RetryableRunnable retryableRunnable(Status status) {
       return new RetryableRunnable( retryDemoService, status );
   }
}

服务

@Service
@Slf4j
public class RetryDemoService {

   void demoRun() {
      log.info( "+++ Run! +++" );
   }

   void demoRecover() {
      log.info( "+++ Recover! +++" );
   }

}

状态枚举

public enum Status {
   STATUS1, STATUS2
}

测试显示问题

@RunWith( SpringRunner.class )
@SpringBootTest
public class RetryableRunnableTest {

   @Autowired
   private BeanFactory beanFactory;

   @MockBean
   RetryDemoService retryDemoService;

   @Test
   public void retrieableRunnableIsRetriedOnlyThreeTimesAndRecoverMethodIsRun() throws InterruptedException {
      RetryableRunnable testInstance1 = beanFactory.getBean( RetryableRunnable.class, Status.STATUS1 );
      RetryableRunnable testInstance2 = beanFactory.getBean( RetryableRunnable.class, Status.STATUS2 );
      doThrow( new RuntimeException() )
          .doThrow( new RuntimeException() )
          .doThrow( new RuntimeException() )
          .when( retryDemoService ).demoRun();

      Thread thread1 = new Thread( testInstance1 );
      thread1.start();
      thread1.join();

      Thread thread2 = new Thread( testInstance2 );
      thread2.start();
      thread2.join();
    }
}

现在日志的输出是:

+++ RetryableRunnable executed! +++
+++ RetryableRunnable executed! +++
STATUS1
+++ RetryableRunnable executed! +++
+++ RetryableRunnable executed! +++
STATUS1

虽然应该是:

+++ RetryableRunnable executed! +++
+++ RetryableRunnable executed! +++
STATUS1
+++ RetryableRunnable executed! +++
+++ RetryableRunnable executed! +++
STATUS2

当我调试此测试方法时,RetryRunnable@3053 第一次和第二次调用了 recover 方法!

这是一个错误还是我没有理解一个概念?我该怎么做才能解决这个问题并调用相应的 prototype-bean "Status"-field?

当前不支持原型范围。

只有一个 AnnotationAwareRetryOperationsInterceptor 并且它根据 Method 缓存委托 RetryOperationsInterceptor,而不是对象实例...

private MethodInterceptor getDelegate(Object target, Method method) {
    if (!this.delegates.containsKey(method)) {
         ...
    }
    return this.delegates.get(method);
}

调用正确的 @Retryable 方法,但所有实例将调用第一个缓存的 @Recoverer

缓存必须更改为目标对象和 Method 组合的键。

您可以open an issue on github引用这个问题。

欢迎投稿。

这是我用来重现问题的应用...

@SpringBootApplication
@EnableRetry
public class So47513907Application {

    private static final Log log = LogFactory.getLog(So47513907Application.class);

    public static void main(String[] args) {
        SpringApplication.run(So47513907Application.class, args);
    }

    @Bean
    public ApplicationRunner runner(ApplicationContext ctx) {
        return args -> {
            Baz baz1 = ctx.getBean(Baz.class, "one");
            Baz baz2 = ctx.getBean(Baz.class, "two");
            baz1.foo();
            baz2.foo();
        };
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Baz baz(String arg) {
        return new Baz(arg);
    }

    @Service
    public static class Foo {

        void demoRun(Baz baz) {
            log.info(baz.instance + " +++ Run! +++");
            throw new RuntimeException();
        }

        void demoRecover(Baz baz) {
            log.info(baz.instance + " +++ Recover! +++");
        }

    }

    public interface Bar {

        void foo();

        void bar();

    }

    public static class Baz implements Bar {

        public String instance;

        @Autowired
        private Foo foo;

        public Baz(String instance) {
            this.instance = instance;
        }

        @Retryable
        @Override
        public void foo() {
            log.info(this.instance);
            foo.demoRun(this);
        }

        @Recover
        @Override
        public void bar() {
            log.info("recover: " + this.instance);
            foo.demoRecover(this);
        }

    }

}

编辑

最简单的解决方法是使用 RetryTemplate 而不是注释:

@Bean
public RetryTemplate retryTemplate() {
    RetryTemplate template = new RetryTemplate();
    template.setRetryPolicy(new SimpleRetryPolicy(2));
    return template;
}

public static class Baz implements Bar {

    public String instance;

    @Autowired
    private Foo foo;

    @Autowired
    private RetryTemplate retryTemplate;

    public Baz(String instance) {
        this.instance = instance;
    }

//  @Retryable
    @Override
    public void foo() {
        this.retryTemplate.execute(context -> {
            log.info(this.instance);
            foo.demoRun(this);
            return null;
        }, context -> {
            bar();
            return null;
        });
    }

//  @Recover
    @Override
    public void bar() {
        log.info("recover: " + this.instance);
        foo.demoRecover(this);
    }

}

如果需要,您可以使用重试上下文将信息从失败的方法传递给恢复器。