Guice是Google开源的一个依赖注入类库,相比于Spring IoC来说更小更快。Elasticsearch大量使用了Guice,本文简单的介绍下Guice的基本概念和使用方式。
假设一个在线预订Pizza的网站,其有一个计费服务接口:
1
2
3
4
5
6
7
8
|
public
interface BillingService {
/**
* 通过信用卡支付。无论支付成功与否都需要记录交易信息。
*
*
@return 交易回执。支付成功时返回成功信息,否则记录失败原因。
*/
Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}
|
使用new的方式获取信用卡支付处理器和数据库交易日志记录器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public
class RealBillingService implements BillingService {
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
CreditCardProcessor processor =
new PaypalCreditCardProcessor();
TransactionLog transactionLog =
new DatabaseTransactionLog();
try {
ChargeResult result = processor.charge(creditCard, order.getAmount());
transactionLog.logChargeResult(result);
return result.wasSuccessful()
? Receipt.forSuccessfulCharge(order.getAmount())
: Receipt.forDeclinedCharge(result.getDeclineMessage());
}
catch (UnreachableException e) {
transactionLog.logConnectException(e);
return Receipt.forSystemFailure(e.getMessage());
}
}
}
|
使用new的问题是使得代码耦合,不易维护和测试。比如在UT里不可能直接用真实的信用卡支付,需要Mock一个CreditCardProcessor。相比于new,更容易想到的改进是使用工厂方法,但是工厂方法在测试中仍存在问题(因为通常使用全局变量来保存实例,如果在用例中未重置可能会影响其他用例)。更好的方式是通过构造方法注入依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public
class RealBillingService implements BillingService {
private
final CreditCardProcessor processor;
private
final TransactionLog transactionLog;
public RealBillingService(CreditCardProcessor processor,
TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
try {
ChargeResult result = processor.charge(creditCard, order.getAmount());
transactionLog.logChargeResult(result);
return result.wasSuccessful()
? Receipt.forSuccessfulCharge(order.getAmount())
: Receipt.forDeclinedCharge(result.getDeclineMessage());
}
catch (UnreachableException e) {
transactionLog.logConnectException(e);
return Receipt.forSystemFailure(e.getMessage());
}
}
}
|
对于真实的网站应用可以注入真正的业务处理服务类:
1
2
3
4
5
6
7
|
public static void main(String[] args) {
CreditCardProcessor processor =
new PaypalCreditCardProcessor();
TransactionLog transactionLog =
new DatabaseTransactionLog();
BillingService billingService
=
new RealBillingService(processor, transactionLog);
...
}
|
而在测试用例中可以注入Mock类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public
class RealBillingServiceTest extends TestCase {
private
final PizzaOrder order =
new PizzaOrder(
100);
private
final CreditCard creditCard =
new CreditCard(
"1234",
11,
2010);
private
final InMemoryTransactionLog transactionLog =
new InMemoryTransactionLog();
private
final FakeCreditCardProcessor processor =
new FakeCreditCardProcessor();
public void testSuccessfulCharge() {
RealBillingService billingService
=
new RealBillingService(processor, transactionLog);
Receipt receipt = billingService.chargeOrder(order, creditCard);
assertTrue(receipt.hasSuccessfulCharge());
assertEquals(
100, receipt.getAmountOfCharge());
assertEquals(creditCard, processor.getCardOfOnlyCharge());
assertEquals(
100, processor.getAmountOfOnlyCharge());
assertTrue(transactionLog.wasSuccessLogged());
}
}
|
那通过Guice怎么实现依赖注入呢?首先我们需要告诉Guice如果找到接口对应的实现类,这个可以通过模块来实现:
1
2
3
4
5
6
7
8
|
public
class BillingModule extends AbstractModule {
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
bind(BillingService.class).to(RealBillingService.class);
}
}
|
这里的模块只需要实现Module接口或继承自AbstractModule,然后在configure方法中设置绑定(后面会继续介绍)即可。然后只需在原有的构造方法中增加@Inject注解即可注入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public
class RealBillingService implements BillingService {
private
final CreditCardProcessor processor;
private
final TransactionLog transactionLog;
public RealBillingService(CreditCardProcessor processor,
TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
try {
ChargeResult result = processor.charge(creditCard, order.getAmount());
transactionLog.logChargeResult(result);
return result.wasSuccessful()
? Receipt.forSuccessfulCharge(order.getAmount())
: Receipt.forDeclinedCharge(result.getDeclineMessage());
}
catch (UnreachableException e) {
transactionLog.logConnectException(e);
return Receipt.forSystemFailure(e.getMessage());
}
}
}
|
最后,再看看main方法中是如何调用的:
1
2
3
4
5
|
public static void main(String[] args) {
Injector injector = Guice.createInjector(
new BillingModule());
BillingService billingService = injector.getInstance(BillingService.class);
...
}
|
连接绑定是最常用的绑定方式,它将一个类型和它的实现进行映射。下面的例子中将TransactionLog接口映射到它的实现类DatabaseTransactionLog。
1
2
3
4
5
6
|
public
class BillingModule extends AbstractModule {
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
}
}
|
连接绑定还支持链式,比如下面的例子最终将TransactionLog接口映射到实现类MySqlDatabaseTransactionLog。
1
2
3
4
5
6
7
|
public
class BillingModule extends AbstractModule {
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);
}
}
|
通过一个类型可能存在多个实现,比如在信用卡支付处理器中存在PayPal的支付和Google支付,这样通过连接绑定就搞不定。这时我们可以通过注解绑定来实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
PayPal {}
public
class RealBillingService implements BillingService {
public RealBillingService(@PayPal CreditCardProcessor processor,
TransactionLog transactionLog) {
...
}
}
// 当注入的方法参数存在@PayPal注解时注入PayPalCreditCardProcessor实现
bind(CreditCardProcessor.class).annotatedWith(PayPal.class).to(PayPalCreditCardProcessor.class);
|
可以看到在模块的绑定时用annotatedWith方法指定具体的注解来进行绑定,这种方式有一个问题就是我们必须增加自定义的注解来绑定,基于此Guice内置了一个@Named注解满足该场景:
1
2
3
4
5
6
7
8
9
10
11
|
public
class RealBillingService implements BillingService {
public RealBillingService(@Named("Checkout") CreditCardProcessor processor,
TransactionLog transactionLog) {
...
}
}
// 当注入的方法参数存在@Named注解且值为Checkout时注入CheckoutCreditCardProcessor实现
bind(CreditCardProcessor.class).annotatedWith(Names.named(
"Checkout")).to(CheckoutCreditCardProcessor.class);
|
将一个类型绑定到一个具体的实例而非实现类,这个通过是在无依赖的对象(比如值对象)中使用。如果toInstance包含复杂的逻辑会导致启动速度,此时应该通过@Provides方法绑定。
1
2
|
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注解的、方法返回值即为绑定映射的类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public
class BillingModule extends AbstractModule {
protected void configure() {
...
}
TransactionLog provideTransactionLog() {
DatabaseTransactionLog transactionLog =
new DatabaseTransactionLog();
transactionLog.setJdbcUrl(
"jdbc:mysql://localhost/pizza");
transactionLog.setThreadPoolSize(
30);
return transactionLog;
}
CreditCardProcessor providePayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) {
PayPalCreditCardProcessor processor =
new PayPalCreditCardProcessor();
processor.setApiKey(apiKey);
return processor;
}
}
|
如果使用@Provides方法绑定逻辑越来越复杂时就可以通过Provider绑定(一个实现了Provider接口的实现类)来实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public
interface Provider<T> {
T get();
}
public
class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
private
final Connection connection;
public DatabaseTransactionLogProvider(Connection connection) {
this.connection = connection;
}
public TransactionLog get() {
DatabaseTransactionLog transactionLog =
new DatabaseTransactionLog();
transactionLog.setConnection(connection);
return transactionLog;
}
}
public
class BillingModule extends AbstractModule {
protected void configure() {
bind(TransactionLog.class).toProvider(DatabaseTransactionLogProvider.class);
}
}
|
当我们想提供对一个具体的类给注入器时就可以采用无目标绑定。
1
2
|
bind(MyConcreteClass.class);
bind(AnotherConcreteClass.class).in(Singleton.class);
|
3.0新增的绑定,适用于第三方提供的类或者是有多个构造器参与依赖注入。通过@Provides方法可以显式调用构造器,但是这种方式有一个限制:无法给这些实例应用AOP。
1
2
3
4
5
6
7
8
9
10
|
public
class BillingModule extends AbstractModule {
protected void configure() {
try {
bind(TransactionLog.class).toConstructor(DatabaseTransactionLog.class.getConstructor(DatabaseConnection.class));
}
catch (NoSuchMethodException e) {
addError(e);
}
}
}
|
默认情况下,Guice每次都会返回一个新的实例,这个可以通过范围(Scope)来配置。常见的范围有单例(@Singleton)、会话(@SessionScoped)和请求(@RequestScoped),另外还可以通过自定义的范围来扩展。
范围的注解可以应该在实现类、@Provides方法中,或在绑定的时候指定(优先级最高):
1
2
3
4
5
6
7
8
9
10
11
12
|
public
class InMemoryTransactionLog implements TransactionLog {
/* everything here should be threadsafe! */
}
// scopes apply to the binding source, not the binding target
bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);
TransactionLog provideTransactionLog() {
...
}
|
另外,Guice还有一种特殊的单例模式叫饥饿单例(相对于懒加载单例来说):
1
2
3
|
// Eager singletons reveal initialization problems sooner,
// and ensure end-users get a consistent, snappy experience.
bind(TransactionLog.class).to(InMemoryTransactionLog.class).asEagerSingleton();
|
依赖注入的要求就是将行为和依赖分离,它建议将依赖注入而非通过工厂类的方法去查找。注入的方式通常有构造器注入、方法注入、属性注入等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
// 构造器注入
public
class RealBillingService implements BillingService {
private
final CreditCardProcessor processorProvider;
private
final TransactionLog transactionLogProvider;
public RealBillingService(CreditCardProcessor processorProvider,
TransactionLog transactionLogProvider) {
this.processorProvider = processorProvider;
this.transactionLogProvider = transactionLogProvider;
}
}
// 方法注入
public
class PayPalCreditCardProcessor implements CreditCardProcessor {
private
static
final String DEFAULT_API_KEY =
"development-use-only";
private String apiKey = DEFAULT_API_KEY;
public void setApiKey(@Named("PayPal API key") String apiKey) {
this.apiKey = apiKey;
}
}
// 属性注入
public
class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
public TransactionLog get() {
return
new DatabaseTransactionLog(connection);
}
}
// 可选注入:当找不到映射时不报错
public
class PayPalCreditCardProcessor implements CreditCardProcessor {
private
static
final String SANDBOX_API_KEY =
"development-use-only";
private String apiKey = SANDBOX_API_KEY;
true)
(optional=
public void setApiKey(@Named("PayPal API key") String apiKey) {
this.apiKey = apiKey;
}
}
|
辅助注入(Assisted Inject)属于Guice扩展的一部分,它通过@Assisted注解自动生成工厂来加强非注入参数的使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
// RealPayment中有两个参数startDate和amount无法直接注入
public class RealPayment implements Payment {
public RealPayment(
CreditService creditService, // from the Injector
AuthService authService, // from the Injector
Date startDate, // from the instance's creator
Money amount); // from the instance's creator
}
...
}
// 一种方式是增加一个工厂来构造
public interface PaymentFactory {
public Payment create(Date startDate, Money amount);
}
public class RealPaymentFactory implements PaymentFactory {
private final Provider
private final Provider
@Inject
public RealPaymentFactory(Provider
Provider
this.creditServiceProvider = creditServiceProvider;
this.authServiceProvider = authServiceProvider;
}
public Payment create(Date startDate, Money amount) {
return new RealPayment(creditServiceProvider.get(),
authServiceProvider.get(), startDate, amount);
}
}
bind(PaymentFactory.class).to(RealPaymentFactory.class);
// 通过@Assisted注解可以减少RealPaymentFactory
public class RealPayment implements Payment {
@Inject
public RealPayment(
CreditService creditService,
AuthService authService,
@Assisted Date startDate,
@Assisted Money amount);
}
...
}
// Guice 2.0
// bind(PaymentFactory.class).toProvider(FactoryProvider.newFactory(PaymentFactory.class, RealPayment.class));
// Guice 3.0
install(new FactoryModuleBuilder().implement(Payment.class, RealPayment.class).build(PaymentFactory.class));
|