4 Scopes (实例的作用域)
4.1 默认规则:unreuse instance
-
到目前为止,通过
bind().to()
和@Provides
定义的binding,每次需要注入实例对象时,Guice都会创建一个新的实例// 修改DatabaseTransactionLog,使其打印自身的hash code @Override public void log(String msg) { System.out.printf("%s@%s: %s\n", this.getClass().getSimpleName(), Integer.toHexString(hashCode()), msg); } // 创建多个MyDatabase实例,需要多次注入TransactionLog MyDatabase database1 = injector.getInstance(MyDatabase.class); database1.createTable("olap.users"); MyDatabase database2 = injector.getInstance(MyDatabase.class); database2.createTable("presto.queries");
-
执行上述代码,发现Guice为MyDatabase注入了不同的DatabaseTransactionLog实例
-
这是Guice的默认规则,
Guice returns a new instance each time it supplies a value.
4.2 build-in scopes
- 对象的生命周期可以是应用程序级别的、session级别的、request级别,通过**作用域(scope)**可以配置对象的生命周期,从而实现对象的复用
单例@Singleton
-
在实际应用场景中,需要将类定义为单例模式
-
例如,在PrestoDB中,与属性配置有关的类(
SystemSessionProperties
)、监控各种事件的类(GcMonitor
)、任务调度有关的类(NodeScheduler
)等都被设置为单例 -
Guice支持两种
@Singleton
:javax.inject.Singleton
和com.google.inject.Singleton
,官方建议使用javax.inject.Singleton
,因为注入框架也支持它 -
使用@Singleton修饰DatabaseTransactionLog,将其定义为单例
@Singleton public class DatabaseTransactionLog implements TransactionLog { /* everything here should be threadsafe! */ }
-
重新执行上述代码,发现Guice为MyDatabase注入的是一个实例
@RequestScoped
-
除了@Singleton这个内置的scope,Guice官方文档还提到了@RequestScoped,
servlet extension
所包含的、用于web应用程序的scope -
Guice并未说明@RequestScoped的详细信息,推测是
com.google.inject.servlet.RequestScoped
,可以通过如下maven依赖进行引入<dependency> <groupId>com.google.inject.extensions</groupId> <artifactId>guice-servlet</artifactId> <version>5.0.1</version> </dependency>
4.3 如何使用scope
-
方法一: 在实现类上直接使用@Singleton
@Singleton public class DatabaseTransactionLog implements TransactionLog { /* everything here should be threadsafe! */ }
-
方法二: 在bind()语句中定义单例
// 1. 使用注解定义单例 bind(TransactionLog.class).annotatedWith(Names.named("database")).to(DatabaseTransactionLog.class).in(Singleton.class); // 对应的in()方法 void in(Class<? extends Annotation> scopeAnnotation); // 2. 实用Scopes.SINGLETON定义单例 bind(TransactionLog.class).annotatedWith(Names.named("database")).to(DatabaseTransactionLog.class).in(Scopes.SINGLETON); // 对应的in()方法 void in(Scope scope);
-
方法三: 在
@Provides
方法上定义单例@Provides @Named("database") @Singleton public TransactionLog providerDatabaseTransactionLog(DatabaseTransactionLog log) { return log; } @Provides @Named("database") @Singleton public TransactionLog providerDatabaseTransactionLog() { return new DatabaseTransactionLog(); }
-
上述三种定义单例的方式,最终使得Guice只会创建一个DatabaseTransactionLog对象
4.4 scopes apply to the binding source
- 在linked binding中,scope是应用在binding source上,而非binding target
In linked bindings, scopes apply to the binding source, not the binding target.
- 与其说作用在binding source上,更易理解的是:作用在整个binding上
以上面@Provides定义的linked binding为例:
- binding source:TransactionLog,binding target:DatabaseTransactionLog
- 感觉@Singleton不会起作用,因为TransactionLog —> DatabaseTransactionLog,最终是需要得到DatabaseTransactionLog对象
- 而无论是通过方法入参传入,还是通过new DatabaseTransactionLog()创建,而@Singleton标记不是的DatabaseTransactionLog,Guice应该会创建多个DatabaseTransactionLog对象
- 实际上,Guice只会创建一个DatabaseTransactionLog对象,这是由于Singleton作用在TransactionLog上,使得TransactionLog只对应一个DatabaseTransactionLog对象
- 这个DatabaseTransactionLog对象一旦创建好,Guice就像是直接从缓存中获取一样,而无需再创建新的DatabaseTransactionLog对象
示例2:一个类实现了两个接口,最终创建几个对象?
-
小学生Pupil类,同时实现了Person和Student接口
public interface Person { void eat(); } public interface Student { void study(); } public class Pupil implements Person, Student { @Override public void eat() { System.out.println("A pupil is eating potato chips"); } @Override public void study() { System.out.println("A pupil is studying math"); } @Override public String toString() { return this.getClass().getSimpleName() + "@" + Integer.toHexString(this.hashCode()); } }
-
在Module中定义binding关系
bind(Person.class).to(Pupil.class).in(Scopes.SINGLETON); bind(Student.class).to(Pupil.class).in(Singleton.class);
-
从Guice获取实例:
Student student = injector.getInstance(Student.class); Person person = injector.getInstance(Person.class); System.out.printf("student: %s, person: %s", student, person);
-
执行代码,发现Student对应的Pupil对象和Person对应的Pupil对象,并非同一个
-
如果Singleton作用在binding target上,则最终将从Guice获得同一个Pupil对象;但是,Singleton作用在binding source上,使得Student和Person将对应不同的Pupil对象
如何解决上述问题?
-
若我们希望Pupil是单例,可以在定义Pupil类时使用@Singleton
@Singleton public class Pupil implements Person, Student {}
-
或者添加新的binding,将Pupil定义为单例
bind(Pupil.class).in(Singleton.class);
4.5 单例的懒汉模式 vs 恶汉模式
-
恶汉模式的单例(
Eager singleton
)会在类加载后完成初始化,以保证用户获得一致且快速的体验 -
懒汉模式的单例(
Lazy singleton
,第一次使用时才进行初始化,可以加快程序的编译、运行周期 -
Guice中定义单例的方式很多,具体为哪种模式,可以参考表格
定义方式 PRODUCTION DEVELOPMENT .asEagerSingleton() eager eager .in(Singleton.class) eager lazy .in(Scopes.SINGLETON) eager lazy @Singleton eager lazy
Guice comes with a built-in @Singleton scope that reuses the same instance during the lifetime of an application within a single injector. Both javax.inject.Singleton and com.google.inject.Singleton are supported by Guice, but prefer the standard javax.inject.Singleton since it is also supported by other injection frameworks like Dagger.
5. Instance Bindings
-
到目前为止,我们都是将type绑定到具体的implemention(实现)。
-
在某些场景下,我们需要将type绑定到该type的具体实例
-
例如,在整个应用中jdbc串是固定的,我们可以通过Guice为String类型的
jdbcUrl
传入固定值 -
MyDatabase的构造函数中,使用
@Named("mysql jdbc url")
标记MySQL JDBC串,然后通过instance binding为其赋值"jdbc:mysql://localhost/user"
@Inject public MyDatabase(@Named("database")TransactionLog log, @Named("mysql jdbc url")String jdbcUrl) { this.log = log; System.out.println("Initialize MyDatabase with jdbcUrl: " + jdbcUrl); } // 通过instance binding配置jdbc url bind(String.class).annotatedWith(Names.named("mysql jdbc url")).toInstance("jdbc:mysql://localhost/users");
-
针对基本数据类型、常见类型(如String、enum、Class),还可以使用
bindConstant()
直接绑定具体实例// 创建binding Annotation @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface ForDatabase { } // 使用binding Annotation @Inject public MyDatabase(@Named("database") TransactionLog log, @ForDatabase String jdbcUrl) { this.log = log; System.out.println("Initialize MyDatabase with jdbcUrl: " + jdbcUrl); } // instance binding,注入具体的String对象 bindConstant().annotatedWith(ForDatabase.class).to("jdbc:mysql://localhost/users");
-
最终,创建MyDatabase时将打印传入的jdbcUrl
6. Provide Bindings
6.1 @Provides Methods
- 最简单的方法,使用@Provides method实现对象的创建
- @Provides method也不陌生了,之前在很多地方都有使用
- 可以直接定义@Provides method
@Provides public TransactionLog provideTransactionLog() { DatabaseTransactionLog transactionLog = new DatabaseTransactionLog(); transactionLog.setJdbcUrl("jdbc:mysql://localhost/pizza"); transactionLog.setThreadPoolSize(30); return transactionLog; }
- 还可以定义带注解的@Provides method
// instance binding,定义jdbcUrl的值 bind(String.class).annotatedWith(Names.named("mysql jdbc url")).toInstance("jdbc:mysql://localhost/users"); // Guice自动传入jdbcUrl @Provides @Database public TransactionLog provideTransactionLog(@Named("mysql jdbc url") String jdbcUrl) { DatabaseTransactionLog transactionLog = new DatabaseTransactionLog(); transactionLog.setJdbcUrl(jdbcUrl); transactionLog.setThreadPoolSize(30); return transactionLog; } // 匹配到使用@Database标识的@Provides method @Inject public MyDatabase(@Database TransactionLog log) { this.log = log; } @Override public void log(String msg) { System.out.printf("jdbcUrl: %s, threadPoolSize: %d\n", jdbcUrl, threadPoolSize); }
- 最终,为MyDatabase注入的TransactionLog信息为:
jdbcUrl: jdbc:mysql://localhost/users, threadPoolSize: 30
- 可以直接定义@Provides method
6.2 Provider Bindings
-
@Provides method中,创建对象的代码可能变得越来越复杂,也可能由于Module中存在多个@Provides method,导致Module变得越来越臃肿
-
这时,可以考虑将@Provides method中代码转移到一个独立的类中,这个类实现了Guice的Provider接口
public interface Provider<T> extends javax.inject.Provider<T> { T get(); }
-
下面的代码,在Provider的实现类中定义DatabaseTransactionLog的创建逻辑。其中,Connection需要Guice自动注入
public class DatabaseTransactionLogProvider implements Provider<TransactionLog> { private final Connection connection; @Inject public DatabaseTransactionLogProvider(Connection connection) { this.connection = connection; } public TransactionLog get() { DatabaseTransactionLog transactionLog = new DatabaseTransactionLog(); transactionLog.setConnection(connection); return transactionLog; } }
-
在Module中使用
toProvider()
定义bindingbind(TransactionLog.class).toProvider(DatabaseTransactionLogProvider.class);
7. Constructor Bindings
- 之前,我们通过@Inject标识某个构造函数,告诉Guice这既是依赖注入的入口,又是创建实例对象的入口
- 但是以下情况,@Inject将变得不再适用:
- 使用的是第三方类
- 类的多个构造函数参与了依赖注入
如何理解这个类的多个构造函数参与了依赖注入
?
- 不同的dependent class,需要注入使用不同的构造函数创建的依赖
- 例如,有的dependent class只需要注入一个通过默认构造函数创建的依赖,有的dependent class需要注入一个通过有参构造函数创建的依赖
-
面对上述情况,可以通过Provide binding自己决定使用哪个构造函数创建对象
-
但是,在AOP这种不能手动构造对象的情况,Provide binding也变得不再适用
-
这时,可以考虑使用
toConstructor()
定义constructor binding -
定义有多个构造函数的MyTransactionLog
public class MyTransactionLog implements TransactionLog { private String jdbcUrl; private int threadPoolSize; public MyTransactionLog() { } public MyTransactionLog(@Named("mysql") String jdbcUrl) { this.jdbcUrl = jdbcUrl; } @Override public void log(String msg) { // 打印自身信息 System.out.printf("jdbcUrl: %s, threadPoolSize: %d\n", jdbcUrl, threadPoolSize); } }
-
向MyDatabase和MyWarehouse注入TransactionLog
@Inject public MyDatabase(@Database TransactionLog log) { this.log = log; } @Inject public MyWarehouse(@Named("warehouse") TransactionLog log) { this.log = log; }
-
使用
toConstructor()
定义constructor binding,使得MyDatabase和MyWarehouse注入的try { // MyDatabase使用的构造函数 bind(TransactionLog.class).annotatedWith(Database.class) .toConstructor(MyTransactionLog.class.getConstructor(String.class)); // MyWarehouse使用的构造函数 bind(TransactionLog.class).annotatedWith(Names.named("warehouse")) .toConstructor(MyTransactionLog.class.getConstructor()); } catch (NoSuchMethodException e) { throw new RuntimeException(e); }
-
最终,MyDatabase和MyWarehouse将被注入具有不同属性的TransactionLog
8. Just-in-time Bindings
- 以上binding,最终都落在了继承AbstractModule自定义Module中,被叫做显式绑定(
explicit binding
) - 如果一个type被需要,却又没有显示绑定,这是Guice会去寻找隐式binding(
implicit binding
) - 隐式绑定,又叫Just-In-Time binding,简称 JIT binding
8.1 @Inject的隐式绑定
- Guice创建一个类的对象时,需要找到该类可以依赖注入的构造函数(
injectable constructor
)
以下两种情况,Guice认为构造函数是可注入的:
- 使用@Inject标识的构造函数(推荐的方式)
- 无参构造函数:
- 且定义在非private的类中的、非private构造函数、(实际上,Guice支持private类中private构造函数,但是不推荐,因为反射会导致程序运行变慢)
- 且未要求必须使用显式绑定
binder().requireAtInjectRequired();
以下情况,Guice认为构造函数不是可注入的
- 未使用@Inject标识的、有一个或多个参数的构造函数
- 类中不止存在一个使用@Inject标识的构造函数
- 非静态的内部类中的构造函数
8.2 其他隐式绑定
8.2.1 @ImplementedBy
-
使用
@ImplementedBy
定义linked binding@ImplementedBy(DatabaseTransactionLog.class) public interface TransactionLog { void log(String msg); }
-
等价于下面的
bind()
语句bind(TransactionLog.class).to(DatabaseTransactionLog.class);
-
如果既使用了
bind()
,又使用了@ImplementedBy,则bind()的优先级更高,会覆盖@ImplementedBy的定义
8.2.2 @ProvidedBy
-
定义好的Provider类,可以通过@ProvidedBy告知Guice
@ProvidedBy(DatabaseTransactionLogProvider.class) public interface TransactionLog { void log(String msg); }
-
等价于下面的语句
bind(TransactionLog.class).toProvider(DatabaseTransactionLogProvider.class);
8.3 关闭隐式绑定
-
从Guice 3.0开始,在
configure()
方法中使用如下语句,可以关闭隐式绑定binder().requireExplicitBindings();
9. 后记
-
Guice还提供了很多其他的binding,例如支持binding多个实现的multi binding
Multibinder<UriSummarizer> uriBinder = Multibinder.newSetBinder(binder(), UriSummarizer.class); // 可以Set的形式注入 Set<UriSummarizer> summarizers MapBinder<String, EncoderFactory> encoderFactories = MapBinder.newMapBinder(binder, String.class, EncoderFactory.class); // 可以Map的形式注入 Map<String, EncoderFactory>
-
需要的小伙伴可以继续深入学习,这里就不再记录总结了