Google 开源的依赖注入库,比 Spring 更小更快!

JAVA葵花宝典

共 12443字,需浏览 25分钟

 · 2021-09-27

Guice是Google开源的一个依赖注入类库,相比于Spring IoC来说更小更快。Elasticsearch大量使用了Guice,本文简单的介绍下Guice的基本概念和使用方式。

学习目标

1、概述:了解Guice是什么,有什么特点;
2、快速开始:通过实例了解Guice;
3、核心概念:了解Guice涉及的核心概念,如绑定(Binding)、范围(Scope)和注入(Injection);
4、最佳实践:官方推荐的最佳实践;

Guice概述

1、Guice是Google开源的依赖注入类库,通过Guice减少了对工厂方法和new的使用,使得代码更易交付、测试和重用;
2、Guice可以帮助我们更好地设计API,它是个轻量级非侵入式的类库;
3、Guice对开发友好,当有异常发生时能提供更多有用的信息用于分析;

快速开始

假设一个在线预订Pizza的网站,其有一个计费服务接口:

  1. public interface BillingService {


  2. /**

  3. * 通过信用卡支付。无论支付成功与否都需要记录交易信息。

  4. *

  5. * @return 交易回执。支付成功时返回成功信息,否则记录失败原因。

  6. */

  7. Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);

  8. }

使用new的方式获取信用卡支付处理器和数据库交易日志记录器:

  1. public class RealBillingService implements BillingService {

  2. public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {

  3. CreditCardProcessor processor = new PaypalCreditCardProcessor();

  4. TransactionLog transactionLog = new DatabaseTransactionLog();


  5. try {

  6. ChargeResult result = processor.charge(creditCard, order.getAmount());

  7. transactionLog.logChargeResult(result);


  8. return result.wasSuccessful()

  9. ? Receipt.forSuccessfulCharge(order.getAmount())

  10. : Receipt.forDeclinedCharge(result.getDeclineMessage());

  11. } catch (UnreachableException e) {

  12. transactionLog.logConnectException(e);

  13. return Receipt.forSystemFailure(e.getMessage());

  14. }

  15. }

  16. }

使用new的问题是使得代码耦合,不易维护和测试。比如在UT里不可能直接用真实的信用卡支付,需要Mock一个CreditCardProcessor。相比于new,更容易想到的改进是使用工厂方法,但是工厂方法在测试中仍存在问题(因为通常使用全局变量来保存实例,如果在用例中未重置可能会影响其他用例)

更好的方式是通过构造方法注入依赖:

  1. public class RealBillingService implements BillingService {

  2. private final CreditCardProcessor processor;

  3. private final TransactionLog transactionLog;


  4. public RealBillingService(CreditCardProcessor processor,

  5. TransactionLog transactionLog) {

  6. this.processor = processor;

  7. this.transactionLog = transactionLog;

  8. }


  9. public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {

  10. try {

  11. ChargeResult result = processor.charge(creditCard, order.getAmount());

  12. transactionLog.logChargeResult(result);


  13. return result.wasSuccessful()

  14. ? Receipt.forSuccessfulCharge(order.getAmount())

  15. : Receipt.forDeclinedCharge(result.getDeclineMessage());

  16. } catch (UnreachableException e) {

  17. transactionLog.logConnectException(e);

  18. return Receipt.forSystemFailure(e.getMessage());

  19. }

  20. }

  21. }

对于真实的网站应用可以注入真正的业务处理服务类:

  1. public static void main(String[] args) {

  2. CreditCardProcessor processor = new PaypalCreditCardProcessor();

  3. TransactionLog transactionLog = new DatabaseTransactionLog();

  4. BillingService billingService

  5. = new RealBillingService(processor, transactionLog);

  6. ...

  7. }

而在测试用例中可以注入Mock类:

  1. public class RealBillingServiceTest extends TestCase {


  2. private final PizzaOrder order = new PizzaOrder(100);

  3. private final CreditCard creditCard = new CreditCard("1234", 11, 2010);


  4. private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();

  5. private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor();


  6. public void testSuccessfulCharge() {

  7. RealBillingService billingService

  8. = new RealBillingService(processor, transactionLog);

  9. Receipt receipt = billingService.chargeOrder(order, creditCard);


  10. assertTrue(receipt.hasSuccessfulCharge());

  11. assertEquals(100, receipt.getAmountOfCharge());

  12. assertEquals(creditCard, processor.getCardOfOnlyCharge());

  13. assertEquals(100, processor.getAmountOfOnlyCharge());

  14. assertTrue(transactionLog.wasSuccessLogged());

  15. }

  16. }

那通过Guice怎么实现依赖注入呢?首先我们需要告诉Guice如果找到接口对应的实现类,这个可以通过模块来实现:

  1. public class BillingModule extends AbstractModule {

  2. @Override

  3. protected void configure() {

  4. bind(TransactionLog.class).to(DatabaseTransactionLog.class);

  5. bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);

  6. bind(BillingService.class).to(RealBillingService.class);

  7. }

  8. }

这里的模块只需要实现Module接口或继承自AbstractModule,然后在configure方法中设置绑定(后面会继续介绍)即可

然后只需在原有的构造方法中增加@Inject注解即可注入

  1. public class RealBillingService implements BillingService {

  2. private final CreditCardProcessor processor;

  3. private final TransactionLog transactionLog;


  4. @Inject

  5. public RealBillingService(CreditCardProcessor processor,

  6. TransactionLog transactionLog) {

  7. this.processor = processor;

  8. this.transactionLog = transactionLog;

  9. }


  10. public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {

  11. try {

  12. ChargeResult result = processor.charge(creditCard, order.getAmount());

  13. transactionLog.logChargeResult(result);


  14. return result.wasSuccessful()

  15. ? Receipt.forSuccessfulCharge(order.getAmount())

  16. : Receipt.forDeclinedCharge(result.getDeclineMessage());

  17. } catch (UnreachableException e) {

  18. transactionLog.logConnectException(e);

  19. return Receipt.forSystemFailure(e.getMessage());

  20. }

  21. }

  22. }

最后,再看看main方法中是如何调用的:

  1. public static void main(String[] args) {

  2. Injector injector = Guice.createInjector(new BillingModule());

  3. BillingService billingService = injector.getInstance(BillingService.class);

  4. ...

  5. }

绑定

连接绑定

连接绑定是最常用的绑定方式,它将一个类型和它的实现进行映射。下面的例子中将TransactionLog接口映射到它的实现类DatabaseTransactionLog。

  1. public class BillingModule extends AbstractModule {

  2. @Override

  3. protected void configure() {

  4. bind(TransactionLog.class).to(DatabaseTransactionLog.class);

  5. }

  6. }

连接绑定还支持链式,比如下面的例子最终将TransactionLog接口映射到实现类MySqlDatabaseTransactionLog。

  1. public class BillingModule extends AbstractModule {

  2. @Override

  3. protected void configure() {

  4. bind(TransactionLog.class).to(DatabaseTransactionLog.class);

  5. bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);

  6. }

  7. }

注解绑定

通过一个类型可能存在多个实现,比如在信用卡支付处理器中存在PayPal的支付和Google支付,这样通过连接绑定就搞不定。这时我们可以通过注解绑定来实现:

  1. @BindingAnnotation

  2. @Target({ FIELD, PARAMETER, METHOD })

  3. @Retention(RUNTIME)

  4. public @interface PayPal {}


  5. public class RealBillingService implements BillingService {


  6. @Inject

  7. public RealBillingService(@PayPal CreditCardProcessor processor,

  8. TransactionLog transactionLog) {

  9. ...

  10. }

  11. }


  12. // 当注入的方法参数存在@PayPal注解时注入PayPalCreditCardProcessor实现

  13. bind(CreditCardProcessor.class).annotatedWith(PayPal.class).to(PayPalCreditCardProcessor.class);

可以看到在模块的绑定时用annotatedWith方法指定具体的注解来进行绑定,这种方式有一个问题就是我们必须增加自定义的注解来绑定,基于此Guice内置了一个@Named注解满足该场景:

  1. public class RealBillingService implements BillingService {


  2. @Inject

  3. public RealBillingService(@Named("Checkout") CreditCardProcessor processor,

  4. TransactionLog transactionLog) {

  5. ...

  6. }

  7. }


  8. // 当注入的方法参数存在@Named注解且值为Checkout时注入CheckoutCreditCardProcessor实现

  9. bind(CreditCardProcessor.class).annotatedWith(Names.named("Checkout")).to(CheckoutCreditCardProcessor.class);

实例绑定

将一个类型绑定到一个具体的实例而非实现类,这个通过是在无依赖的对象(比如值对象)中使用。如果toInstance包含复杂的逻辑会导致启动速度,此时应该通过@Provides方法绑定

  1. bind(String.class).annotatedWith(Names.named("JDBC URL")).toInstance("jdbc:mysql://localhost/pizza");

  2. bind(Integer.class).annotatedWith(Names.named("login timeout seconds")).toInstance(10);

@Provides方法绑定

模块中定义的、带有@Provides注解的、方法返回值即为绑定映射的类型。

  1. public class BillingModule extends AbstractModule {

  2. @Override

  3. protected void configure() {

  4. ...

  5. }


  6. @Provides

  7. TransactionLog provideTransactionLog() {

  8. DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();

  9. transactionLog.setJdbcUrl("jdbc:mysql://localhost/pizza");

  10. transactionLog.setThreadPoolSize(30);

  11. return transactionLog;

  12. }


  13. @Provides @PayPal

  14. CreditCardProcessor providePayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) {

  15. PayPalCreditCardProcessor processor = new PayPalCreditCardProcessor();

  16. processor.setApiKey(apiKey);

  17. return processor;

  18. }

  19. }

Provider绑定

如果使用@Provides方法绑定逻辑越来越复杂时就可以通过Provider绑定(一个实现了Provider接口的实现类)来实现。

  1. public interface Provider<T> {

  2. T get();

  3. }


  4. public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {

  5. private final Connection connection;


  6. @Inject

  7. public DatabaseTransactionLogProvider(Connection connection) {

  8. this.connection = connection;

  9. }


  10. public TransactionLog get() {

  11. DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();

  12. transactionLog.setConnection(connection);

  13. return transactionLog;

  14. }

  15. }


  16. public class BillingModule extends AbstractModule {

  17. @Override

  18. protected void configure() {

  19. bind(TransactionLog.class).toProvider(DatabaseTransactionLogProvider.class);

  20. }

  21. }

无目标绑定

当我们想提供对一个具体的类给注入器时就可以采用无目标绑定。

  1. bind(MyConcreteClass.class);

  2. bind(AnotherConcreteClass.class).in(Singleton.class);

构造器绑定

3.0新增的绑定,适用于第三方提供的类或者是有多个构造器参与依赖注入。通过@Provides方法可以显式调用构造器,但是这种方式有一个限制:无法给这些实例应用AOP。

  1. public class BillingModule extends AbstractModule {

  2. @Override

  3. protected void configure() {

  4. try {

  5. bind(TransactionLog.class).toConstructor(DatabaseTransactionLog.class.getConstructor(DatabaseConnection.class));

  6. } catch (NoSuchMethodException e) {

  7. addError(e);

  8. }

  9. }

  10. }

范围

默认情况下,Guice每次都会返回一个新的实例,这个可以通过范围(Scope)来配置。常见的范围有单例(@Singleton)、会话(@SessionScoped)和请求(@RequestScoped),另外还可以通过自定义的范围来扩展

范围的注解可以应该在实现类、@Provides方法中,或在绑定的时候指定(优先级最高):

  1. @Singleton

  2. public class InMemoryTransactionLog implements TransactionLog {

  3. /* everything here should be threadsafe! */

  4. }


  5. // scopes apply to the binding source, not the binding target

  6. bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);


  7. @Provides @Singleton

  8. TransactionLog provideTransactionLog() {

  9. ...

  10. }

另外,Guice还有一种特殊的单例模式叫饥饿单例(相对于懒加载单例来说):

  1. // Eager singletons reveal initialization problems sooner,

  2. // and ensure end-users get a consistent, snappy experience.

  3. bind(TransactionLog.class).to(InMemoryTransactionLog.class).asEagerSingleton();

注入

依赖注入的要求就是将行为和依赖分离,它建议将依赖注入而非通过工厂类的方法去查找。注入的方式通常有构造器注入、方法注入、属性注入等。

  1. // 构造器注入

  2. public class RealBillingService implements BillingService {

  3. private final CreditCardProcessor processorProvider;

  4. private final TransactionLog transactionLogProvider;


  5. @Inject

  6. public RealBillingService(CreditCardProcessor processorProvider,

  7. TransactionLog transactionLogProvider) {

  8. this.processorProvider = processorProvider;

  9. this.transactionLogProvider = transactionLogProvider;

  10. }

  11. }


  12. // 方法注入

  13. public class PayPalCreditCardProcessor implements CreditCardProcessor {

  14. private static final String DEFAULT_API_KEY = "development-use-only";

  15. private String apiKey = DEFAULT_API_KEY;


  16. @Inject

  17. public void setApiKey(@Named("PayPal API key") String apiKey) {

  18. this.apiKey = apiKey;

  19. }

  20. }


  21. // 属性注入

  22. public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {

  23. @Inject Connection connection;


  24. public TransactionLog get() {

  25. return new DatabaseTransactionLog(connection);

  26. }

  27. }


  28. // 可选注入:当找不到映射时不报错

  29. public class PayPalCreditCardProcessor implements CreditCardProcessor {

  30. private static final String SANDBOX_API_KEY = "development-use-only";

  31. private String apiKey = SANDBOX_API_KEY;


  32. @Inject(optional=true)

  33. public void setApiKey(@Named("PayPal API key") String apiKey) {

  34. this.apiKey = apiKey;

  35. }

  36. }

辅助注入

辅助注入(Assisted Inject)属于Guice扩展的一部分,它通过@Assisted注解自动生成工厂来加强非注入参数的使用。

  1. // RealPayment中有两个参数startDate和amount无法直接注入

  2. public class RealPayment implements Payment {

  3. public RealPayment(

  4. CreditService creditService, // from the Injector

  5. AuthService authService, // from the Injector

  6. Date startDate, // from the instance's creator

  7. Money amount); // from the instance's creator

  8. }

  9. ...

  10. }


  11. // 一种方式是增加一个工厂来构造

  12. public interface PaymentFactory {

  13. public Payment create(Date startDate, Money amount);

  14. }


  15. public class RealPaymentFactory implements PaymentFactory {

  16. private final Provider<CreditService> creditServiceProvider;

  17. private final Provider<AuthService> authServiceProvider;


  18. @Inject

  19. public RealPaymentFactory(Provider<CreditService> creditServiceProvider,

  20. Provider<AuthService> authServiceProvider) {

  21. this.creditServiceProvider = creditServiceProvider;

  22. this.authServiceProvider = authServiceProvider;

  23. }


  24. public Payment create(Date startDate, Money amount) {

  25. return new RealPayment(creditServiceProvider.get(),

  26. authServiceProvider.get(), startDate, amount);

  27. }

  28. }


  29. bind(PaymentFactory.class).to(RealPaymentFactory.class);


  30. // 通过@Assisted注解可以减少RealPaymentFactory

  31. public class RealPayment implements Payment {

  32. @Inject

  33. public RealPayment(

  34. CreditService creditService,

  35. AuthService authService,

  36. @Assisted Date startDate,

  37. @Assisted Money amount);

  38. }

  39. ...

  40. }


  41. // Guice 2.0

  42. //bind(PaymentFactory.class).toProvider(FactoryProvider.newFactory(PaymentFactory.class, RealPayment.class));

  43. // Guice 3.0

  44. install(new FactoryModuleBuilder().implement(Payment.class, RealPayment.class).build(PaymentFactory.class));

最佳实践

来源:https://sourl.cn/H4waaS

浏览 31
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报