관리 메뉴

HAMA 블로그

[Play2] DI (의존성 주입) 본문

PlayFramework2

[Play2] DI (의존성 주입)

[하마] 이승현 (wowlsh93@gmail.com) 2016. 10. 10. 17:03

런타임 의존성 주입  


의존성 주입은 컴포넌트들끼리 서로 독립적이게 하기 위한 방법이다. 서로를 정적으로 포함하기보다는 동적으로 서로에게 주입된다.  플레이는 JSR 330. 기반으로 런타임 의존성 주입을 지원한다. 의존해야하는 특정 컴포넌트를 발견하지 못하면 실행할때까지는 에러를 발견하지 못할것인데  플레이는 컴파일타임 DI 를 또한 지원한다. 플레이에서는 디폴트 의존성 주입 기능으로 Guice 사용한다.  하지만 뭐 다른것을 사용해도 좋다. 

디펜던시 선언 

컨트롤러나 컴포넌트를 가지고 있을때 다른 컴포넌트를 요구할 수 있는데 이때  @Inject 어노데이션을 사용한다. @Inject 는 필드나 생성자에서 사용될 수 있지만 생성자에서 사용하길 추천한다.
예를 보자:

import javax.inject._
import play.api.libs.ws._

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

 @Inject 는 클래스 이름 뒤에 오고 생성자 전에 온다 반드시 괄호를 써줘라.

의존성 주입 컨트롤러 

2가지 방법의 의존성 주입 컨트롤러를 만들 수 있다. 

Injected routes generator

디폴트로 플레이는 정적라우터를 생성할것이고 모든 액션들이 정적 메소드라고 보면 된다. 설정을 변경해서 injected routes generator 로 변경할 수 있으며 각각 디펜던시로써 라우트 된다. 즉 당신의 컨트롤러들을 스스로 의존성 주입되게 할 수 있다는 뜻이다. (역주: 디폴트로 컨트롤라가 object 였을것이다. 이거 사용하면 class 로 변경가능해짐.)

우리는 injected routes generator 쓰기를 추천한다.  이것을 쓰기위해서 설정을 해야하는데 

build.sbt: 파일에 다음을 추가하면 된다. 

routesGenerator := InjectedRoutesGenerator


Injected actions

정적 라우트 제네레이터 를 사용하면 @ 와 함께 액션을 주입하게 지시 할 수 있다. 

GET        /some/path           @controllers.Application.index

lifecycle 컴포넌트 생존주기

의존성 주입 시스템은 생존주기를 관리하는데 다음 과 같다. 

  • 해당 컴포넌트가 필요 할때 마다 새 인스턴스를  만든다 : 컴포넌트가 한번이상 사용될때 디폴트로 여러번 생성된다.  단지 한번만 생성되게 하고 싶으면  singleton.주석을 사용하라. 
  • 필요해 질때, 게으른 초기화로 만들어진다.  If a component is never used by another component, then it won’t be created at all. This is usually what you want. For most components there’s no point creating them until they’re needed. However, in some cases you want components to be started up straight away or even if they’re not used by another component. For example, you might want to send a message to a remote system or warm up a cache when the application starts. You can force a component to be created eagerly by using an eager binding.
  • 인스턴스가 자동으로 청소되지 않는다. beyond normal garbage collection. Components will be garbage collected when they’re no longer referenced, but the framework won’t do anything special to shut down the component, like calling aclose method. However, Play provides a special type of component, called theApplicationLifecycle which lets you register components to shut down when the application stops.

Singletons

Sometimes you may have a component that holds some state, such as a cache, or a connection to an external resource, or a component might be expensive to create. In these cases it may be important that there is only be one instance of that component. This can be achieved using the @Singleton annotation:

import javax.inject._

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

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

Stopping/cleaning up

Some components may need to be cleaned up when Play shuts down, for example, to stop thread pools. Play provides an ApplicationLifecycle component that can be used to register hooks to stop your component when Play shuts down:

import scala.concurrent.Future
import javax.inject._
import play.api.inject.ApplicationLifecycle

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

  //...
}

The ApplicationLifecycle will stop all components in reverse order from when they were created. This means any components that you depend on can still safely be used in your components stop hook, since because you depend on them, they must have been created before your component was, and therefore won’t be stopped until after your component is stopped.

Note: It’s very important to ensure that all components that register a stop hook are singletons. Any non singleton components that register stop hooks could potentially be a source of memory leaks, since a new stop hook will be registered each time the component is created.

Providing custom bindings

It is considered good practice to define an trait for a component, and have other classes depend on that trait, rather than the implementation of the component. By doing that, you can inject different implementations, for example you inject a mock implementation when testing your application.

In this case, the DI system needs to know which implementation should be bound to that trait. The way we recommend that you declare this depends on whether you are writing a Play application as an end user of Play, or if you are writing library that other Play applications will consume.

Play applications

We recommend that Play applications use whatever mechanism is provided by the DI framework that the application is using. Although Play does provide a binding API, this API is somewhat limited, and will not allow you to take full advantage of the power of the framework you’re using.

Since Play provides support for Guice out of the box, the examples below show how to provide bindings for Guice.

Binding annotations

The simplest way to bind an implementation to an interface is to use the Guice@ImplementedBy annotation. For example:

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
}

Programmatic bindings

In some more complex situations, you may want to provide more complex bindings, such as when you have multiple implementations of the one trait, which are qualified by@Named annotations. In these cases, you can implement a custom Guice Module:

import com.google.inject.AbstractModule
import com.google.inject.name.Names
  
class HelloModule extends AbstractModule {
  def configure() = {

    bind(classOf[Hello])
      .annotatedWith(Names.named("en"))
      .to(classOf[EnglishHello])

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

To register this module with Play, append it’s fully qualified class name to theplay.modules.enabled list in application.conf:

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

Configurable bindings

Sometimes you might want to read the Play Configuration or use a ClassLoaderwhen you configure Guice bindings. You can get access to these objects by adding them to your module’s constructor.

In the example below, the Hello binding for each language is read from a configuration file. This allows new Hello bindings to be added by adding new settings in yourapplication.conf file.

import com.google.inject.AbstractModule
import com.google.inject.name.Names
import play.api.{ Configuration, Environment }
  
class HelloModule(
  environment: Environment,
  configuration: Configuration) extends AbstractModule {
  def configure() = {
    // Expect configuration like:
    // hello.en = "myapp.EnglishHello"
    // hello.de = "myapp.GermanHello"
    val helloConfiguration: Configuration =
      configuration.getConfig("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.getString(l).get
      val bindingClass: Class[_ <: Hello] =
        environment.classLoader.loadClass(bindingClassName)
        .asSubclass(classOf[Hello])
      bind(classOf[Hello])
        .annotatedWith(Names.named(l))
        .to(bindingClass)
    }
  }
}

Note: In most cases, if you need to access Configuration when you create a component, you should inject the Configuration object into the component itself or into the component’s Provider. Then you can read the Configuration when you create the component. You usually don’t need to read Configuration when you create the bindings for the component.

Eager bindings

In the code above, new EnglishHello and GermanHello objects will be created each time they are used. If you only want to create these objects once, perhaps because they’re expensive to create, then you should use the @Singleton annotation asdescribed above. If you want to create them once and also create them eagerly when the application starts up, rather than lazily when they are needed, then you can Guice’s eager singleton binding.

import com.google.inject.AbstractModule
import com.google.inject.name.Names
  
class HelloModule extends AbstractModule {
  def configure() = {

    bind(classOf[Hello])
      .annotatedWith(Names.named("en"))
      .to(classOf[EnglishHello]).asEagerSingleton

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

Eager singletons can be used to start up a service when an application starts. They are often combined with a shutdown hook so that the service can clean up its resources when the application stops.

Play libraries

If you’re implementing a library for Play, then you probably want it to be DI framework agnostic, so that your library will work out of the box regardless of which DI framework is being used in an application. For this reason, Play provides a lightweight binding API for providing bindings in a DI framework agnostic way.

To provide bindings, implement a Module to return a sequence of the bindings that you want to provide. The Module trait also provides a DSL for building bindings:

import play.api.inject._

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

This module can be registered with Play automatically by appending it to theplay.modules.enabled list in reference.conf:

play.modules.enabled += "com.example.HelloModule"
  • The Module bindings method takes a Play Environment and Configuration. You can access these if you want to configure the bindings dynamically.
  • Module bindings support eager bindings. To declare an eager binding, add .eagerlyat the end of your Binding.

In order to maximise cross framework compatibility, keep in mind the following things:

  • Not all DI frameworks support just in time bindings. Make sure all components that your library provides are explicitly bound.
  • Try to keep binding keys simple - different runtime DI frameworks have very different views on what a key is and how it should be unique or not.

Excluding modules

If there is a module that you don’t want to be loaded, you can exclude it by appending it to the play.modules.disabled property in application.conf:

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

Advanced: Extending the GuiceApplicationLoader

Play’s runtime dependency injection is bootstrapped by the GuiceApplicationLoaderclass. This class loads all the modules, feeds the modules into Guice, then uses Guice to create the application. If you want to control how Guice initializes the application then you can extend the GuiceApplicationLoader class.

There are several methods you can override, but you’ll usually want to override thebuilder method. This method reads the ApplicationLoader.Context and creates aGuiceApplicationBuilder. Below you can see the standard implementation forbuilder, which you can change in any way you like. You can find out how to use theGuiceApplicationBuilder in the section about testing with Guice.

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

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

When you override the ApplicationLoader you need to tell Play. Add the following setting to your application.conf:

play.application.loader = "modules.CustomApplicationLoader"

You’re not limited to using Guice for dependency injection. By overriding theApplicationLoader you can take control of how the application is initialized. Find out more in the next section.

Next: Compile time dependency injection


Comments