作者:GinoBeFunny
https://zhuanlan.zhihu.com/p/24924391
Google開源的一個依賴注入類庫Guice,相比於Spring IoC來說更小更快。
Elasticsearch大量使用了Guice,本文簡單的介紹下Guice的基本概念和使用方式。
學習目標
-
概述:了解Guice是什么,有什么特點;
-
快速開始:通過實例了解Guice;
-
核心概念:了解Guice涉及的核心概念,如綁定(Binding)、范圍(Scope)和注入(Injection);
-
最佳實踐:官方推薦的最佳實踐;
Guice概述
-
Guice是Google開源的依賴注入類庫,通過Guice減少了對工廠方法和new的使用,使得代碼更易交付、測試和重用;
-
Guice可以幫助我們更好地設計API,它是個輕量級非侵入式的類庫;
-
Guice對開發友好,當有異常發生時能提供更多有用的信息用於分析;
快速開始
假設一個在線預訂Pizza的網站,其有一個計費服務接口:
public interface BillingService {
/**
* 通過信用卡支付。無論支付成功與否都需要記錄交易信息。
*
* @return 交易回執。支付成功時返回成功信息,否則記錄失敗原因。
*/
Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}
使用new的方式獲取信用卡支付處理器和數據庫交易日志記錄器:
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,更容易想到的改進是使用工廠方法,但是工廠方法在測試中仍存在問題(因為通常使用全局變量來保存實例,如果在用例中未重置可能會影響其他用例)。
更好的方式是通過構造方法注入依賴:
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());
}
}
}
對於真實的網站應用可以注入真正的業務處理服務類:
public static void main(String[] args) {
CreditCardProcessor processor = new PaypalCreditCardProcessor();
TransactionLog transactionLog = new DatabaseTransactionLog();
BillingService billingService
= new RealBillingService(processor, transactionLog);
...
}
中可以注入Mock類:
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如果找到接口對應的實現類,這個可以通過模塊來實現:
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);
}
}
這里的模塊只需要實現Module接口或繼承自AbstractModule,然后在configure方法中設置綁定(后面會繼續介紹)即可。Spring Boot 構造器參數綁定,這篇推薦大家看下。
然后只需在原有的構造方法中增加@Inject注解即可注入:
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) {
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方法中是如何調用的:
public static void main(String[] args) {
Injector injector = Guice.createInjector(new BillingModule());
BillingService billingService = injector.getInstance(BillingService.class);
...
}
綁定
連接綁定
連接綁定是最常用的綁定方式,它將一個類型和它的實現進行映射。下面的例子中將TransactionLog接口映射到它的實現類DatabaseTransactionLog。
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
}
}
連接綁定還支持鏈式,比如下面的例子最終將TransactionLog接口映射到實現類MySqlDatabaseTransactionLog。
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);
}
}
注解綁定
通過一個類型可能存在多個實現,比如在信用卡支付處理器中存在PayPal的支付和Google支付,這樣通過連接綁定就搞不定。Spring Boot 最核心的 25 個注解,推薦大家看下。關注微信公眾號:Java技術棧,在后台回復:spring,可以獲取我整理的 N 篇最新 Spring 系列技術教程,都是干貨。
這時我們可以通過注解綁定來實現:
@BindingAnnotation
@Target({ FIELD, PARAMETER, METHOD })
@Retention(RUNTIME)
public @interface PayPal {}
public class RealBillingService implements BillingService {
@Inject
public RealBillingService(@PayPal CreditCardProcessor processor,
TransactionLog transactionLog) {
...
}
}
// 當注入的方法參數存在@PayPal注解時注入PayPalCreditCardProcessor實現
bind(CreditCardProcessor.class).annotatedWith(PayPal.class).to(PayPalCreditCardProcessor.class);
可以看到在模塊的綁定時用annotatedWith方法指定具體的注解來進行綁定,這種方式有一個問題就是我們必須增加自定義的注解來綁定,基於此Guice內置了一個@Named注解滿足該場景:
public class RealBillingService implements BillingService {
@Inject
public RealBillingService(@Named("Checkout") CreditCardProcessor processor,
TransactionLog transactionLog) {
...
}
}
// 當注入的方法參數存在@Named注解且值為Checkout時注入CheckoutCreditCardProcessor實現
bind(CreditCardProcessor.class).annotatedWith(Names.named("Checkout")).to(CheckoutCreditCardProcessor.class);
實例綁定
將一個類型綁定到一個具體的實例而非實現類,這個通過是在無依賴的對象(比如值對象)中使用。如果toInstance包含復雜的邏輯會導致啟動速度,此時應該通過@Provides方法綁定。
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方法綁定
模塊中定義的、帶有@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;
}
@Provides @PayPal
CreditCardProcessor providePayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) {
PayPalCreditCardProcessor processor = new PayPalCreditCardProcessor();
processor.setApiKey(apiKey);
return processor;
}
}
Provider綁定
如果使用@Provides方法綁定邏輯越來越復雜時就可以通過Provider綁定(一個實現了Provider接口的實現類)來實現。
public interface Provider<T> {
T get();
}
public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
private final Connection connection;
@Inject
public DatabaseTransactionLogProvider(Connection connection) {
this.connection = connection;
}
public TransactionLog get() {
DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
transactionLog.setConnection(connection);
return transactionLog;
}
}
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).toProvider(DatabaseTransactionLogProvider.class);
}
}
無目標綁定
當我們想提供對一個具體的類給注入器時就可以采用無目標綁定。
bind(MyConcreteClass.class);
bind(AnotherConcreteClass.class).in(Singleton.class);
構造器綁定
3.0新增的綁定,適用於第三方提供的類或者是有多個構造器參與依賴注入。通過@Provides方法可以顯式調用構造器,但是這種方式有一個限制:無法給這些實例應用AOP。Spring Boot 構造器參數綁定,推薦大家看下吧。
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
try {
bind(TransactionLog.class).toConstructor(DatabaseTransactionLog.class.getConstructor(DatabaseConnection.class));
} catch (NoSuchMethodException e) {
addError(e);
}
}
}
關注微信公眾號:Java技術棧,在后台回復:spring,可以獲取我整理的 N 篇最新 Spring 系列技術教程,都是干貨。
范圍
默認情況下,Guice每次都會返回一個新的實例,這個可以通過范圍(Scope)來配置。常見的范圍有單例(@Singleton)、會話(@SessionScoped)和請求(@RequestScoped),另外還可以通過自定義的范圍來擴展。12 種 Spring 常用注解,推薦大家看下。
范圍的注解可以應該在實現類、@Provides方法中,或在綁定的時候指定(優先級最高):
@Singleton
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);
@Provides @Singleton
TransactionLog provideTransactionLog() {
...
}
另外,Guice還有一種特殊的單例模式叫飢餓單例(相對於懶加載單例來說):
// Eager singletons reveal initialization problems sooner,
// and ensure end-users get a consistent, snappy experience.
bind(TransactionLog.class).to(InMemoryTransactionLog.class).asEagerSingleton();
注入
依賴注入的要求就是將行為和依賴分離,它建議將依賴注入而非通過工廠類的方法去查找。注入的方式通常有構造器注入、方法注入、屬性注入等。
// 構造器注入
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;
}
}
// 方法注入
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;
}
}
// 屬性注入
public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
@Inject Connection connection;
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;
@Inject(optional=true)
public void setApiKey(@Named("PayPal API key") String apiKey) {
this.apiKey = apiKey;
}
}
輔助注入
輔助注入(Assisted Inject)屬於Guice擴展的一部分,它通過@Assisted注解自動生成工廠來加強非注入參數的使用。
// 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<CreditService> creditServiceProvider;
private final Provider<AuthService> authServiceProvider;
@Inject
public RealPaymentFactory(Provider<CreditService> creditServiceProvider,
Provider<AuthService> authServiceProvider) {
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));
最佳實踐
-
最小化可變性:盡可能注入的是不可變對象;
-
只注入直接依賴:不用注入一個實例來獲取真正需要的實例,增加復雜性且不易測試;
-
避免循環依賴
-
避免靜態狀態:靜態狀態和可測試性就是天敵;
-
采用@Nullable:Guice默認情況下禁止注入null對象;
-
模塊的處理必須要快並且無副作用
-
在Providers綁定中當心IO問題:因為Provider不檢查異常、不支持超時、不支持重試;
-
不用在模塊中處理分支邏輯
-
盡可能不要暴露構造器
關注公眾號Java技術棧回復"面試"獲取我整理的2020最全面試題及答案。
推薦去我的博客閱讀更多:
2.Spring MVC、Spring Boot、Spring Cloud 系列教程
3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程
覺得不錯,別忘了點贊+轉發哦!