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

Posted Java技术栈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Google 开源的依赖注入库,比 Spring 更小更快!相关的知识,希望对你有一定的参考价值。

作者: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最全面试题及答案。

推荐去我的博客阅读更多:

1.Java JVM、集合、多线程、新特性系列教程

2.Spring MVC、Spring Boot、Spring Cloud 系列教程

3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

4.Java、后端、架构、阿里巴巴等大厂最新面试题

觉得不错,别忘了点赞+转发哦!


以上是关于Google 开源的依赖注入库,比 Spring 更小更快!的主要内容,如果未能解决你的问题,请参考以下文章

Go的依赖注入库dig

史上最好用的依赖注入框架Google Guice转

Go 开源库运行时依赖注入框架 Dependency injection

Google 开源的这个库,性能快到让程序员飞起来!

如何在Maven中配置Spring依赖

Android 常用开源框架源码解析 系列 dagger2 呆哥兔 依赖注入库