Play Framework 2.5 中抽象 class 和对象的依赖注入

Dependency injection with abstract class and object in Play Framework 2.5

我正在尝试从 Play 2.4 迁移到 2.5 以避免弃用的内容。

我有一个 abstract class Microservice,我从中创建了一些对象。 Microservice class 的一些函数使用 play.api.libs.ws.WS 发出 HTTP 请求,还 play.Play.application.configuration 读取配置。

以前,我只需要一些导入,例如:

import play.api.libs.ws._
import play.api.Play.current
import play.api.libs.concurrent.Execution.Implicits.defaultContext

但是现在你should use dependency injection to use WS and also to use access the current Play application.

我有这样的东西(缩写):

abstract class Microservice(serviceName: String) {
    // ...
    protected lazy val serviceURL: String = play.Play.application.configuration.getString(s"microservice.$serviceName.url")
    // ...and functions using WS.url()...
}

对象看起来像这样(缩短):

object HelloWorldService extends Microservice("helloWorld") {
    // ...
}

不幸的是,我不明白如何将所有内容(WS、配置、ExecutionContect)放入抽象 class 中以使其工作。

我尝试将其更改为:

abstract class Microservice @Inject() (serviceName: String, ws: WSClient, configuration: play.api.Configuration)(implicit context: scala.concurrent.ExecutionContext) {
    // ...
}

但这并不能解决问题,因为现在我也必须更改对象,但我不知道如何更改。

我试着把 object 变成 @Singleton class,比如:

@Singleton
class HelloWorldService @Inject() (implicit ec: scala.concurrent.ExecutionContext) extends Microservice ("helloWorld", ws: WSClient, configuration: play.api.Configuration) { /* ... */ }

我尝试了各种组合,但我没有取得任何进展,我觉得我在这里不是在正确的轨道上。

有什么想法可以正确使用 WS 之类的东西(不使用过时的方法)而不会使事情变得如此复杂吗?

这更多地与 Guice 处理继承的方式有关,如果您不使用 Guice,您必须完全按照自己的方式进行操作,即向 superclass 声明参数并调用 super 构造函数你的 child classes。 Guice 甚至建议在 its docs:

Wherever possible, use constructor injection to create immutable objects. Immutable objects are simple, shareable, and can be composed.

Constructor injection has some limitations:

  • Subclasses must call super() with all dependencies. This makes constructor injection cumbersome, especially as the injected base class changes.

在纯 Java 中,这意味着做这样的事情:

public abstract class Base {

  private final Dependency dep;

  public Base(Dependency dep) {
    this.dep = dep;
  }
}

public class Child extends Base {

  private final AnotherDependency anotherDep;

  public Child(Dependency dep, AnotherDependency anotherDep) {
    super(dep); // guaranteeing that fields at superclass will be properly configured
    this.anotherDep = anotherDep;
  }
}

依赖项注入不会改变这一点,您只需添加注释以指示如何注入依赖项。在这种情况下,由于 Base class 是 abstract,因此无法创建 Base 的实例,我们可以跳过它,只需注释 Child class:

public abstract class Base {

  private final Dependency dep;

  public Base(Dependency dep) {
    this.dep = dep;
  }
}

public class Child extends Base {

  private final AnotherDependency anotherDep;

  @Inject
  public Child(Dependency dep, AnotherDependency anotherDep) {
    super(dep); // guaranteeing that fields at superclass will be properly configured
    this.anotherDep = anotherDep;
  }
}

翻译成 Scala,我们会有这样的东西:

abstract class Base(dep: Dependency) {
  // something else
}

class Child @Inject() (anotherDep: AnotherDependency, dep: Dependency) extends Base(dep) {
  // something else
}

现在,我们可以重写您的代码以使用这些知识并避免弃用的 API:

abstract class Microservice(serviceName: String, configuration: Configuration, ws: WSClient) {
    protected lazy val serviceURL: String = configuration.getString(s"microservice.$serviceName.url")
    // ...and functions using the injected WSClient...
}

// a class instead of an object
// annotated as a Singleton
@Singleton
class HelloWorldService(configuration: Configuration, ws: WSClient)
    extends Microservice("helloWorld", configuration, ws) {
    // ...
}

最后一点是 implicit ExecutionContext 这里我们有两个选择:

  1. 使用 default execution context,这将是 play.api.libs.concurrent.Execution.Implicits.defaultContext
  2. 使用other thread pools

这取决于您,但您可以轻松地注入一个 ActorSystem 来查找调度程序。如果您决定使用自定义线程池,您可以这样做:

abstract class Microservice(serviceName: String, configuration: Configuration, ws: WSClient, actorSystem: ActorSystem) {

    // this will be available here and at the subclass too
    implicit val executionContext = actorSystem.dispatchers.lookup("my-context")

    protected lazy val serviceURL: String = configuration.getString(s"microservice.$serviceName.url")
    // ...and functions using the injected WSClient...
}

// a class instead of an object
// annotated as a Singleton
@Singleton
class HelloWorldService(configuration: Configuration, ws: WSClient, actorSystem: ActorSystem)
    extends Microservice("helloWorld", configuration, ws, actorSystem) {
    // ...
}

如何使用HelloWorldService?

现在,为了在需要的地方正确注入 HelloWorldService 的实例,您需要了解两件事。

从哪里 HelloWorldService 获取它的依赖项?

Guice docs对此有很好的解释:

Dependency Injection

Like the factory, dependency injection is just a design pattern. The core principle is to separate behaviour from dependency resolution.

The dependency injection pattern leads to code that's modular and testable, and Guice makes it easy to write. To use Guice, we first need to tell it how to map our interfaces to their implementations. This configuration is done in a Guice module, which is any Java class that implements the Module interface.

然后,Playframework declare modules for WSClient and for Configuration. Both modules gives Guice enough information about how to build these dependencies, and there are modules to describe how to build the dependencies necessary for WSClient and Configuration. Again, Guice docs 对此有很好的解释:

With dependency injection, objects accept dependencies in their constructors. To construct an object, you first build its dependencies. But to build each dependency, you need its dependencies, and so on. So when you build an object, you really need to build an object graph.

在我们的例子中,对于 HelloWorldService,我们使用 constructor injection 使 Guice 能够 set/create 我们的 object 图表。

HelloWorldService是如何注入的?

就像 WSClient 有一个模块来描述实现如何绑定到 interface/trait,我们可以为 HelloWorldService 做同样的事情。 Play docs关于如何创建和配置模块已经有明确的解释,这里不再赘述。

但是在创建模块后,要向控制器注入 HelloWorldService,只需将其声明为依赖项即可:

class MyController @Inject() (service: Microservice) extends Controller {

    def index = Action {
        // access "service" here and do whatever you want 
    }
}

在 Scala 中,

-> 如果你不想显式地将所有注入的参数转发给基本构造函数,你可以这样做:

abstract class Base {
  val depOne: DependencyOne
  val depTwo: DependencyTwo
  // ...
}

case class Child @Inject() (param1: Int,
                            depOne: DependencyOne,
                            depTwo: DependencyTwo) extends Base {
  // ...
}