文档

§依赖注入

依赖注入是一种广泛使用的设计模式,它有助于将组件的行为与依赖项解析分离。组件声明其依赖项,通常作为构造函数参数,而依赖注入框架帮助您将这些组件连接在一起,因此您无需手动执行此操作。

开箱即用,Play 提供基于 JSR 330 的依赖注入支持。Play 附带的默认 JSR 330 实现是 Guice,但也可以插入其他 JSR 330 实现。要启用 Play 提供的 Guice 模块,请确保在 build.sbt 中的库依赖项中包含 guice,例如:

libraryDependencies += guice

Guice wiki 是一个很好的资源,可以帮助您了解 Guice 的功能以及一般情况下 DI 设计模式。

§动机

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

有关更详细的解释,请参阅 Guice wiki 中的一些示例。

§工作原理

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

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

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

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

§声明依赖项

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

import javax.inject.*;
import play.libs.ws.*;

public class MyComponent {
  @Inject WSClient ws;

  // ...
}

请注意,这些是实例字段。通常没有意义注入静态字段,因为它会破坏封装。

要使用构造函数注入

import javax.inject.*;
import play.libs.ws.*;

public class MyComponent {
  private final WSClient ws;

  @Inject
  public MyComponent(WSClient ws) {
    this.ws = ws;
  }

  // ...
}

字段注入更短,但我们建议在您的应用程序中使用构造函数注入。它是可测试性最高的,因为在单元测试中,您需要传递所有构造函数参数来创建类的实例,并且编译器会确保所有依赖项都存在。它也很容易理解,因为没有“神奇”的字段设置。DI 框架只是自动执行您可以手动编写的相同构造函数调用。

Guice 还有一些其他 注入类型,它们在某些情况下可能有用。如果您正在迁移使用静态变量的应用程序,您可能会发现它的静态注入支持很有用。

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

§依赖注入控制器

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

要专门启用注入的路由生成器,请在 build.sbt 中的构建设置中添加以下内容

routesGenerator := InjectedRoutesGenerator

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

§组件生命周期

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

§单例

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

import javax.inject.*;

@Singleton
public class CurrentSharePrice {
  private volatile int price;

  public void set(int p) {
    price = p;
  }

  public int get() {
    return price;
  }
}

§停止/清理

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

import java.util.concurrent.CompletableFuture;
import javax.inject.*;
import play.inject.ApplicationLifecycle;

@Singleton
public class MessageQueueConnection {
  private final MessageQueue connection;

  @Inject
  public MessageQueueConnection(ApplicationLifecycle lifecycle) {
    connection = MessageQueue.connect();

    lifecycle.addStopHook(
        () -> {
          connection.stop();
          return CompletableFuture.completedFuture(null);
        });
  }

  // ...
}

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(EnglishHello.class)
public interface Hello {

  String sayHello(String name);
}
public class EnglishHello implements Hello {

  public String sayHello(String name) {
    return "Hello " + name;
  }
}

§编程绑定

在一些更复杂的情况下,您可能希望提供更复杂的绑定,例如当您有多个接口实现时,这些实现通过 @Named 注解进行限定。在这些情况下,您可以实现一个自定义的 Guice Module

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

public class Module extends AbstractModule {
  protected void configure() {

    bind(Hello.class).annotatedWith(Names.named("en")).to(EnglishHello.class);

    bind(Hello.class).annotatedWith(Names.named("de")).to(GermanHello.class);
  }
}

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

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

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

play.modules.disabled += "Module"

§可配置绑定

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

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

import com.google.inject.AbstractModule;
import com.google.inject.name.Names;
import com.typesafe.config.Config;
import play.Environment;

public class Module extends AbstractModule {

  private final Environment environment;
  private final Config config;

  public Module(Environment environment, Config config) {
    this.environment = environment;
    this.config = config;
  }

  protected void configure() {
    // Expect configuration like:
    // hello.en = "myapp.EnglishHello"
    // hello.de = "myapp.GermanHello"
    final Config helloConf = config.getConfig("hello");
    // Iterate through all the languages and bind the
    // class associated with that language. Use Play's
    // ClassLoader to load the classes.
    helloConf
        .entrySet()
        .forEach(
            entry -> {
              try {
                String name = entry.getKey();
                Class<? extends Hello> bindingClass =
                    environment
                        .classLoader()
                        .loadClass(entry.getValue().toString())
                        .asSubclass(Hello.class);
                bind(Hello.class).annotatedWith(Names.named(name)).to(bindingClass);
              } catch (ClassNotFoundException ex) {
                throw new RuntimeException(ex);
              }
            });
  }
}

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

§急切绑定

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

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

// A Module is needed to register bindings
public class Module extends AbstractModule {
  protected void configure() {

    // Bind the `Hello` interface to the `EnglishHello` implementation as eager singleton.
    bind(Hello.class).annotatedWith(Names.named("en")).to(EnglishHello.class).asEagerSingleton();

    bind(Hello.class).annotatedWith(Names.named("de")).to(GermanHello.class).asEagerSingleton();
  }
}

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

import javax.inject.*;
import play.inject.ApplicationLifecycle;
import play.Environment;
import java.util.concurrent.CompletableFuture;

// This creates an `ApplicationStart` object once at start-up.
@Singleton
public class ApplicationStart {

  // Inject the application's Environment upon start-up and register hook(s) for shut-down.
  @Inject
  public ApplicationStart(ApplicationLifecycle lifecycle, Environment environment) {
    // Shut-down hook
    lifecycle.addStopHook(
        () -> {
          return CompletableFuture.completedFuture(null);
        });
    // ...
  }
}
import com.google.inject.AbstractModule;

public class StartModule extends AbstractModule {
  protected void configure() {
    bind(ApplicationStart.class).asEagerSingleton();
  }
}

§Play 库

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

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

import com.typesafe.config.Config;
import java.util.Arrays;
import java.util.List;
import play.Environment;
import play.inject.Binding;
import play.inject.Module;

public class HelloModule extends Module {
  @Override
  public List<Binding<?>> bindings(Environment environment, Config config) {
    return Arrays.asList(
        bindClass(Hello.class).qualifiedWith("en").to(EnglishHello.class),
        bindClass(Hello.class).qualifiedWith("de").to(GermanHello.class));
  }
}

此模块可以通过将其附加到 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"

§管理循环依赖

循环依赖发生在你的一个组件依赖于另一个组件时,而另一个组件又依赖于原始组件(直接或间接)。例如

public class Foo {
  @Inject
  public Foo(Bar bar) {
    // ...
  }
}

public class Bar {
  @Inject
  public Bar(Baz baz) {
    // ...
  }
}

public class Baz {
  @Inject
  public Baz(Foo foo) {
    // ...
  }
}

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

public class Foo {
  @Inject
  public Foo(Bar bar) {
    // ...
  }
}

public class Bar {
  @Inject
  public Bar(Baz baz) {
    // ...
  }
}

public class Baz {
  @Inject
  public Baz(Provider<Foo> fooProvider) {
    // ...
  }
}

请注意,如果您使用的是构造函数注入,那么当您遇到循环依赖时,情况会更加清晰,因为您将无法手动实例化组件。

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

§高级:扩展 GuiceApplicationLoader

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

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

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import play.ApplicationLoader;
import play.inject.guice.GuiceApplicationBuilder;
import play.inject.guice.GuiceApplicationLoader;

public class CustomApplicationLoader extends GuiceApplicationLoader {

  @Override
  public GuiceApplicationBuilder builder(ApplicationLoader.Context context) {
    Config extra = ConfigFactory.parseString("a = 1");
    return initialBuilder
        .in(context.environment())
        .loadConfig(extra.withFallback(context.initialConfig()))
        .overrides(overrides(context));
  }
}

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

play.application.loader = "modules.CustomApplicationLoader"

您不限于使用 Guice 进行依赖注入。通过覆盖 ApplicationLoader,您可以控制应用程序的初始化方式。

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

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


import com.google.inject.ImplementedBy; @ImplementedBy(LiveCounter.class) interface Counter { public void inc(String label); }
public class NoopCounter implements Counter {
  public void inc(String label) {}
}
import javax.inject.Singleton;

@Singleton
public class LiveCounter implements Counter {
  public void inc(String label) {
    System.out.println("inc " + label);
  }
}
import javax.inject.Inject;
import play.mvc.Controller;
import play.mvc.Result;

public class BaseController extends Controller {
  // LiveCounter will be injected
  @Inject protected volatile Counter counter = new NoopCounter();

  public Result someBaseAction(String source) {
    counter.inc(source);
    return ok(source);
  }
}
import javax.inject.Singleton;
import play.mvc.Result;

@Singleton
public class SubclassController extends BaseController {
  public Result index() {
    return someBaseAction("index");
  }
}

下一步:编译时依赖注入


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