文档

§依赖注入

依赖注入是一种广泛使用的设计模式,它有助于将组件的行为与依赖关系解析分离。Play 支持基于 JSR 330 的运行时依赖注入(在本页中描述)和 Scala 中的编译时依赖注入

运行时依赖注入之所以被称为运行时依赖注入,是因为依赖关系图是在运行时创建、连接和验证的。如果在特定组件中找不到依赖关系,您将不会在运行应用程序之前收到错误。

Play 默认情况下支持 Guice,但也可以插入其他 JSR 330 实现。 Guice wiki 是一个很好的资源,可以帮助您了解 Guice 的功能和一般情况下 DI 设计模式。

Play 的 sbt 插件默认情况下不提供任何特定的依赖注入框架。如果您想使用 Play 的 Guice 模块,请将其显式添加到您的库依赖项中,如下所示

libraryDependencies += guice	

注意: Guice 是一个 Java 库,本文档中的示例使用 Guice 的内置 Java API。如果您更喜欢 Scala DSL,您可能希望使用 scala-guicesse-guice 库。

§动机

依赖注入实现了几个目标
1. 它允许您轻松地为同一组件绑定不同的实现。这在测试中特别有用,您可以在其中使用模拟依赖项手动实例化组件或注入备用实现。
2. 它允许您避免全局静态状态。虽然静态工厂可以实现第一个目标,但您必须小心确保您的状态设置正确。特别是 Play 的(现在已弃用)静态 API 需要运行的应用程序,这使得测试不太灵活。并且拥有多个实例可供使用,可以并行运行测试。

Guice wiki 提供了一些很好的示例,更详细地解释了这一点。

§工作原理

Play 提供了许多内置组件,并在模块中声明它们,例如其 BuiltinModule。这些绑定描述了创建 Application 实例所需的一切,包括默认情况下,由路由编译器生成的路由器,该路由器将您的控制器注入构造函数。然后,这些绑定可以转换为在 Guice 和其他运行时 DI 框架中工作。

Play 团队维护 Guice 模块,该模块提供 GuiceApplicationLoader。它为 Guice 执行绑定转换,使用这些绑定创建 Guice 注入器,并从注入器请求 Application 实例。

还有一些第三方加载器为其他框架执行此操作,包括 ScaldiSpring.

或者,Play 提供了一个 BuiltInComponents 特性,允许您创建一个纯 Scala 实现,该实现将您的应用程序 在编译时 连接在一起。

我们在下面详细解释了如何自定义默认绑定和应用程序加载器。

§声明运行时 DI 依赖项

如果您有一个组件(例如控制器),并且它需要其他一些组件作为依赖项,那么可以使用 @Inject 注解来声明这一点。@Inject 注解可以在字段或构造函数上使用。我们建议您在构造函数上使用它,例如

import javax.inject._

import play.api.libs.ws._

class MyComponent @Inject() (ws: WSClient) {
  // ...
}

请注意,@Inject 注解必须位于类名之后,但在构造函数参数之前,并且必须带有括号。

此外,Guice 还附带了其他几种 注入类型,但构造函数注入通常在 Scala 中最清晰、简洁且可测试,因此我们建议使用它。

Guice 能够自动实例化任何在其构造函数上带有 @Inject 的类,而无需显式绑定它。此功能称为 即时绑定,在 Guice 文档中进行了更详细的描述。如果您需要执行更复杂的操作,可以声明自定义绑定,如下所述。

§依赖注入控制器

Play 的路由编译器会生成一个路由类,该类在构造函数中声明您的控制器为依赖项。这允许将您的控制器注入到路由器中。

在控制器名称前加上 @ 符号具有特殊含义:控制器不会直接注入,而是会注入控制器的 Provider。例如,这允许使用原型控制器,以及用于打破循环依赖关系的选项。

§组件生命周期

依赖注入系统管理注入组件的生命周期,根据需要创建它们并将它们注入到其他组件中。以下是组件生命周期的工作原理

§单例

有时您可能有一个组件,它保存了一些状态,例如缓存或与外部资源的连接,或者创建组件可能很昂贵。在这些情况下,可能重要的是只有一个该组件的实例。这可以使用 @Singleton 注解来实现

import javax.inject._

@Singleton
class CurrentSharePrice {
  @volatile private var price = 0

  def set(p: Int) = price = p
  def get         = price
}

§停止/清理

某些组件可能需要在 Play 关闭时清理,例如,停止线程池。Play 提供了一个 ApplicationLifecycle 组件,可用于注册钩子,以便在 Play 关闭时停止您的组件

import javax.inject._

import scala.concurrent.Future

import play.api.inject.ApplicationLifecycle

@Singleton
class MessageQueueConnection @Inject() (lifecycle: ApplicationLifecycle) {
  val connection = connectToMessageQueue()
  lifecycle.addStopHook { () => Future.successful(connection.stop()) }

  // ...
}

ApplicationLifecycle 会按照组件创建时的逆序停止所有组件。这意味着您依赖的任何组件仍然可以在您的组件的停止钩子中安全使用。因为您依赖它们,所以它们必须在您的组件创建之前创建,因此只有在您的组件停止后才会停止。

注意:确保所有注册停止钩子的组件都是单例非常重要。任何注册停止钩子的非单例组件都可能成为内存泄漏的来源,因为每次创建组件时都会注册一个新的停止钩子。

您也可以使用 协调关闭 实现清理逻辑。Play 在内部使用 Pekko 的协调关闭,但它也适用于用户代码。ApplicationLifecycle#stop 被实现为协调关闭任务。主要区别在于 ApplicationLifecycle#stop 按顺序运行所有停止钩子,顺序可预测,而协调关闭并行运行同一阶段的所有任务,这可能更快,但不可预测。

§提供自定义绑定

定义组件的特征并让其他类依赖该特征,而不是组件的实现,被认为是最佳实践。通过这样做,您可以注入不同的实现,例如,在测试应用程序时注入模拟实现。

在这种情况下,DI 系统需要知道哪个实现应该绑定到该特征。我们建议您声明此方法的方式取决于您是作为 Play 的最终用户编写 Play 应用程序,还是编写其他 Play 应用程序将使用的库。

§Play 应用程序

我们建议 Play 应用程序使用应用程序正在使用的 DI 框架提供的任何机制。虽然 Play 提供了绑定 API,但此 API 有些限制,并且不允许您充分利用您正在使用的框架的功能。

由于 Play 默认提供对 Guice 的支持,因此以下示例展示了如何为 Guice 提供绑定。

§绑定注解

将实现绑定到接口的最简单方法是使用 Guice @ImplementedBy 注解。例如

import com.google.inject.ImplementedBy

@ImplementedBy(classOf[EnglishHello])
trait Hello {
  def sayHello(name: String): String
}

class EnglishHello extends Hello {
  def sayHello(name: String) = "Hello " + name
}

§编程绑定

在某些更复杂的情况下,您可能希望提供更复杂的绑定,例如当您具有一个特征的多个实现时,这些实现由 @Named 注解限定。在这些情况下,您可以实现一个自定义 Guice Module

import com.google.inject.name.Names
import com.google.inject.AbstractModule

class Module extends AbstractModule {
  override def configure() = {
    bind(classOf[Hello])
      .annotatedWith(Names.named("en"))
      .to(classOf[EnglishHello])

    bind(classOf[Hello])
      .annotatedWith(Names.named("de"))
      .to(classOf[GermanHello])
  }
}

如果您将此模块命名为Module并将其放置在根包中,它将自动注册到 Play。或者,如果您想为它指定不同的名称或将其放置在不同的包中,则可以通过将它的完全限定类名附加到application.conf中的play.modules.enabled列表来将其注册到 Play。

play.modules.enabled += "modules.HelloModule"

您还可以通过将它添加到禁用模块中来禁用对根包中名为Module的模块的自动注册。

play.modules.disabled += "Module"

§可配置绑定

有时您可能希望在配置 Guice 绑定时读取 Play Configuration 或使用ClassLoader。您可以通过将它们添加到模块的构造函数中来访问这些对象。

在下面的示例中,每种语言的Hello绑定都从配置文件中读取。这允许通过在您的application.conf文件中添加新的设置来添加新的Hello绑定。

import com.google.inject.name.Names
import com.google.inject.AbstractModule
import play.api.Configuration
import play.api.Environment

class Module(environment: Environment, configuration: Configuration) extends AbstractModule {
  override def configure() = {
    // Expect configuration like:
    // hello.en = "myapp.EnglishHello"
    // hello.de = "myapp.GermanHello"
    val helloConfiguration: Configuration =
      configuration.getOptional[Configuration]("hello").getOrElse(Configuration.empty)
    val languages: Set[String] = helloConfiguration.subKeys
    // Iterate through all the languages and bind the
    // class associated with that language. Use Play's
    // ClassLoader to load the classes.
    for (l <- languages) {
      val bindingClassName: String = helloConfiguration.get[String](l)
      val bindingClass: Class[_ <: Hello] =
        environment.classLoader
          .loadClass(bindingClassName)
          .asSubclass(classOf[Hello])
      bind(classOf[Hello])
        .annotatedWith(Names.named(l))
        .to(bindingClass)
    }
  }
}

注意:在大多数情况下,如果您需要在创建组件时访问Configuration,您应该将Configuration对象注入到组件本身或组件的Provider中。然后,您可以在创建组件时读取Configuration。通常,您不需要在创建组件的绑定时读取Configuration

§急切绑定

在上面的代码中,每次使用EnglishHelloGermanHello对象时都会创建新的对象。如果您只想创建这些对象一次,也许是因为创建它们很昂贵,那么您应该使用@Singleton注解。如果您想创建它们一次,并且在应用程序启动时急切地创建它们,而不是在需要时延迟创建它们,那么您可以使用Guice 的急切单例绑定.

  import com.google.inject.name.Names
  import com.google.inject.AbstractModule

// A Module is needed to register bindings
  class Module extends AbstractModule {
    override def configure() = {
      // Bind the `Hello` interface to the `EnglishHello` implementation as eager singleton.
      bind(classOf[Hello])
        .annotatedWith(Names.named("en"))
        .to(classOf[EnglishHello])
        .asEagerSingleton()

      bind(classOf[Hello])
        .annotatedWith(Names.named("de"))
        .to(classOf[GermanHello])
        .asEagerSingleton()
    }
  }

急切单例可用于在应用程序启动时启动服务。它们通常与关闭挂钩结合使用,以便服务可以在应用程序停止时清理其资源。

  import javax.inject._

  import scala.concurrent.Future

  import play.api.inject.ApplicationLifecycle

// This creates an `ApplicationStart` object once at start-up and registers hook for shut-down.
  @Singleton
  class ApplicationStart @Inject() (lifecycle: ApplicationLifecycle) {
    // Shut-down hook
    lifecycle.addStopHook { () => Future.successful(()) }
    // ...
  }
import com.google.inject.AbstractModule

class StartModule extends AbstractModule {
  override def configure() = {
    bind(classOf[ApplicationStart]).asEagerSingleton()
  }
}

§Play 库

如果您正在为 Play 实现一个库,那么您可能希望它与 DI 框架无关,这样您的库就可以在应用程序中使用任何 DI 框架的情况下开箱即用。为此,Play 提供了一个轻量级的绑定 API,用于以与 DI 框架无关的方式提供绑定。

要提供绑定,请实现一个 Module 来返回要提供的绑定的序列。Module 特性还提供了一个 DSL 用于构建绑定

import play.api.inject._
import play.api.Configuration
import play.api.Environment

class HelloModule extends Module {
  def bindings(environment: Environment, configuration: Configuration): Seq[play.api.inject.Binding[_]] = Seq(
    bind[Hello].qualifiedWith("en").to[EnglishHello],
    bind[Hello].qualifiedWith("de").to[GermanHello]
  )
}

可以通过将此模块附加到 reference.conf 中的 play.modules.enabled 列表来自动将其注册到 Play 中

play.modules.enabled += "com.example.HelloModule"

为了最大限度地提高跨框架兼容性,请记住以下几点

§排除模块

如果您不想加载某个模块,可以通过将其附加到 application.conf 中的 play.modules.disabled 属性来排除它

play.modules.disabled += "play.api.db.evolutions.EvolutionsModule"

§管理循环依赖

循环依赖发生在您的某个组件依赖于另一个依赖于原始组件的组件时(无论是直接还是间接)。例如

import javax.inject.Inject

class Foo @Inject() (bar: Bar)
class Bar @Inject() (baz: Baz)
class Baz @Inject() (foo: Foo)

在这种情况下,Foo 依赖于 BarBar 依赖于 BazBaz 依赖于 Foo。因此,您将无法实例化这些类中的任何一个。您可以使用 Provider 来解决此问题

import javax.inject.Inject
import javax.inject.Provider

class Foo @Inject() (bar: Bar)
class Bar @Inject() (baz: Baz)
class Baz @Inject() (foo: Provider[Foo])

通常,可以通过更原子地分解组件或找到更具体的依赖组件来解决循环依赖问题。一个常见的问题是依赖于Application。当您的组件依赖于Application时,这意味着它需要一个完整的应用程序来完成其工作;通常情况并非如此。您的依赖项应该在更具体的组件(例如Environment)上,这些组件具有您需要的特定功能。作为最后的手段,您可以通过注入Provider[Application]来解决这个问题。

§高级:扩展 GuiceApplicationLoader

Play 的运行时依赖注入由GuiceApplicationLoader 类引导。此类加载所有模块,将模块馈送到 Guice,然后使用 Guice 创建应用程序。如果您想控制 Guice 如何初始化应用程序,则可以扩展GuiceApplicationLoader 类。

您可以覆盖几种方法,但通常您需要覆盖builder 方法。此方法读取ApplicationLoader.Context 并创建一个GuiceApplicationBuilder。下面您可以看到builder 的标准实现,您可以根据需要进行更改。您可以在关于使用 Guice 进行测试 的部分中找到如何使用GuiceApplicationBuilder

import play.api.inject._
import play.api.inject.guice._
import play.api.ApplicationLoader
import play.api.Configuration

class CustomApplicationLoader extends GuiceApplicationLoader() {
  override def builder(context: ApplicationLoader.Context): GuiceApplicationBuilder = {
    val extra = Configuration("a" -> 1)
    initialBuilder
      .in(context.environment)
      .loadConfig(context.initialConfiguration.withFallback(extra))
      .overrides(overrides(context): _*)
  }
}

当您覆盖ApplicationLoader 时,您需要告诉 Play。将以下设置添加到您的application.conf

play.application.loader = "modules.CustomApplicationLoader"

您不限于使用 Guice 进行依赖注入。通过覆盖ApplicationLoader,您可以控制应用程序的初始化方式。在下一节中了解更多信息。

§在不触碰子类的情况下向类添加依赖项

有时您可能希望向可能有多个子类的某个基类添加新的依赖项。
为了避免将依赖项直接提供给它们中的每一个,您可以将其添加为可注入字段。
这种方法可能会降低类的可测试性,因此请谨慎使用。

import com.google.inject.ImplementedBy
import com.google.inject.Inject
import com.google.inject.Singleton
import play.api.mvc._

@ImplementedBy(classOf[LiveCounter])
trait Counter {
  def inc(label: String): Unit
}

object NoopCounter extends Counter {
  override def inc(label: String): Unit = ()
}

@Singleton
class LiveCounter extends Counter {
  override def inc(label: String): Unit = println(s"inc $label")
}

class BaseController extends ControllerHelpers {
  // LiveCounter will be injected
  @Inject
  @volatile protected var counter: Counter = NoopCounter

  def someBaseAction(source: String): Result = {
    counter.inc(source)
    Ok(source)
  }
}

@Singleton
class SubclassController @Inject() (action: DefaultActionBuilder) extends BaseController {
  def index = action {
    someBaseAction("index")
  }
}

下一步:编译时依赖注入


在此文档中发现错误?此页面的源代码可以在此处找到。阅读完文档指南后,请随时贡献拉取请求。有疑问或建议要分享?前往我们的社区论坛与社区开始对话。