`ProvisionException` 被缓存并且永远不会重试构造函数代码

`ProvisionException` is cached and constructor code is never retried

我有一个 Java 使用 PlayFramework 2.6.5 和 Guice DI (libraryDependencies += guice) 构建的 Web 服务,只是在时间注入模式下。所有依赖项都是通过构造函数注入的,使用 @Inject@ImplementedBy,并且 Guice Module 是空的。

由于暂时性错误,某些依赖项可能会在构造函数中引发异常。发生这种情况时,服务会失败并显示 ProvisionException(没关系,客户端需要重试)。

我发现这些异常被缓存了,即使解决了异常的根本原因,Play 或 Guice 也从不重试实例化这些 classes,并一直抛出相同的异常,直到 web 服务重新启动。

考虑以下 class Clock 的示例,其中构造函数在午夜 (00:xx) 时失败。一旦系统时钟到达午夜,服务就无法实例化 class。当时钟到达凌晨 1 点时,同样的异常不断被抛出。此外,异常消息始终相同(在示例中,异常消息是第一次发生异常的时间)

@ImplementedBy(OddClock.class)
public interface IClock {
    //...
}

public class OddClock implements IClock {
    @Inject
    public OddClock() throws Exception {
        if (DateTime.now().hourOfDay().get() == 0) {
            throw new Exception(DateTime.now().toString());
        }
    }
}

public class TimeController {
    @Inject
    public TimeController(IClock clock) {
        this.clock = clock;
    }
}

顺便说一句,控制台应用程序中也使用了相同的代码库,它没有遇到这个问题,所以我认为 Play+Guice 集成有一些特别之处。有什么关闭异常缓存的建议吗?

抛出异常并缓存异常似乎是 Guice 中的内置行为。这也是一种公平的行为,因为 Guice 期望它创建的对象避免 IO 和其他非确定性操作。

https://github.com/google/guice/wiki/BeCarefulAboutIoInProviders

Provider doesn't define a retry-strategy. When a value is unavailable, calling get() multiple times may cause multiple failed provisions.

您可以通过更改您使用的 scope 来避免缓存,以便每次都重新创建实例。例如。使用瞬态作用域而不是单例。

在我看来,更好的解决方案是获取不可靠的对象并将其包装在另一个隐藏故障并处理重试的对象中。这样 Guice 在尝试创建可靠对象时总是会成功,并且您可以在可靠包装器中添加自己的故障处理代码。

例如重试构造的简单示例:

public class ReliableClock { 
  private Factory<Clock> clockFactory;
  private Clock internalClock;
  public ReliableClock(Factory<Clock> clockFactory) {
    this.clockFactory = clockFactory;
  }
  private synchronized Clock currentClock() throws Exception {
    if (clock == null) {
      clock = clockFactory.create() // May throw exception
    }
    return clock;
  }
  // ... methods ...
}

我找到了使 PF 的行为更加明确和可预测的解决方案。 PF 生成的路由默认缓存控制器,即使它们的实例被破坏,假设单例控制器是用户想要的。

here 所述,可以更改默认行为,在路由配置中的每个操作前添加 @

例如,之前:

GET    /test    webservice.TestController.test

之后:

GET    /test    @webservice.TestController.test

使用这种语法,控制器不是默认的单例,并且仍然可以在需要时使用 @Singleton。此外,在出现异常时不会缓存单例控制器,从而允许在不重新启动服务的情况下恢复暂时性错误。 代码副本可用 here.