§依赖注入
依赖注入是一种广泛使用的设计模式,它有助于将组件的行为与依赖关系解析分离。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-guice 或 sse-guice 库。
§动机
依赖注入实现了几个目标
1. 它允许您轻松地为同一组件绑定不同的实现。这在测试中特别有用,您可以在其中使用模拟依赖项手动实例化组件或注入备用实现。
2. 它允许您避免全局静态状态。虽然静态工厂可以实现第一个目标,但您必须小心确保您的状态设置正确。特别是 Play 的(现在已弃用)静态 API 需要运行的应用程序,这使得测试不太灵活。并且拥有多个实例可供使用,可以并行运行测试。
该 Guice wiki 提供了一些很好的示例,更详细地解释了这一点。
§工作原理
Play 提供了许多内置组件,并在模块中声明它们,例如其 BuiltinModule。这些绑定描述了创建 Application
实例所需的一切,包括默认情况下,由路由编译器生成的路由器,该路由器将您的控制器注入构造函数。然后,这些绑定可以转换为在 Guice 和其他运行时 DI 框架中工作。
Play 团队维护 Guice 模块,该模块提供 GuiceApplicationLoader。它为 Guice 执行绑定转换,使用这些绑定创建 Guice 注入器,并从注入器请求 Application
实例。
还有一些第三方加载器为其他框架执行此操作,包括 Scaldi 和 Spring.
或者,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
。例如,这允许使用原型控制器,以及用于打破循环依赖关系的选项。
§组件生命周期
依赖注入系统管理注入组件的生命周期,根据需要创建它们并将它们注入到其他组件中。以下是组件生命周期的工作原理
- 每次需要组件时都会创建新实例。如果一个组件被使用多次,那么默认情况下,将创建该组件的多个实例。如果您只想创建一个组件的单个实例,那么您需要将其标记为 单例。
- 实例在需要时被延迟创建。如果一个组件从未被另一个组件使用,那么它根本不会被创建。这通常是您想要的。对于大多数组件来说,在需要之前创建它们毫无意义。但是,在某些情况下,您希望组件立即启动,即使它们没有被另一个组件使用。例如,您可能希望在应用程序启动时向远程系统发送消息或预热缓存。您可以使用 急切绑定 强制创建组件。
- 实例不会自动清理,除了正常的垃圾回收。当组件不再被引用时,它们将被垃圾回收,但框架不会做任何特殊的事情来关闭组件,例如调用
close
方法。但是,Play 提供了一种特殊的组件类型,称为ApplicationLifecycle
,它允许您注册组件以在 应用程序停止时关闭。
§单例
有时您可能有一个组件,它保存了一些状态,例如缓存或与外部资源的连接,或者创建组件可能很昂贵。在这些情况下,可能重要的是只有一个该组件的实例。这可以使用 @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
。
§急切绑定
在上面的代码中,每次使用EnglishHello
和GermanHello
对象时都会创建新的对象。如果您只想创建这些对象一次,也许是因为创建它们很昂贵,那么您应该使用@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"
Module
的bindings
方法接受一个 PlayEnvironment
和Configuration
。如果您想 动态配置绑定,可以访问它们。- 模块绑定支持 急切绑定。要声明一个急切绑定,请在您的
Binding
末尾添加.eagerly
。
为了最大限度地提高跨框架兼容性,请记住以下几点
- 并非所有 DI 框架都支持即时绑定。确保您的库提供的全部组件都已显式绑定。
- 尝试使绑定键保持简单 - 不同的运行时 DI 框架对键是什么以及如何使其唯一或不唯一有截然不同的看法。
§排除模块
如果您不想加载某个模块,可以通过将其附加到 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
依赖于 Bar
,Bar
依赖于 Baz
,Baz
依赖于 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")
}
}
下一步:编译时依赖注入
在此文档中发现错误?此页面的源代码可以在此处找到。阅读完文档指南后,请随时贡献拉取请求。有疑问或建议要分享?前往我们的社区论坛与社区开始对话。