目的:
直接调用new -
直接耦合具体的实现类。这是最不灵活的方式,直接紧密耦合,当需要置换具体实施方式的时候(比如在测试中不能用真正的服务)会遇到麻烦。
使用 Factory
用户调用工厂 .getInstance() 方法得到具体实现。间接耦合。缺点是每次使用前都要设置工厂,以便得到想要的实现。每个接口都要有相应的工厂。如果对象增加依赖,要记得在每一处需要的地方(比如每个 Unit Test)设置工厂。如果忘记设置/初始化工厂,仅仅在要用到服务的时候才会出错。
初始化时注入
在初始化一个对象的时候给出所有的依赖对象。好处是(1)间接耦合,对象仅仅关心接口。(2)当对象需要新增加一个依赖的时候,编译器会强制每个用户给出该依赖的实现对象。缺点是,现在对象的用户需要关心所有的初始化和依赖。
Dependency Injection with Guice
首先,是一个配置或者说映射。谢天谢地,Google 也恨 XML。所以,Google 用一个 java class 来做配置模块。
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
bind(BillingService.class).to(RealBillingService.class);
}
}
看起来很简单。就是把接口映射到具体的实现的类。再来看需要被注入依赖实例的类:
public class RealBillingService implements BillingService {
private final CreditCardProcessor processor;
private final TransactionLog transactionLog;
@Inject
public RealBillingService(CreditCardProcessor processor,
TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
}
}
也很简单,一个 @Inject 就可以了。最后,最终用户需要从配置模块生成 Injector,再从 Injector 得到需要的实例。
public static void main(String[] args) {
Injector injector = Guice.createInjector(new BillingModule());
BillingService billingService = injector.getInstance(BillingService.class);
}
Bindings
首先是各种 Binding,也就是说如何配置想注入的对象。Module 是由 Bindings 组成的。有以下几种Binding:
Linked Binding:
例子: bind(Interface.class).to(Implementation.class)
用处: 把实现连接到接口,或者把子类连接到父类(慎用)。甚至可以 A 连接到 B,B 连接到 C 这样地串联起来。这样,当要 A 的时候,得到 C。
Binding Annotations
先定义一个标注
@BindingAnnotation @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME)
public @interface PayPal {}
然后在定义 @Inject 的时候同时使用标注
@Inject
public RealBillingService(@PayPal CreditCardProcessor processor,
TransactionLog transactionLog) {
...
}
最后,在 Binding 的时候使用标注来说明想要注入的是哪个实例
bind(CreditCardProcessor.class)
.annotatedWith(PayPal.class)
.to(PayPalCreditCardProcessor.class);
如果不想自己写标注,也能容忍字符匹配,可以用@named标注:
@Inject
public RealBillingService(@Named("Checkout") CreditCardProcessor processor,
TransactionLog transactionLog) {
...
}
bind(CreditCardProcessor.class)
.annotatedWith(Names.named("Checkout"))
.to(CheckoutCreditCardProcessor.class);
也可以自己写带有参数的标注。先写标注接口,再写标注实例,注意实现 equals() 和 hashCode(),就可以用在 annotatedWith 里面了。
用处:当不同的地方要不同的实例的时候。
Instance Binding
提供一个简单的实例,而不是去初始化。通常就是简单类型,做配置用。
bind(String.class)
.annotatedWith(Names.named("JDBC URL"))
.toInstance("jdbc:mysql://localhost/pizza");
bind(Integer.class)
.annotatedWith(Names.named("login timeout seconds"))
.toInstance(10);
@Provides Methods
在配置模块里面写 @Provides 方法。
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
...
}
@Provides
TransactionLog provideTransactionLog() {
// 这个方法生成实例并且返回。
}
}
@Provides 也可以带有标注,或者有参数的标注比如 @Named("Checkout")。这样这个标注就会被绑定在这个@Provider方法上了。
@Provides @PayPal
CreditCardProcessor providePayPalCreditCardProcessor(
@Named("PayPal API key") String apiKey) {
// 具体实现。。。
}
总之,Guice 以 @Provider 函数返回的类型以及标注来判断该调用哪个函数来得到实例。
Provider Bindings
@Provider 写得太多,模块就会变得太大,而且太杂乱。这时候就可以写 Provider。Provider 需要实现这个简单的接口:
public interface Provider<T> {
T get();
}
Provider 也可以有自己的依赖关系。然后,就可以在Binding 里面使用了。
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class)
.toProvider(DatabaseTransactionLogProvider.class);
}
Untargetted Bindings
这个最简单了,不需要指派绑定到的具体实现。这个需要接口指定了默认的
@ImplementedBy (类)或者 @ProvidedBy (Provider类)。
bind(MyConcreteClass.class);
bind(AnotherConcreteClass.class).in(Singleton.class);
Built-in Bindings
这些是Guice提供的“拿来就用”的绑定,很方便。比如
java.util.logging.Logger。Guice 还会帮助做些设置。其他的似乎 Guice 也不推荐或者详细说明,就不研究了。
@Inject
public ConsoleTransactionLog(Logger logger) {
this.logger = logger;
}
Just-in-Time Bindings
就是没有在模块里面说明,而是在需要注入的时候决定的。首先是默认构造函数,如果一个类有默认构造函数(就是没有参数的),那么Guice就用它。其次是接口指定了默认的
@ImplementedBy (类)或者 @ProvidedBy (Provider类)。
@ImplementedBy(PayPalCreditCardProcessor.class)
public interface CreditCardProcessor {
ChargeResult charge(String amount, CreditCard creditCard)
throws UnreachableException;
}
@ProvidedBy(DatabaseTransactionLogProvider.class)
public interface TransactionLog {
void logConnectException(UnreachableException e);
void logChargeResult(ChargeResult result);
}
这种默认的接口实现可以在使用时,由明确的 Binding 取代。而如果没有被取代,就用默认。
Scope
上面讲了绑定。下面讲 Scope。Scope 就是在什么范围内可以共享同一个实例。比如@Singleton就在整个应用中使用同一个实例。Scope 可以这样指定:
标注在实现类上:
@Singleton
public class InMemoryTransactionLog implements TransactionLog {
/* everything here should be threadsafe! */
}
在Binding中明确表示
bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);
在 @Provides 方法中
@Provides @Singleton
TransactionLog provideTransactionLog() {
...
}
注意 Scope 是按照要得到的类型(接口类型)而不是具体实现的类而计算的。比如,Applebees 实现了 Bar 和 Grill 两个接口,下面将会用两个 Applebees 实例。
bind(Bar.class).to(Applebees.class).in(Singleton.class);
bind(Grill.class).to(Applebees.class).in(Singleton.class);
想只用一个实例,就这样加上一条规则:
bind(Applebees.class).in(Singleton.class);
在 in 子句里面可以用
RequestScoped.class 或者
ServletScopes.REQUEST。用前者比较好,因为表示的不只是 Servlet 中的 Scope。
默认的 Scope 是每一次 Guice 都生成一个新实例。其他Scope 还有 Singleton, Request, Session。
确定为 Singleton 的实例可能在启动的时候生成,这样可以早发现问题,但可能增加启动时间。
选择 Scope 要看实例是不是有状态,该状态要在什么范围共享,以及创建实例要花费的代价。 @Singleton 和 @SessionScoped 必须线程安全。而 @RequestScoped 不需要。
注入
注入可以在多处进行。首先,是前面用过的在构造函数中(推荐,编译器检查,好测试):
public class RealBillingService implements BillingService {
private final CreditCardProcessor processorProvider;
private final TransactionLog transactionLogProvider;
@Inject
public RealBillingService(CreditCardProcessor processorProvider,
TransactionLog transactionLogProvider) {
this.processorProvider = processorProvider;
this.transactionLogProvider = transactionLogProvider;
}
可以在方法中(仅仅类型影响Guice,方法名字不重要。比构造函数灵活。):
public class PayPalCreditCardProcessor implements CreditCardProcessor {
private static final String DEFAULT_API_KEY = "development-use-only";
private String apiKey = DEFAULT_API_KEY;
@Inject
public void setApiKey(@Named("PayPal API key") String apiKey) {
this.apiKey = apiKey;
}
也可以在 Field 中(不推荐)
public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
@Inject Connection connection;
public TransactionLog get() {
return new DatabaseTransactionLog(connection);
}
}
Method 和 Field 还可选。如果找不到,Guice 就不注入。
@Inject(optional=true)
public void setApiKey(@Named("PayPal API key") String apiKey) {
this.apiKey = apiKey;
}
另外,对于一个已经初始化了的实例,Guice的injector还有一个injectMembers方法。
Static Injections 手册对于新写的代码不推荐,不再研究。
Automatic Injection :对于作为参数传给了 toInstance()方法的实例(Instance Binding),还有传给了 toProvider()方法的实例(Provider Binding),如果需要,Guice都会自动去注入。
另外,也可以将一个 Provider 注入给用户,由用户自己决定什么时候去调用 Provider 去取得实例。这样可以获得多个实例,或者实现 lazy loading。
AOP
对于有Guice生成的实例,Guice还可以进行方法拦截。具体不再叙述,看手册的例子。