1. 序言
-
上一篇博客,《Google Guice 2:Mental Model》,讲述了Guice的建模思路:
Guice is a map
-
Guice官网认为:binding是一个对象,它对应Guice map中的一个entry,通过创建binding就可以向Guice map中添加entry
A binding is an object that corresponds to an entry in the Guice map. You add new entries into the Guice map by creating bindings.
-
创建binding的方法主要有两种:
- explicit binding(显式绑定):继承
AbstractModule
自定义Module,在Module中使用bind()
或者@Provides
定义的binding。 - JIT binding(隐式绑定):除显式绑定外的其他绑定,被称作隐式绑定
- explicit binding(显式绑定):继承
-
Guice提供了丰富的binding创建方式,结合去具体的使用场景去学习这些binding,才不容易迷糊
2. Linked Bindings
- 有两种方式可以创建Linked binding,一种是通过
bind().to()
, 另一种是通过@Provides method
作用一:type到implementationde的映射
map a type to its implementation
,例如,将接口映射到其实现bind().to()
创建Linked binding:public class LogModule extends AbstractModule { @Override protected void configure() { // TransactionLog为一个接口,DatabaseTransactionLog是它的一个实现类 bind(TransactionLog.class).to(DatabaseTransactionLog.class); } }
@Provides method
创建Linked binding:// 示例1:@Provides method,自己负责实例对象的创建 @Provides public TransactionLog providerTransactionLog() { return new DatabaseTransactionLog(); } // 示例2:通过方法入参传入实例对象,该对象由Guice通过JIT binding隐式创建 @Provides public TransactionLog providerTransactionLog(DatabaseTransactionLog impl) { return impl; }
- 从Guice获取TransactionLog类型的实例时,将获取到DatabaseTransactionLog实例
public static void main(String[] args) { Injector injector = Guice.createInjector(new LogModule()); TransactionLog log = injector.getInstance(TransactionLog.class); log.log("Create a table named olap.cluster"); }
作用二:多个Linked binding形成链条
-
例如,接口A —> 实现类B —>实现类的子类C,从Guice获取A类型的实例,最终将获取到C的实例
bind().to()
:bind(TransactionLog.class).to(DatabaseTransactionLog.class); // 形成一个绑定链,TransactionLog映射到TidbDatabaseTransactionLog bind(DatabaseTransactionLog.class).to(TidbDatabaseTransactionLog.class);
@Provides method
@Provides public TransactionLog providerTransactionLog(DatabaseTransactionLog impl) { return impl; // 必须使用Guice自动注入的DatabaseTransactionLog,不能使用 new DatabaseTransactionLog() } @Provides public DatabaseTransactionLog providerDatabaseTransactionLog(TidbDatabaseTransactionLog subClass) { return subClass; }
-
其中,作用一可以看做单个节点的binding链条,这两种使用场景下的binding叫做Linked binding是OK的 😁 😁 😁
3. Binding Annotations
- 有时,同一个类型需要有多个绑定。例如,数据库需要使用DatabaseTransactionLog打印日志,数仓需要使用WarehouseTransactionLog打印日志
- 这时,可以为不同场景添加不同的绑定注解(binding annotation),注解和类型(type)一起标识一个唯一的binding
方法一:自定义绑定注解
1. 自定义绑定注解
-
自定义的绑定注解,必须使用
@Qualifier
或者@BindingAnnotation
进行标识,以告诉Guice这是一个绑定注解 -
其中,
@Qualifier
是JSR-330的元注解,@BindingAnnotation
则是Guice具有同样效果的注解。 -
建议使用
@Qualifier
标识绑定注解,这样更具通用性 -
为数据库和数仓两种使用场景,自定义绑定注解
@Target({FIELD, PARAMETER, METHOD}) // 注解可以使用在哪些地方 @Retention(RUNTIME) // Guice要求该注解在运行时可见 @Qualifier // 告诉Guice这是一个注解绑定 public @interface Database { } @Target({FIELD, PARAMETER, METHOD}) @Retention(RUNTIME) @Qualifier public @interface Warehouse { }
2. 使用绑定注解
-
在MyDatabase中,以constructor injection的方式注入TransactionLog,且TransactionLog使用
@Database
标识public class MyDatabase { private TransactionLog log; @Inject // 使用constructor injection public MyDatabase(@Database TransactionLog log) { // 使用@Database标识需要注入的TransactionLog this.log = log; } public void createTable(String tableName) { log.log(format("Success to create table %s in database", tableName)); } }
-
在MyWarehouse中,以Field injection的方式注入TransactionLog,且TransactionLog使用
@Warehouse
标识public class MyWarehouse { @Inject // 使用Field injection @Warehouse // 使用@Warehouse标识需要注入的TransactionLog private TransactionLog log; public void createTable(String tableName) { log.log(format("Success to create table %s warehouse", tableName)); } }
3. 创建binding
-
使用带
annotatedWith()
的bind().to()
语句,定义TransactionLog与DatabaseTransactionLog之间的映射关系bind(TransactionLog.class).annotatedWith(Database.class).to(DatabaseTransactionLog.class);
-
这时,从Guice map中获取DatabaseTransactionLog的key如下:
Key.get(TransactionLog.class, Database.class);
-
根据binding,Guice会向MyDatabase注入DatabaseTransactionLog
-
使用带
@Warehouse
的@Provides method
,定义TransactionLog与WarehouseTransactionLog之间的映射关系// Guice自动注入WarehouseTransactionLog @Provides @Warehouse public TransactionLog providerWarehouseTransactionLog(WarehouseTransactionLog log) { return log; }
-
这时,从Guice map中获取WarehouseTransactionLog的key如下:
Key.get(TransactionLog.class, Warehouse.class)
-
根据binding,Guice会向MyWarehouse注入WarehouseTransactionLog
4. 使用binding
- 从Gucie中获取实例,将使用到上述binding
public class Main { public static void main(String[] args) { Injector injector = Guice.createInjector(new LogModule()); MyDatabase database = injector.getInstance(MyDatabase.class); database.createTable("olap.users"); MyWarehouse warehouse = injector.getInstance(MyWarehouse.class); warehouse.createTable("tpch_300x.orders"); } }
- 执行结果如下
5. 优缺点
- 优点: 代码编译时,会校验绑定注解(如是否存在该注解)。编译时发现的错误,更容易修改
- 缺点:
- 如果一个类型需要绑定的很多,则需创建大量的注解
- 这不仅会增加代码开发的工作量,还不容易区分,甚至需要文档加以说明
方法二:@Named(Guice内置的绑定注解)
1. 使用方法及优缺点
-
针对上面的问题,Guice提供了一个内置的绑定注解
@Named
,可以使用@Named("bindingName")
替代自定义的绑定注解@Retention(RUNTIME) @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) @BindingAnnotation public @interface Named { String value(); // 元素名称为value,使用时可以直接赋值 }
-
例如,上面的数据库场景,使用@Named注解改造如下:
// 1. @Named("database")标识需要注入的TransactionLog @Inject public MyDatabase(@Named("database") TransactionLog log) { this.log = log; } // 2. 创建TransactionLog与DatabaseTransactionLog映射关系 // @Named("database")与TransactionLog一起唯一标识DatabaseTransactionLog bind(TransactionLog.class).annotatedWith(Names.named("database")).to(DatabaseTransactionLog.class);
-
此时,从Guice map中获取DatabaseTransactionLog的key如下:
Key.get(TransactionLog.class, Names.named("database"))
-
注意:
@Named
注解的值为String类型,需要与Names.named()
的值保持一致 -
这也是使用@Named注解的缺点:编译器无法检查二者的值是否一致,只有运行起来后才可能触发错误
-
相对编译时错误,运行时发现的错误一般更难排查。因此,Guice建议谨慎使用
@Named
注解
2. 一些疑问,帮助理解@Named的实现原理
- 对比自定义绑定注解和@Named的使用可以发现:@Named提供了一个快速定义绑定注解的方式,其定义的绑定注解可以使用
Names.named()
进行“标识”
疑问:使用@Named时,为何没有使用annotatedWith(Named.class)
声明注解?
-
通过查看源码也可知,自定义绑定注解和@Named在定义binding时,使用的
annotatedWith()
方法是不同的// 传入注解的类型,自定义绑定注解使用该方法 LinkedBindingBuilder<T> annotatedWith(Class<? extends Annotation> annotationType); // 传入注解对象,@Named使用该方法 LinkedBindingBuilder<T> annotatedWith(Annotation annotation);
-
笔者的理解:
- @Database是一个不含任何元素的注解,可以直接通过其类型
Database.class
进行匹配到 - @Named是一个带有元素(
value
)的注解,如果不使用带有元素值的annotation对象,则无法精确匹配
- @Database是一个不含任何元素的注解,可以直接通过其类型
-
如果给annotatedWith()方法传入Named.class,从Guice map中获取DatabaseTransactionLog的key如下:
Key.get(TransactionLog.class, Named.class)
-
因此,任何使用@Named标识的TransactionLog,都将被Guice传入DatabaseTransactionLog
bind(TransactionLog.class).annotatedWith(Named.class).to(DatabaseTransactionLog.class);
-
将MyWarehouse修改如下:取消Field injection,转为带有@Named的constructot injection
public class MyWarehouse { private TransactionLog log; @Inject public MyWarehouse(@Named("warehouse") TransactionLog log) { this.log = log; } ... // 其他代码省略 }
-
最终执行结果如下,可见无论@Named的元素值为多少,最终都将按照类型做泛匹配,而非按照对象做精确匹配
疑问: Names.named("xxx")
创建的是什么对象?
-
查看
Names
的源码,发现类注释的第一句为:Utility methods for use with @Named.
-
Names只有一个
private
类型的构造函数,但提供了一个public static named()
方法,用于创建NamedImpl
对象public class Names { private Names() {} // Creates a Named annotation with name as the value. public static Named named(String name) { return new NamedImpl(name); } // 省略bindProperties()方法的定义 }
-
查看NamedImpl的源码如下,NamedImpl实现了@Named
class NamedImpl implements Named, Serializable { private final String value; public NamedImpl(String value) { this.value = checkNotNull(value, "name"); } @Override public String value() { return this.value; } ... // 其他方法省略 @Override public Class<? extends Annotation> annotationType() { return Named.class; } private static final long serialVersionUID = 0; }
-
同时,NamedImpl的访问权限为包访问权限,用户代码中无法使用
new NamedImpl("xxx")
创建对象 -
为此,Guice开发人员提供了工具类
Names
,以允许应用开发者创建NamedImpl对象
疑问: 需要Annotation类型的对象,为何却传入NamedImpl对象?
- 学习Java注解时,曾提到过:注解的本质是一个继承了
java.lang.annotation.Annotation
接口的接口 - Annotation和Named都是接口,
Named extends Annotation
→ \rightarrow →NamedImpl implements Named
- 根据Java的继承与多态特性,NamedImpl可以向上转型,是Annotation类型的对象(
可能描述不准确,欢迎交流
)
3. @Named的实现原理
- 通过对上面疑问的解答,我们可以总结出@Named的实现过程
- 定义带有元素value的@Named
- 定义NamedImpl类,实现Named
- 定义工厂类Names,内含创建NamedImpl对象的named()方法
- @Named(“xxx”)与Names.named(“xxx”)对应的Named对象相等,就实现了Guice map中entry的精准匹配
- 其中,最关键的就是NamedImpl如何实现Named注解
- Annotation接口定义了与Object类相同的
equals()
、hashCode()
、toString()
方法,但对这些方法有不同的约定 - 在注解的实现类中,只有按照约定重写与对象相等判断有关的
equals()
和hashCode()
方法,才能实现注解对象的相等判断 - 例如,NamedImpl就严格按照约定重写了Annotation接口的中方法,才使得@Named(“xxx”)与Names.named(“xxx”)相等,从而实现Guice map中entry的精准匹配
- 关于Annotation接口,以及如何implements注解,可以参考博客:《Java的Annotation接口》
方法三:一个较为完美的绑定注解
-
相对@Named,自定义绑定注解支持编译时check,能尽早发现问题。但是,自定义绑定注解会增加代码开发的工作量,甚至需要文档加以说明
-
结合二者的优缺点,能否实现一个能支持元素值校验的@Named注解?
-
这时,可以考虑将元素定义为枚举类型,以借助编译时check尽早发现bug
-
定义枚举类,描述Log的类型
public enum Logger { DATABASE, WAREHOUSE, DATA_LAKE, UNKNOWN }
-
定义注解,内含枚举类型的元素值
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface LogProvider { Logger type() default Logger.UNKNOWN; }
-
定义注解的实现类,按照规定重写Annotation中的方法
class LogProviderImpl implements LogProvider { private final Logger type; public LogProviderImpl(Logger type) { this.type = type; } @Override public int hashCode() { return (127 * "type".hashCode()) ^ type.hashCode(); } @Override public boolean equals(Object obj) { if (!(obj instanceof LogProvider)) { return false; } LogProvider other = (LogProvider) obj; return this.type.equals(other.type()); } @Override public String toString() { return "@" + LogProvider.class.getName() + "(" + Annotations.memberValueString("type", type) + ")"; } @Override public Logger type() { return this.type; } @Override public Class<? extends Annotation> annotationType() { return LogProvider.class; } }
-
定义工具类,提供创建注解对象的工具方法
public class LogProviders { private LogProviders() { } public static LogProvider provider(Logger type) { return new LogProviderImpl(type); } }
-
使用@LogProvider
// MyDatabase中使用@LogProvider @Inject public MyDatabase(@LogProvider(type = Logger.DATABASE) TransactionLog log) { this.log = log; } // MyWarehouse中使用@LogProvider @Inject @LogProvider(type = Logger.WAREHOUSE) private TransactionLog log;
-
将元素定义为枚举类型,任何的拼写错误或使用不存在的值,IDE都会有提示,代码编译也无法通过
-
定义binding:除了使用
annotatedWith()
定义binding,还使用@Provides method定义bindingbind(TransactionLog.class).annotatedWith(LogProviders.provider(Logger.DATABASE)).to(DatabaseTransactionLog.class); @Provides @LogProvider(type = Logger.WAREHOUSE) public TransactionLog providerWarehouseTransactionLog(WarehouseTransactionLog log) { return log; }
-
重新执行Main类中的main()方法,成功通过@LogProvider匹配到对应的Log