本文适合对依赖注入有相对了解的读者,文章中对于部分名词未作详细解释。对于没有恰当的中文与之对应的英文内容,遂未翻译
Guice简介
Guice 简介,本文中的内容也是参考该文档完成,如有不一致,以该文为准。
快速上手
作为示例,我们使用 BillingService
,它依赖于 CreditCardProcessor
和 TransactionLog
两个接口。接下来我们看看如何使用Guice:
class BillingService {private final CreditCardProcessor processor;private final TransactionLog transactionLog;@InjectBillingService(CreditCardProcessor processor, TransactionLog transactionLog) {this.processor = processor;this.transactionLog = transactionLog;}public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {...}
}
我们将会把 PaypalCreditCardProcessor 和 DatabaseTransactionLog 注入BillingService。
Guice 使用 bindings
将类型和实现对应起来。module
是特定的 bindings
的集合。
public class BillingModule extends AbstractModule {@Override protected void configure() {/***这是告诉Guice,当遇到一个对象依赖于TransactionLog时,*将DatabaseTransactionLog注入*/bind(TransactionLog.class).to(DatabaseTransactionLog.class);/**同上*/bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);}
}
现在可以编写main方法了:
public static void main(String[] args) {/** Guice.createInjector() takes your Modules, and returns a new Injector* instance. Most applications will call this method exactly once, in their* main() method.*/Injector injector = Guice.createInjector(new BillingModule());/** Now that we've got the injector, we can build objects.*/BillingService billingService = injector.getInstance(BillingService.class);...}
大功告成!!!
Bindings
injector 的职责是确定系统中需要构造哪些对象,解析依赖,装配对象(将被依赖的对象注入依赖的对象)。那么如何指定依赖的解析方式,答案是使用 bindings 配置你的 injector
创建bindings
继承 AbstractModule
重写 configure
方法。在该方法中调用 bind()
便创建了一个binding。完成module之后,调用 Guice.createInjector()
,将module作为参数传入,便可获得一个injector对象。
Linked Bindings
Linked bindings 将一个实现绑定到一个类型。
下面例子将 DatabaseTransactionLog
绑定到接口 TransactionLog
public class BillingModule extends AbstractModule {@Override protected void configure() {bind(TransactionLog.class).to(DatabaseTransactionLog.class);}
}
绑定之后,当你调用 injector.getInstance(TransactionLog.class)
或当injector遇到一个对象依赖与 TransactionLog
,它便会使用 DatabaseTransactionLog
。链接可以建立于接口和其实现类,或者子类和父类之间。Linked bindings 可以链式使用。
public class BillingModule extends AbstractModule {@Override protected void configure() {bind(TransactionLog.class).to(DatabaseTransactionLog.class);bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);}
}
在这种情况下,当一个对象依赖于 TransactionLog
,injector将会返回一个 MySqlDatabaseTransactionLog
.
BindingAnnotations
Binding Annotations
有时候,你想要给特定的类型绑定多个实现。例如,你想给 CreditCardProcessor
同时绑定 PaypalCreditCardProcessor
和 CheckoutCreditCardProcessor
两个实现. Guice 通过binding annotation满足上述需求。注解和类型共同确定了一个唯一的binding。 在这儿,注解和类型构成了Key
。
首先我们定义一个注解:
import com.google.inject.BindingAnnotation;
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;@BindingAnnotation @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME)
public @interface PayPal {}
然后使用我们定义的注解标示需要注入的类型。
public class RealBillingService implements BillingService {@Injectpublic RealBillingService(@PayPal CreditCardProcessor processor,TransactionLog transactionLog) {...}
最后我们还需要创建相应的binding。
bind(CreditCardProcessor.class).annotatedWith(PayPal.class).to(PayPalCreditCardProcessor.class);
@Named
也并不是必须创建自己的注解,Guice提供了一个内建的注解@Named
。用法如下:
public class RealBillingService implements BillingService {@Injectpublic RealBillingService(@Named("Checkout") CreditCardProcessor processor,TransactionLog transactionLog) {...}bind(CreditCardProcessor.class).annotatedWith(Names.named("Checkout")).to(CheckoutCreditCardProcessor.class);
因为编译器不能对String类型进行检查,所以不建议使用@Named
Instance Bindings
你可以将一个特定类型的实例绑定到该类型。
bind(String.class).annotatedWith(Names.named("JDBC URL")).toInstance("jdbc:mysql://localhost/pizza");
bind(Integer.class).annotatedWith(Names.named("login timeout seconds")).toInstance(10);
尽量避免给创建起来比较复杂的对象使用 .toInstance
方法,那样会导致应用启动比较慢。可以使用 @Provides
代替该方法。
Provides Methods
当你需要编写创建对象的代码,使用 @Provides
方法。该方法只能定义在module中。并且需要使用 @Provides
修饰。他的返回值是一个对象。当Injector需要某个类型的实例时,便会调用相应的@Provides
方法。
public class BillingModule extends AbstractModule {@Overrideprotected void configure() {...}@ProvidesTransactionLog provideTransactionLog() {DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();transactionLog.setJdbcUrl("jdbc:mysql://localhost/pizza");transactionLog.setThreadPoolSize(30);return transactionLog;}
}
如果 @Provides
方法有binding annotation ,比如@Paypal
或者 @Named("Checkout")
,Guice 也是可以的。所有的被依赖对象以参数形式传入该方法即可。
@Provides @PayPalCreditCardProcessor providePayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) {PayPalCreditCardProcessor processor = new PayPalCreditCardProcessor();processor.setApiKey(apiKey);return processor;}
需要注意的是, Guice不允许 @Provides
方法抛出异常。
Provider Bindings
当 @Provides
方法比较复杂时,你也许会考虑将该方法转移到一个单独的类中。Provider类继承Guice的 Provider
接口。 Provider
接口定义如下:
public interface Provider<T> {T get();
}
我们的Provider实现类有自己的依赖,所有的依赖是通过被@Inject
修饰的构造函数接收的。
public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {private final Connection connection;@Injectpublic DatabaseTransactionLogProvider(Connection connection) {this.connection = connection;}public TransactionLog get() {DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();transactionLog.setConnection(connection);return transactionLog;}
}
绑定
public class BillingModule extends AbstractModule {@Overrideprotected void configure() {bind(TransactionLog.class).toProvider(DatabaseTransactionLogProvider.class);}
Untargeted Bindings
一些情况下,你需要创建bindings,但是却不能指定具体的实现。这个对于被@ImplementedBy
或者 @ProvidedBy
修饰的具体类或者类型特别有用。An untargetted binding informs the injector about a type, so it may prepare dependencies eagerly. Untargetted bindings have no to clause, like so:
bind(MyConcreteClass.class);
bind(AnotherConcreteClass.class).in(Singleton.class);
当指定binding annotation时,必须加上绑定的目标。
bind(MyConcreteClass.class).annotatedWith(Names.named("foo")).to(MyConcreteClass.class);
bind(AnotherConcreteClass.class).annotatedWith(Names.named("foo")).to(AnotherConcreteClass.class).in(Singleton.class);
Constructor Bindings
有时候, 我们需要绑定一个类型到任意的的构造函数。以下情况会有这种需求:@Inject
注解无法被应用到目标构造函数。或者该类型是一个第三方类。或者该类型中有多个构造函数参与依赖注入。
针对这个,Guice 有 @toConstructor()
类型的绑定方式。
public class BillingModule extends AbstractModule {@Override protected void configure() {try {bind(TransactionLog.class).toConstructor(DatabaseTransactionLog.class.getConstructor(DatabaseConnection.class));} catch (NoSuchMethodException e) {addError(e);}}
}
JustInTimeBindings
Just-in-time Bindings
当Injector需要一个类型的实例的时候,它需要一个binding。 如果这个binding在一个module中被创建,那么这个binding是显式binding,此时injector在每次需要该类型实例时,都使用该实例。但是如果Injector需要一个类型的实例,但是这个类型并没有对应的显式binding。此时injector会尝试创建一个Just-in-time binding。也叫JIT binding或者隐式binding。
合格的构造函数
Guice会使用具体类型的可注入构造函数创建binding。可注入构造函数需要是非private,无参数的或者该构造函数被 @Inject
修饰。
public class PayPalCreditCardProcessor implements CreditCardProcessor {private final String apiKey;@Injectpublic PayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) {this.apiKey = apiKey;}
@ImplementedBy
告诉injector,该类型的默认实现类。
@ImplementedBy(PayPalCreditCardProcessor.class)
public interface CreditCardProcessor {ChargeResult charge(String amount, CreditCard creditCard)throws UnreachableException;
}
上述代码和一下代码是等价的:
bind(CreditCardProcessor.class).to(PayPalCreditCardProcessor.class);
如果某个类型同时使用 bind()
和 @ImplementedBy
,bind()
会生效。
@ProvidedBy
告诉Injector,产生该类型的Provider类
@ProvidedBy(DatabaseTransactionLogProvider.class)
public interface TransactionLog {void logConnectException(UnreachableException e);void logChargeResult(ChargeResult result);
}
上述代码等价于:
bind(TransactionLog.class).toProvider(DatabaseTransactionLogProvider.class);
如果同时使用 @ProvidedBy
和 bind()
, bind()
会生效。
Scopes
默认情况下,Guice 每次都会返回一个新创建的对象。不过这也是可以配置的,以便于我们重用实例。
配置Scopes
Guice 使用注解标示Scope。例如:
@Singleton
public class InMemoryTransactionLog implements TransactionLog {/* everything here should be threadsafe! */
}@Provides @Singleton
TransactionLog provideTransactionLog() {...
}
上例中,@Singleton
标示该类型只能有一个实例。并且是线程安全的。
Scope也可以通过代码来配置:
bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);
如果某个类型已经被注解标注了scope,同时还使用bind()
方法配置了scope,那么后者生效。如果一个类型已经被注解配置了scope而你不想那样,你可以使用 bind()
覆盖。
预加载的单例
bind(TransactionLog.class).to(InMemoryTransactionLog.class).asEagerSingleton();
Injections
构造函数注入
这种情况下,需要用 @Inject
标注构造函数。构造函数同时需要将所依赖的类型作为其参数。通常情况下,都是将传入的参数复制给类的final成员。
public class RealBillingService implements BillingService {private final CreditCardProcessor processorProvider;private final TransactionLog transactionLogProvider;@Injectpublic RealBillingService(CreditCardProcessor processorProvider,TransactionLog transactionLogProvider) {this.processorProvider = processorProvider;this.transactionLogProvider = transactionLogProvider;}
如果没有 @Inject
标注的构造函数,Guice 会使用共有的午餐构造函数(如果存在)。
方法注入
这种情况下,需要使用@Inject
标注方法,该方法的参数为需要注入的类型。
public class PayPalCreditCardProcessor implements CreditCardProcessor {private static final String DEFAULT_API_KEY = "development-use-only";private String apiKey = DEFAULT_API_KEY;@Injectpublic void setApiKey(@Named("PayPal API key") String apiKey) {this.apiKey = apiKey;}
字段注入
这种情况下,需要使用 @Inject
标注字段。
public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {@Inject Connection connection;public TransactionLog get() {return new DatabaseTransactionLog(connection);}
}
可选的注入
有时候,我们的依赖项不是必须的,如果系统中存在依赖项则注入,如果不存在,也不强制要求注入。这种情况在方法注入和字段注入中都是适用的。 启用可选注入,只需要使用 @Inejct(optional=true)
标注字段或方法即可。
public class PayPalCreditCardProcessor implements CreditCardProcessor {private static final String SANDBOX_API_KEY = "development-use-only";private String apiKey = SANDBOX_API_KEY;@Inject(optional=true)public void setApiKey(@Named("PayPal API key") String apiKey) {this.apiKey = apiKey;}
不过,如果混用可选注入和Just-in-time bindings,可能会产生奇怪的接口。例如:
@Inject(optional=true) Date launchDate;
上面代码中的date总是会被成功注入即使没有为他创建对应的显示binding,因为它有无参构造函数,Guice会为他创建Just-in-time bindings。
On-demand注入
方法和字段注入可以用来初始化一个现存的实例。我们可以使用Injector.injectMember()
API:
public static void main(String[] args) {Injector injector = Guice.createInjector(...);CreditCardProcessor creditCardProcessor = new PayPalCreditCardProcessor();injector.injectMembers(creditCardProcessor);
AOP
Guice 支持方法拦截器。这里直接看一个实例:
假如我们需要禁止在周末调用特定方法
为了标注我们在周末禁止调用的方法,我们定义一个注解类型:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@interface NotOnWeekends {}
然后使用该注解标注我们方法
public class RealBillingService implements BillingService {@NotOnWeekendspublic Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {...}
}
现在我们定义我们的拦截器,我们的拦截器需要实现org.aopalliance.intercept.MethodInterceptor
接口。
public class WeekendBlocker implements MethodInterceptor {public Object invoke(MethodInvocation invocation) throws Throwable {Calendar today = new GregorianCalendar();if (today.getDisplayName(DAY_OF_WEEK, LONG, ENGLISH).startsWith("S")) {throw new IllegalStateException(invocation.getMethod().getName() + " not allowed on weekends!");}return invocation.proceed();}
}
然后配置我们的拦截器
public class NotOnWeekendsModule extends AbstractModule {protected void configure() {/**在这里,我们匹配所有的类,但是只匹配类中有NotOnWeekends的方法*/bindInterceptor(Matchers.any(), Matchers.annotatedWith(NotOnWeekends.class), new WeekendBlocker());}
}
所有工作就完成了。
注入拦截器
如果需要注入拦截器,使用 `requestInjection` API
public class NotOnWeekendsModule extends AbstractModule {protected void configure() {WeekendBlocker weekendBlocker = new WeekendBlocker();requestInjection(weekendBlocker);bindInterceptor(Matchers.any(), Matchers.annotatedWith(NotOnWeekends.class), weekendBlocker);}
}
另外一种方式是使用 `Binder.getProvider`,将依赖的内容传入拦截器的构造函数。
public class NotOnWeekendsModule extends AbstractModule {protected void configure() {bindInterceptor(any(),annotatedWith(NotOnWeekends.class),new WeekendBlocker(getProvider(Calendar.class)));}
}
END
本人有意进一步阅读Guice源码,但是个人能力有限,如有感兴趣的读者,希望可以一起研究。