最近在将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组件的实例作为依赖,我们可以这么定义:
|
|
注意,@Inject()
需要插入在类名之后,构造参数列表之前,后边跟上需要注入的对象列表。
Guice里面的依赖注入有好几种方式:构造注入、方法注入 等等。这里采用最常用的构造注入。
生命周期及范围
依赖注入系统管理着各个注入组件的生命周期和范围。有以下规则:
- 每次从Injector里取出的都是新的对象,即每次需要此组件的时候都会创建新的实例,用Spring IoC的话来说就是Bean的范围是 Prototype 。这一点和Spring不同(Spring默认是Singleton)。当然可以通过给待注入的类加上
@Singleton
注解来实现 Singleton 。 - 遵循懒加载原则,即不用的时候就不创建。如果需要提前创建实例的话可以使用 Eager Binding 。
ApplicationLifecycle
有些组件需要在Play结束运行的时候进行一些清理工作,如关闭连接、关闭句柄。Play提供了ApplicationLifecycle
类,可以通过addStopHook
函数给组件注册回调,在Play结束运行的时候进行清理工作。addStopHook
函数有两个版本,常用的是第一个:
|
|
底层实现嘛比较直观,默认的实现是DefaultApplicationLifecycle
类:
|
|
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就可以直接使用HasDatabaseConfigProvider
的db
对象了。当然如果不想继承HasDatabaseConfigProvider
接口的话也可以仅注入DatabaseConfigProvider
并自己在类中获取dbConfig
和db
(其它方式见Play-Slcik的文档)。
代码如下:
接下来就是在Controller里配置DI将Service注入至Controller中。以UserController为例:
|
|
DI底层调用过程
Play API中所有的DI都用的 Google Guice。它们最后都是调用了GuiceInjector
类的instanceOf
函数:
|
|
再往底层调用com.google.inject.internal#getProvider
方法获取Provider,最终都会调用到某个种类的Injector的inject
、provision
、construct
方法。
题外话-函数式编程中的DI
以前用DI的时候一直在想,这玩意在OOP中用途这么广泛,那么在FP里会是什么光景呢?其实在FP里,Currying 就可以当做是OOP中的DI。这里先挖个坑,待填坑:)