Google 開源的依賴注入庫,比 Spring 更小更快

程序員小樂 發佈 2020-01-21T15:52:42+00:00

猜你還想看阿里、騰訊、百度、華為、京東最新面試題匯集Java中關於try、catch、finally中的細節分析,看了都說好!

點擊上方 "程式設計師小樂"關注, 星標或置頂一起成長

每天凌晨00點00分, 第一時間與你相約


每日英文

Be true to who you are. Stop trying to please other people or be someone else. It’s better to be an original version of yourself than an exact duplicate of someone else.

做真實的自己,不要為了取悅別人或試圖成為某個人。做你最原始的自己,比做任何人的複製品都來得好。


每日掏心話

大多數人的壓力,來自於不假思索跟著大部隊走。大部隊說,要先有錢才能有面子,於是終日焦慮財富不夠,忘了品味生活。



來自:GinoBeFunny | 責編:樂樂

連結:zhuanlan.zhihu.com/p/24924391

程式設計師小樂(ID:study_tech)第 752 次推文 圖片來自 Pexels


往日回顧:重磅!美國對巴西發出警告:敢用華為就降低安全級別,巴西霸氣回應!


正文


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方法中設置綁定(後面會繼續介紹)即可。

然後只需在原有的構造方法中增加@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支付,這樣通過連接綁定就搞不定。這時我們可以通過註解綁定來實現:
@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。public class BillingModule extends AbstractModule {
@Override
protected void configure() {
try {
bind(TransactionLog.class).toConstructor(DatabaseTransactionLog.class.getConstructor(DatabaseConnection.class));
} catch (NoSuchMethodException e) {
addError(e);
}
}
}
範圍
默認情況下,Guice每次都會返回一個新的實例,這個可以通過範圍(Scope)來配置。常見的範圍有單例(@Singleton)、會話(@SessionScoped)和請求(@RequestScoped),另外還可以通過自定義的範圍來擴展。
範圍的註解可以應該在實現類、@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中關於try、catch、finally中的細節分析,看了都說好!

IDEA熱部署之JRebel的安裝與破解教程

為什麼要看源碼?如何看源碼?高手進階必看!


關注「程式設計師小樂」,收看更多精彩內容
嘿,你在看嗎?


關鍵字: