Play Framework 2.5 | Dependency Injection 总结

最近在将Samsara Aquarius从Play 2.4.6迁移至Play 2.5.0的时候发现,Play 2.5将一些全局对象deprcated了,并强烈建议全面使用依赖注入来代替全局对象,所以就把Aquarius的代码用DI重构了一下。其实从Play 2.4.0开始就引入了依赖注入了(基于JSR 330标准),只不过还没有很好地推广。这里就来总结一下Play Framework中DI的使用。(本来开发的时候想保持FP风格的,无奈DI更多的是偏OO的风格。。FP与OO杂糅不好把握呀。。)

为何需要引入依赖注入

依赖注入(Dependency Injection)在OOP中早已是一个耳熟能详的原则了,其中Spring里用DI都用烂了。简单来说,依赖注入使得我们不需要自己创建对象,而是由容器来帮我们创建。每个组件之间不再是直接相互依赖,而是通过容器进行注入,这降低了组件之间的耦合度。这个容器就像是一个全局的大工厂,专门“生产”对象,而我们只需要进行配置(常见的通过XML文件或通过注解)。

在Play API中有一个Global对象,保存着一些全局的可变状态。另外还有一个Application对象相当于当前正在运行的Play实例。这两个伴生对象经常会在测试和部署的时候引发问题,并且也会影响Play实例的生命周期以及插件系统的工作。因此从Play 2.4开始,开发者对底层的结构做了很大的调整,底层所有的组件(包括Application、Route、Controller)都通过依赖注入进行管理,而不再使用Global和Application对象。后面版本中这两个对象只是从DI中获取实例的引用。从Play 2.5开始,这些全局对象被deprecated。

Play内部的DI组件都用的是 Google Guice。只要是符合JSR-330标准的DI组件都可用于Play Framework中。

DI in Play Framework

如何使用

比如我们的B组件需要A组件的实例作为依赖,我们可以这么定义:

1
2
3
import javax.inject.Inject
class B @Inject() (a: A)

注意,@Inject()需要插入在类名之后,构造参数列表之前,后边跟上需要注入的对象列表。

Guice里面的依赖注入有好几种方式构造注入方法注入 等等。这里采用最常用的构造注入。

生命周期及范围

依赖注入系统管理着各个注入组件的生命周期和范围。有以下规则:

  • 每次从Injector里取出的都是新的对象,即每次需要此组件的时候都会创建新的实例,用Spring IoC的话来说就是Bean的范围是 Prototype 。这一点和Spring不同(Spring默认是Singleton)。当然可以通过给待注入的类加上@Singleton注解来实现 Singleton
  • 遵循懒加载原则,即不用的时候就不创建。如果需要提前创建实例的话可以使用 Eager Binding

ApplicationLifecycle

有些组件需要在Play结束运行的时候进行一些清理工作,如关闭连接、关闭句柄。Play提供了ApplicationLifecycle类,可以通过addStopHook函数给组件注册回调,在Play结束运行的时候进行清理工作。addStopHook函数有两个版本,常用的是第一个:

1
2
3
def addStopHook(hook: () => Future[_]): Unit
def addStopHook(hook: Callable[_ <: CompletionStage[_]]): Unit

底层实现嘛比较直观,默认的实现是DefaultApplicationLifecycle类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Default implementation of the application lifecycle.
*/
@Singleton
class DefaultApplicationLifecycle extends ApplicationLifecycle {
private val mutex = new Object()
@volatile private var hooks = List.empty[() => Future[_]]
def addStopHook(hook: () => Future[_]) = mutex.synchronized {
hooks = hook :: hooks
}
def stop(): Future[_] = {
// Do we care if one hook executes on another hooks redeeming thread? Hopefully not.
import play.api.libs.iteratee.Execution.Implicits.trampoline
hooks.foldLeft(Future.successful[Any](())) { (future, hook) =>
future.flatMap { _ =>
hook().recover {
case e => Logger.error("Error executing stop hook", e)
}
}
}
}
}

DefaultApplicationLifecycle类里维护了一个钩子列表hook用于存储所有注册的回调函数,类型为List[() => Future[_]]。由于DefaultApplicationLifecycle组件为单例,因此为避免资源争用,将hook变量声明为@volatile,并且注册回调函数时需要加锁。注意回调函数是按注册的顺序进行存储的。在应用结束时,会调用stop函数,通过foldLeft依次调用各个回调函数。

Play 应用重构实例

之前我把部分的Service设计成了Object(脑残了),并且在获取Slick的DatabaseConfig的时候使用了全局变量play.api.Play.current。这里我们来重构一下。

首先把Service重构为单例的类,并且通过DI的方式获取db。可以继承HasDatabaseConfigProvider[JdbcProfile]接口并注入DatabaseConfigProvider,这样Service就可以直接使用HasDatabaseConfigProviderdb对象了。当然如果不想继承HasDatabaseConfigProvider接口的话也可以仅注入DatabaseConfigProvider并自己在类中获取dbConfigdb(其它方式见Play-Slcik的文档)。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Singleton
class UserService @Inject()(protected val dbConfigProvider: DatabaseConfigProvider) extends HasDatabaseConfigProvider[JdbcProfile] {
import driver.api._
private val users = TableQuery[UserTable]
def add(user: User): Future[Int] = {
db.run(users += user) recover {
case duplicate: com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException => DB_ADD_DUPLICATE
case _: Exception => -2
}
}
// 其他代码略......
}

接下来就是在Controller里配置DI将Service注入至Controller中。以UserController为例:

1
2
3
4
5
6
7
8
9
10
11
@Singleton
class UserController @Inject() (service: UserService) extends Controller {
def loginIndex = Action { implicit request =>
request.session.get("aq_token") match {
case Some(user) => Redirect(routes.Application.index())
case None => Ok(views.html.login(LoginForm.form))
}
} // 其他代码略......
}

DI底层调用过程

Play API中所有的DI都用的 Google Guice。它们最后都是调用了GuiceInjector类的instanceOf函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Play Injector backed by a Guice Injector.
*/
class GuiceInjector @Inject() (injector: com.google.inject.Injector) extends PlayInjector {
/**
* Get an instance of the given class from the injector.
*/
def instanceOf[T](implicit ct: ClassTag[T]) = instanceOf(ct.runtimeClass.asInstanceOf[Class[T]])
/**
* Get an instance of the given class from the injector.
*/
def instanceOf[T](clazz: Class[T]) = injector.getInstance(clazz)
/**
* Get an instance bound to the given binding key.
*/
def instanceOf[T](key: BindingKey[T]) = injector.getInstance(GuiceKey(key))
}

再往底层调用com.google.inject.internal#getProvider方法获取Provider,最终都会调用到某个种类的Injector的injectprovisionconstruct方法。

Call Stack

题外话-函数式编程中的DI

以前用DI的时候一直在想,这玩意在OOP中用途这么广泛,那么在FP里会是什么光景呢?其实在FP里,Currying 就可以当做是OOP中的DI。这里先挖个坑,待填坑:)

文章目录
  1. 1. 为何需要引入依赖注入
  2. 2. DI in Play Framework
    1. 2.1. 如何使用
    2. 2.2. 生命周期及范围
    3. 2.3. ApplicationLifecycle
  3. 3. Play 应用重构实例
    1. 3.1. DI底层调用过程
  4. 4. 题外话-函数式编程中的DI