接上文,我们会发现在Guice的配置模块里面bind这个方法出现了很多次,这篇文章就带大家深入了解一下绑定的细节。
Injector的任务是把程序里的各种对象的依赖图组装起来,这么说有一点抽象,其实就是当你声明需要一个类型的实例的时候,然后injector解析出来这个实例依赖的对象,然后把所有需要的依赖都绑定起来,这就是绑定。如前文所示,建立一个绑定,继承AbstractModule
并重写configure
方法即可,在方法里面,调用bind
来具体声明每种绑定,最终把你的module传入Guice.createInjector()
的参数即可。
Guice的module可以建立linked binding,instance binding,provider binding,constructor binding和untargetted binding等等,接下来我们将一一讲解。
Linked Bindings
linked binding是最简单也是最基本的一种绑定,它把一种类型绑定到一个具体的实现类型上面,常见的绑定有把接口绑定到某个实现类:
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
}
}
这个时候当你调用injector.getInstance(TransactionLog.class)
, 或者当injector碰到了某个类依赖于TransactionLog
,它将使用DatabaseTransactionLog
。你甚至可以继续把具体的DatabaseTransactionLog
link到它的子类
bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);
这样当需要TransactionLog
的时候,injector将返回一个MySqlDatabaseTransactionLog
Instance Bindings
你还可以把某一类型绑定到具体的实例上面去,这对于那些自身没有任何自身的对象是有用的,比如value对象
bind(String.class)
.annotatedWith(Names.named("JDBC URL"))
.toInstance("jdbc:mysql://localhost/pizza");
bind(Integer.class)
.annotatedWith(Names.named("login timeout seconds"))
.toInstance(10);
声明了把通过@named("JDBC URL")
注解的String
类型绑定到某一个具体的值上去,下面的代码同样把Integer绑定到了某个具体值。annotatedWith是什么意思呢,下面来了解:
Binding Annotations
有的时候你对于同一类型需要有多个绑定,回想我们pizza订餐系统的例子,我们可能同时需要一个Paypal credit card processor和一个Square credit card processor这两种不同的支付手段。为了支持这种情况,bindings支持通过binding annotation和具体类型来确认一个唯一的绑定,这种annotation和type的配对,在Guice里叫做Key,key我们稍后再讲,先看绑定注解是怎么实现的吧:
@Qualifier
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface PayPal {}
首先我们需要定义一个自己的Java注解,常写Java的同学应该对此不陌生。然后当我们绑定具体的类型的时候:
public class RealBillingService implements BillingService {
@Inject
public RealBillingService(@PayPal CreditCardProcessor processor,
TransactionLog transactionLog) {
//...
}
我们在CreditCardProcessor
的前面加上@PayPal
注解即可,最后我们绑定的时候,只需加上annotatedWith
语句即可:
bind(CreditCardProcessor.class)
.annotatedWith(PayPal.class)
.to(PayPalCreditCardProcessor.class);
这样当我们还需要一个SquareCreditCardProcessor
的时候,只需做类似以上的步骤重新定义一个Square
的Java注解即可。
在前面的例子里大家可能注意到了@Named
这样一个注解,它是Guice的built-in的注解,它接受一个字符串作为参数
public class RealBillingService implements BillingService {
@Inject
public RealBillingService(@Named("Square") CreditCardProcessor processor,
TransactionLog transactionLog) {
//...
}
然后我们通过Names.named()来声明这样一个绑定
bind(CreditCardProcessor.class)
.annotatedWith(Names.named("Square"))
.to(SquareCreditCardProcessor.class);
这种方法提供了自定义Java注解以外的一种注解绑定模式,但是因为编译器没法check string,如果写错了字符串只有在运行时才会报错,所以我们推荐大家使用自定义Java注解的方式来进行注解绑定。
@Provides Methods
当你在做绑定的时候,需要写代码来创建一些对象,那么此时前面的直接绑定将不起作用,此时我们可以写一个带有@Provides
注解的方法:
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
//...
}
@Provides
TransactionLog provideTransactionLog() {
DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
transactionLog.setJdbcUrl("jdbc:mysql://localhost/pizza");
transactionLog.setThreadPoolSize(30);
return transactionLog;
}
}
这个方法必须写在继承了AbstractModule
的配置类里面,方法的返回类型就是绑定的具体类型,当injector需要那个类型的具体实例的时候,这个方法将被调用。Provides方法还可以把前面所讲的绑定注解结合起来
@PayPal
@Provides
CreditCardProcessor providePayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) {
PayPalCreditCardProcessor processor = new PayPalCreditCardProcessor();
processor.setApiKey(apiKey);
return processor;
}
具体的依赖可以像上面的例子一样通过Provides方法的参数传进来,在调用方法前injector会去解析这些依赖。
Provider Bindings
熟悉Java函数式编程的同学应该对Provider这个函数式接口不陌生,他的主要目的就是提供一个值
public interface Provider {
T get();
}
当我们在前面的例子中@Provides方法里的代码变得多又复杂的时候,我们可以考虑把他们挪到一个自己的类里面去,@Provides方法其实就是一种Provider模式。首先我们定义一个自己的Provider类,去实现Provider接口
public class DatabaseTransactionLogProvider implements Provider {
private final Connection connection;
@Inject
public DatabaseTransactionLogProvider(Connection connection) {
this.connection = connection;
}
public TransactionLog get() {
DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
transactionLog.setConnection(connection);
return transactionLog;
}
}
在Provider类中,我们可以使用@Inject任意注入依赖对象,get()方法返回的类型,就是具体绑定后的类型,在模块中,我们可以通过.toProvider()
来告知injector
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class)
.toProvider(DatabaseTransactionLogProvider.class);
}
}
当injector遇到TransactionLog
这个依赖的时候,Provider里的get()方法会被调用。
JustInTime Bindings
JustInTime,or JIT,在Java里叫做即时编译,使用这种技术,可以加速Java程序的执行速度。在Guice里,JustInTime Binding指的是那些自动被Guice建立好的绑定, 也叫implicit binding。几种主要的JIT binding包括:injectable constructors, 带有@ImplementedBy和@ProvidedBy注解的类型
injectable constructors
Guice可以为某一具体类型提供绑定:如果这个类型包含一个non-private的并且参数为空的constructor,或者拥有一个带有@Inject注解的constructor
public class PayPalCreditCardProcessor {
private final String apiKey;
@Inject
public PayPalCreditCardProcessor(@PaypalApiKey String apiKey) {
this.apiKey = apiKey;
}
比如在上面的例子里面我们从来不会去在哪个module里把PayPalCreditCardProcessor绑定起来,我们只需要绑定apiKey,然后通过injector.getInstance(PayPalCreditCardProcessor.class)
去调用即可,这是因为Guice已经帮我们做了这样一层绑定。
@ImplementedBy
@ImplementedBy(PayPalCreditCardProcessor.class)
public interface CreditCardProcessor {
ChargeResult charge(String amount, CreditCard creditCard);
}
上面的代码告诉injector,CreditCardProcessor
的默认实现是PayPalCreditCardProcessor
,相当于我们省去了
bind(CreditCardProcessor.class).to(PayPalCreditCardProcessor.class);
@ProvidedBy
@ProvidedBy(DatabaseTransactionLogProvider.class)
public interface TransactionLog {
void logChargeResult(ChargeResult result);
}
上面的代码告诉injector,这有一个DatabaseTransactionLogProvider
类来提供创建实例的逻辑,它相当于
bind(TransactionLog.class).toProvider(DatabaseTransactionLogProvider.class);
如果你不希望Guice使用这样隐含的绑定,那么可以通过下面的代码来告诉Guice,所有的绑定关系都必须由我来手动实现。
final class ExplicitBindingModule extends AbstractModule {
@Override
protected void configue() {
binder().requireExplicitBindings();
}
}
MultiBindings
MultiBinder
有的时候我们希望可以一下子罗列出来某个接口的一系列实现,MultiBinder支持了这样一个feature,首先我们在module里通过Multibinder.newSetBinder()来生成一个多绑定的集合,
public class SnacksModule extends AbstractModule {
protected void configure() {
Multibinder multibinder
= Multibinder.newSetBinder(binder(), Snack.class);
multibinder.addBinding().toInstance(new JianBingGuoZi());
multibinder.addBinding().toProvider(JiDanGuanBingProvider.class);
//...
}
}
然后这个Set
class SnackMachine {
@Inject
public SnackMachine(Set snacks) { ... }
}
你还可以把集合的不同bind分散在不同的module里面,当通过Guice去createInjector的时候分别建立不同的module,这样最终inject进来的也是Snack的一个集合,并且当你iterate这个set的时候,它的顺序就是bind的顺序,这个集合也是unmodifiable的。但是multiple bind必须保证bind的element是唯一且non null的。
MapBinder
MapBinder和MultiBinder类似,最终我们使用的时候也只需要inject就可以了
public class SnacksModule extends AbstractModule {
protected void configure() {
MapBinder mapBinder =
MapBinder.newMapBinder(binder(), String.class, Snack.class);
mapBinder.addBinding("JianBing").to(JianBingGuoZi.class);
//...
}
}
class SnackMachine {
@Inject
public SnackMachine(Map snacks) { ... }
}
类似的binder还有OptionalBinder,DefaultBinder等等,这里我们就不展开了。
AOP
Guice的injector绑定的细节基本就介绍完了,其他的大家可以参考Guice的官方文档。下面我们来说一说Guice对于AOP的支持。
AOP,全称是Aspect Oriented Programming,熟悉Spring的同学应该不陌生,中文名称叫做面向切面编程。首先什么叫做AOP呢:Guice支持方法的拦截,它可以让你写一些code并且当某个匹配的方法被调用的时候,这些code相应地被执行。
Aspect这次词,源于电影里面交叉剪辑
的技术,什么是交叉剪辑呢:是指把同一时间、不同空间发生的两条或多条情节线进行迅速而频繁的交叉剪辑,而且他们最终会汇聚成同一个焦点。比如说在电影里我们常见的“一分钟营救”。人质的身上绑着炸弹,而英勇的主人公正在驱车赶往,为了形成紧张的状态,剪辑师通常会在两边做交叉剪辑,最终主人公会来到人质身边,便是交叉剪辑。而人质绑着炸弹的场景和主人公营救的场景,都是一个切面,aspect。放在Java程序里面呢,AOP比较适合用于transaction,security和logging的场景,因为拦截器把程序划分成不同面,而非object,所以叫做aspect oriented programming。
Matcher在Guice里简单来说就是一个接口,它要么接受要么拒绝一个value,Guice支持AOP,首先你需要两个Matcher
:一个定义哪些类参与AOP,另一个定义了那些类里的具体方法。
MathodInterceptors
会被执---行当一个匹配的方法被调用的时候,他们可以检查这个调用:方法名,参数,接收的实例,等等。他会做类似交叉剪辑的逻辑,然后delegate给底层方法,然后还会检查底层方法返回的值或者异常。我们还是拿定pizza的例子来说,假设我们想在周末的时候禁止订餐系统的调用,因为周末外卖员休息,这个情景与使用AOP来做authorization验证类似:
首先我们定义一个注解,叫做非休息日
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface NotOnWeekends {}
然后我们把它应用在我们想要去拦截的方法上
public class RealBillingService implements BillingService {
@NotOnWeekends
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
//...
}
}
然后我们定义一个拦截器,它实现了org.aopalliance.intercept.MethodInterceptor
接口,并且我们通过调用invocation.proceed()
来实现调用真正的方法
public class WeekendBlocker implements MethodInterceptor {
@Override
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();
}
}
最后我们创建Matcher,把具体需要被拦截检查的类和方法与相应的拦截器绑定在一起
public class NotOnWeekendsModule extends AbstractModule {
protected void configure() {
bindInterceptor(Matchers.any(), Matchers.annotatedWith(NotOnWeekends.class),
new WeekendBlocker());
}
}
在这个例子里,我们匹配任意class的标有@NotOnWeekends
的方法。当一旦chargeOrder
方法在周末被调用,则会抛出我们预设的异常
Exception in thread "main" java.lang.IllegalStateException: chargeOrder not allowed on weekends!
at com.hulu.david.guice.pizza.WeekendBlocker.invoke(WeekendBlocker.java:65)
at com.google.inject.internal.InterceptorStackCallback.intercept(...)
at com.hulu.david.guice.RealBillingService$$EnhancerByGuice$$49ed77ce.chargeOrder()
at com.hulu.david.guice.pizza.WeekendExample.main(WeekendExample.java:47)
当然在Guice里使用AOP还是有一定的限制,因为Guice动态生成需要被拦截类型的子类并且重写被拦截方法来把拦截器放进去,所以使用AOP必须满足之一:
- 类必须是public或者package private
- 类必须是non-final
- 方法必须是public, package private或者protected
- 方法必须是non-final
- 并且实例必须通过Guice的@Inject注解注释的构造函数或者无参构造函数创建