作用域(Scopes)
作用域
默认情况下,Guice每次都会返回一个新的实例。这个特性可以通过作用域(scopes)进行配置。Scopes允许你重用实例对象,例如在整个应用生命周期级别的重用(@Singleton
)、session级别的重用(@SessionScoped
)或request级别的重用(@RequestScoped
)。Guice提供了一个servlet扩展用于支持在web应用中的作用域。同时,你还可以自已定义作用域用于其他类型的应用。
应用Scopes
Guice支持使用注解来定义作用域。你可以通过在实现类上添加作用域注解来指定绑定实例的作用域。这些注解同时也提供了文档注释功能。例如,@Singleton
注解同时也表明该类应该是线程安全的。
@Singleton
public class InMemoryTransactionLog implements TransactionLog {
/* everything here should be threadsafe! */
}
作用域也可以在bind()
语句中配置:
bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);
另外,通过注解@Provides
方法也可以达到同样的目的:
@Provides @Singleton
TransactionLog provideTransactionLog() {
...
}
如果一个类型存在的多个作用域发生冲突,Guice会优先使用bind()
语句中的。这就意味着,如果一个类型被绑定了你不希望使用的作用域,你可以在bind()
语句中使用Scopes.NO_SCOPE
覆盖它。
需要注意的是:在关联绑定中作用域是应用在绑定源(binding source)而不是绑定目标上(binding target)的。例如,我们有一个类Applebees
同时是实现了Bar
接口和Grill
接口,下面的绑定方式最终会生成两个Applebees
的实例:一个是Bar
类型的、一个是Grill
类型的。
bind(Bar.class).to(Applebees.class).in(Singleton.class);
bind(Grill.class).to(Applebees.class).in(Singleton.class);
原因就在于,作用域影响的是边界类型(Bar
和Grill
)而不是满足绑定的类型(Applebees
)。为了保证只有一个实例被创建,需要在类型的声明上添加@Singleton注解或者额外添加一个绑定:
bind(Applebees.class).in(Singleton.class);
有了上面的绑定,就无需前面的两组.in(Singleton.class
)了。
in()
语句可以接受一个作用域注解,如RequestScoped.class
;或者一个Scope
的实例,如ServletScopes.REQUEST:
bind(UserPreferences.class)
.toProvider(UserPreferencesProvider.class)
.in(ServletScopes.REQUEST);
Guice建议使用基于注解的方式,因为它可以使得module重用于不同类型的应用中。例如,一个@RequestScoped
对象可以被用在web应用中的HTTP请求中或用在API服务器的RPC中。
恶汉模式的Singleton
Guice提供了一种定义单例绑定的语法来使注入器更早地创建实例对象:
bind(TransactionLog.class).to(InMemoryTransactionLog.class).asEagerSingleton();
恶汉模式singleton可以更早地发现初始化问题,从而确保终端用户能够有更一致优雅的体验。相反的,懒汉模式的singleton则能够保证开发-编译-运行这个流程更加快捷。通常,Stage
枚举指定了不同的加载策略。
PRODUTION | DEVELOPMENT | |
---|---|---|
.asEagerSingleton() | eager | eager |
.in(Sigleton.class) | eager | lazy |
.in(Scope.SINGLETON) | eager | lazy |
@Singleton | eager* | lazy |
*Guice只会为它明确知道的类型执行恶汉模式的单例构建,包括在modules中显示定义的类型和这些类型的传递依赖。(也就是隐式绑定不会被)
选择合适的作用域
如果对象是有状态的(stateful),那么作用域的选择是显而易见的:对单个应用中使用的对象采用@Singleton
,对单个请求中的使用的对象采用@RequestScoped
。如果对象是无状态的(stateless)并且创建开销很小,作用域不是必须的。在绑定配置中保持默认即可,Guice会在需要的时候自动创建实例。
单例模式在Java应用中非常流行,但是很多情况下作用有限,尤其是在依赖注入的场景中。尽管单例模式能够节省对象创建的开销(进一步节省GC),但是单例的初始化过程往往需要同步加锁,获取指向单例的引用只需要读取一个volatile变量即可。单例在下面的场景中往往是比较有用的:
- 有状态对象,比如配置对象和计数器;
- 创建或查找开销比较大的对象;
- 包装了某些资源的对象,例如数据库连接池。
作用域与并发
@Singleton
或@SessionScoped
注解的类必须保证线程安全。被注入到这些类内部的类也必须保证自身的线程安全。你需要遵循最小化易变性的原则来减少类中的状态信息,从而减少同步保护的开销。
当然@RequestScoped
是不需要保证线程安全的(因为每次都会从注入器获得一个新的实例)。一般而言,@Singleton
或@SessioinScoped
注解的对象是不能依赖@RequestScoped
注解的对象的(因为@RequestScoped
每次都会从注入器获得一个新的实例导致线程不安全)。如果你需要一个更小的作用域,可以通过注入该对象的Provider
来实现。