Play 2.6 依赖注入

本文介绍了Play框架中的依赖注入机制,包括Guice的使用方法、组件生命周期管理、循环依赖的处理等。通过具体示例展示了如何在Play应用中实现依赖注入。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

依赖注入

https://playframework.com/documentation/2.6.x/JavaDependencyInjection

Play对实现了JSR 330的依赖注入提供了支持。Play自带的默认的实现是Guice,其他实现也可以添加进来。为了启用Guice模块,先要在依赖中添加信息

libraryDependencies += guice

Guice wiki是学习Guice和依赖模式非常好的资源。

Motivation

Dependency injection achieves several goals:
1. It allows you to easily bind different implementations for the same component. This is useful especially for testing, where you can manually instantiate components using mock dependencies or inject an alternate implementation.
2. It allows you to avoid global static state. While static factories can achieve the first goal, you have to be careful to make sure your state is set up properly. In particular Play’s (now deprecated) static APIs require a running application, which makes testing less flexible. And having more than one instance available at a time makes it possible to run tests in parallel.

The Guice wiki has some good examples explaining this in more detail.

工作原理

Play提供了大量的内建组件并且在modules中进行了声明,比如BuiltinModule。这些绑定描述了每一个创建一个Application实例所需的东西,包括,在默认情况下,根据routes生成一个router,并且将controller都注到构造中。这些绑定可以在后续中转化为Guice或其他运行时DI框架的工作。

Play小组维护Guice模块,提供了一个 GuiceApplicationLoade。该模块为Guice转化绑定,根据绑定创建Guice注入器,然后冲注入器中获取一个Application实例。

也有第三方的加载器为其他的框架进行这些工作,包括Spring

下面将会展示如何定制化默认的绑定和应用加载器。

声明依赖

如果你又一个组件,并且该组件需要其他组件作为依赖,然后可以使用@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的文档中获取更多细节。如果你需要更加复杂的功能,下面将介绍如何进行订计划的绑定。

注入Controller

有两种方式可以注入controller

注入routes generator

默认情况下,从2.5.0开始,Play会生成一个router类在构造器中声明你的controller。这允许将你的controller注入到router中。

如果想特地的启用routes generator,在build.sbt添加配置:

routesGenerator := InjectedRoutesGenerator

当使用routes generator时,在acrion前添加一个@前缀会有特别的含义,这意味着一个controller的Provider会代替controller被注入到router中。这样允许使用protype controller,可以作为打破循环依赖的选项。

静态routes generator

你可以配置Play来使用遗留的静态routes generator(在2.5.0之前),这会假设所以的action都是静态方法,可以使用以下配置

routesGenerator := StaticRoutesGenerator

我们建议使用注入的routes generator。在已有工程没办法一次将controller非静态化,静态routes generator作为一种迁移手段而存在。

如果使用静态routes generator,你可以通过添加一个@前缀表明action有一个被注入的controller

GET        /some/path        @controllers.Application.index()

组件生命周期

依赖注入系统管理被注入组件的生命周期,在需要的时候创建然后注入到其他逐渐中。下面展示了组件的声明周期如何工作:
- 在需要时每次都创建新的实例。如果一个组件使用超过一次,默认情况下,会有多个实例被创建。如果你指向要一个实例,可以标记为单例
- 实例懒加载。如果一个组件没有被别的组件使用,那么就不需要去创建实例,这也是你希望的方式。对于大多数组件,在被使用前都不会创建。无论如何,你希望组件直接启动。举个例子,在服务启动时你想给一个远程服务发送信息或者预热缓存。可以通过eager binding来强制组件尽早创建。
- 实例不会自动清理,超出正常的垃圾回收之外。组件在他们不再被引用了之后才会回收,但是框架不会做任何操作来结束组件,像是调用一个close方法。不过组件提供一种特殊类型的组件,ApplicationLifecycle,可以注册你的组件,在服务停机时进行关闭操作

单例

有时你的组件会保持一些创建,比如一个缓存,或者一个对外部数据的连接,或是创建的开销非常大。在这些情况下你只需要一个实例,可以使用一个@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 javax.inject.*;
import play.inject.ApplicationLifecycle;

import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;

@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会以创建的反序关闭所以的组件。这意味着你所依赖的任何组件都可以在停止hook中安全的使用,因为你依赖于这些组件,他们必须在你的组件之前创建,因此在你的组件关闭前它们都不会被停止。

Note: 注册到钩子中的组件必须是单例。任何非单例的组件都有可能导致内存泄漏,因为没创建一个组件都会注册一个新的钩子

提供定制化的绑定

一个很好的实践是为组件定义一个接口,然后提供一个实现类而不是直接去实现组件。提供接口可以让你为组件注入不同的实现,比如你可以注入一个mock来测试你的程序。

在这种情况下,DI框架需要知道去绑定哪一个实现。我们的建议根据你是一名Play应用的使用者还是一个Play类库的开发者有所区别。

Play应用开发

我们建议在Play中使用DI框架。虽然Play提供绑定API,但是作用非常有限。

Play对Guice提供了很好的支持,下面的例子都使用Guice

绑定注解

最简单方式是使用@ImplementedBy注解

import com.google.inject.ImplementedBy;

@ImplementedBy(EnglishHello.class)
public interface Hello {

    String sayHello(String name);
}
程序绑定

在一些更加复杂的情况下,你也许需要提供更加负责的绑定,比如一个特质拥有多个实现,可以使用@Named进行限定,这种情况可以实现一个自定义的Guice Model

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);
    }
}

如果你将model起名为Module并放在根目录下,会自动注册Play。当然你也可以使用application.conf中进行配置

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

你也可以停用对根目录下的Module自动注入

play.modules.disabled += "Module"
可配置绑定

再配置Guice时,也许你想读取Config或者使用CLassLoader。你可以将这些添加到module的构造器中。

在下面的例子中,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);
            }
        });
    }
}
Note: 在大多数情况下,如果你需要在创建组件中获取Config,你需要将Config对象注入到组件中,或者Provider里。然后在创建组件时就可以读取Config。在为组件创建绑定时一般不需要读取Config
Eager bindings

在下面的例子中,EnglishHello和GermanHello在使用时每次都会创建新的对象。如果你只需要创建一次,可以使用@Singleton注解。如果你只想创建一次而且要尽可能早的创建,可以使用Guice’s eager singleton bindingbs

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();
    }
}

Eager singletons可以在应用启动时开启一些服务。他们通常与关闭钩子一起使用,在应用结束时清理资源

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框架不可见,这样你的类库才能与各类框架一起使用。Play提供了一个轻量级的API来帮助你完成这个功能。

实现一个Module返回一个你需要绑定的序列。Module特质也提供了用于构建绑定的DSL

import play.api.*;
import play.api.inject.*;
import scala.collection.Seq;

public class HelloModule extends Module {
    @Override
    public Seq<Binding<?>> bindings(Environment environment, Configuration configuration) {
        return seq(
            bind(Hello.class).qualifiedWith("en").to(EnglishHello.class),
            bind(Hello.class).qualifiedWith("de").to(GermanHello.class)
        );
    }
}

可以在reference.conf(application.conf也可以)中添加如下内容,Play会自动进行注册

play.modules.enabled += "com.example.HelloModule"
  • Module bindings方法获取一个Play Environment和Configuration对象,如果你想动态的配置绑定,也可以去获取这些参数。
  • 同样支持eager bindings。可以通过.eagerly()方法来声明。

为了尽可能大的增加兼容性,记住以下几条:
- 不是所有的框架都支持及时绑定。保证你类库锁提on个的组件有个显示边界。
- 试着去保持绑定key的简单性,不同的框架对于什么是key,key是否不同有着不同的视角

排除modules

如果有一个module你不想加载,你可以在application.conf中进行排除

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) {
    // ...
  }
}

在这个例子中,你不可能创建任何实例,可以通过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。当你的组件依赖Aplication时,表明需要一个完整的应用来支持这个组件,大多数情况下都是不必要的。你的组件需要依赖于一下更加具体的组件(比如Environment)提供你所需的特定功能。最后你可以注入一个Provider<Application>

高级应用,扩展GuiceApplicationLoader

Play运行时依赖注入由GuiceApplicationLoader这个类加载所有的module,将module传递给Guice,然后使用Guice创建应用。如果你想控制Guice初始化的过程,你可以扩展GuiceApplicationLoader

GuiceApplicationLoader有很多方法可以被覆盖,通常来说你只需要覆盖builder方法。这个方法读取ApplicationLoader.Context然后创建一个GuiceApplicationBuilder。下面展示了一个标准的builder实现,你可以按你的需要来修改。你可以在测试Guice章节来看到具体的用法。

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。

play.application.loader = "modules.CustomApplicationLoader"

在Play中并不是只能使用Guice进行依赖注入,通过重写ApplicationLoader你可以对初始化进行控制。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值