速读摘要文章源自JAVA秀-https://www.javaxiu.com/6606.html
Guice是Google开源的一个依赖注入类库,相比于Spring IoC来说更小更快。相比于new,更容易想到的改进是使用工厂方法,但是工厂方法在测试中仍存在问题(因为通常使用全局变量来保存实例,如果在用例中未重置可能会影响其他用例)。当我们想提供对一个具体的类给注入器时就可以采用无目标绑定。默认情况下,Guice每次都会返回一个新的实例,这个可以通过范围(Scope)来配置。注入的方式通常有构造器注入、方法注入、属性注入等。文章源自JAVA秀-https://www.javaxiu.com/6606.html
原文约 3344 字 | 图片 3 张 | 建议阅读 7 分钟 | 评价反馈文章源自JAVA秀-https://www.javaxiu.com/6606.html
Google 开源的依赖注入库,比 Spring 更小更快!
Java后端编程 文章源自JAVA秀-https://www.javaxiu.com/6606.html
来源:zhuanlan.zhihu.com/p/24924391文章源自JAVA秀-https://www.javaxiu.com/6606.html
文章源自JAVA秀-https://www.javaxiu.com/6606.html
Guice是Google开源的一个依赖注入类库,相比于Spring IoC来说更小更快。Elasticsearch大量使用了Guice,本文简单的介绍下Guice的基本概念和使用方式。文章源自JAVA秀-https://www.javaxiu.com/6606.html
学习目标
概述:了解Guice是什么,有什么特点;文章源自JAVA秀-https://www.javaxiu.com/6606.html
快速开始:通过实例了解Guice;文章源自JAVA秀-https://www.javaxiu.com/6606.html
核心概念:了解Guice涉及的核心概念,如绑定(Binding)、范围(Scope)和注入(Injection);文章源自JAVA秀-https://www.javaxiu.com/6606.html
最佳实践:官方推荐的最佳实践;文章源自JAVA秀-https://www.javaxiu.com/6606.html
Guice概述
Guice是Google开源的依赖注入类库,通过Guice减少了对工厂方法和new的使用,使得代码更易交付、测试和重用;文章源自JAVA秀-https://www.javaxiu.com/6606.html
Guice可以帮助我们更好地设计API,它是个轻量级非侵入式的类库;文章源自JAVA秀-https://www.javaxiu.com/6606.html
Guice对开发友好,当有异常发生时能提供更多有用的信息用于分析;文章源自JAVA秀-https://www.javaxiu.com/6606.html
快速开始
假设一个在线预订Pizza的网站,其有一个计费服务接口:文章源自JAVA秀-https://www.javaxiu.com/6606.html
public interface BillingService { /** * 通过信用卡支付。无论支付成功与否都需要记录交易信息。 * * @return 交易回执。支付成功时返回成功信息,否则记录失败原因。 */ Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);}
使用new的方式获取信用卡支付处理器和数据库交易日志记录器:文章源自JAVA秀-https://www.javaxiu.com/6606.html
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,更容易想到的改进是使用工厂方法,但是工厂方法在测试中仍存在问题(因为通常使用全局变量来保存实例,如果在用例中未重置可能会影响其他用例)。更好的方式是通过构造方法注入依赖:文章源自JAVA秀-https://www.javaxiu.com/6606.html
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()); } }}
对于真实的网站应用可以注入真正的业务处理服务类:文章源自JAVA秀-https://www.javaxiu.com/6606.html
public static void main(String[] args) { CreditCardProcessor processor = new PaypalCreditCardProcessor(); TransactionLog transactionLog = new DatabaseTransactionLog(); BillingService billingService = new RealBillingService(processor, transactionLog); ... }
而在测试用例中可以注入Mock类:文章源自JAVA秀-https://www.javaxiu.com/6606.html
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如果找到接口对应的实现类,这个可以通过模块 来实现:文章源自JAVA秀-https://www.javaxiu.com/6606.html
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注解即可注入 :文章源自JAVA秀-https://www.javaxiu.com/6606.html
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方法中是如何调用的:文章源自JAVA秀-https://www.javaxiu.com/6606.html
public static void main(String[] args) { Injector injector = Guice.createInjector(new BillingModule()); BillingService billingService = injector.getInstance(BillingService.class); ... }
绑定
连接绑定
连接绑定是最常用的绑定方式,它将一个类型和它的实现进行映射。下面的例子中将TransactionLog接口映射到它的实现类DatabaseTransactionLog。文章源自JAVA秀-https://www.javaxiu.com/6606.html
public class BillingModule extends AbstractModule { @Override protected void configure() { bind(TransactionLog.class).to(DatabaseTransactionLog.class); }}
连接绑定还支持链式,比如下面的例子最终将TransactionLog接口映射到实现类MySqlDatabaseTransactionLog。文章源自JAVA秀-https://www.javaxiu.com/6606.html
public class BillingModule extends AbstractModule { @Override protected void configure() { bind(TransactionLog.class).to(DatabaseTransactionLog.class); bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class); }}
注解绑定
通过一个类型可能存在多个实现,比如在信用卡支付处理器中存在PayPal的支付和Google支付,这样通过连接绑定就搞不定。这时我们可以通过注解绑定来实现:文章源自JAVA秀-https://www.javaxiu.com/6606.html
@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注解满足该场景:文章源自JAVA秀-https://www.javaxiu.com/6606.html
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方法绑定。文章源自JAVA秀-https://www.javaxiu.com/6606.html
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注解的、方法返回值即为绑定映射的类型。文章源自JAVA秀-https://www.javaxiu.com/6606.html
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接口的实现类)来实现。文章源自JAVA秀-https://www.javaxiu.com/6606.html
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); }}
无目标绑定
当我们想提供对一个具体的类给注入器时就可以采用无目标绑定。文章源自JAVA秀-https://www.javaxiu.com/6606.html
bind(MyConcreteClass.class);bind(AnotherConcreteClass.class).in(Singleton.class);
构造器绑定
3.0新增的绑定,适用于第三方提供的类或者是有多个构造器参与依赖注入。通过@Provides方法可以显式调用构造器,但是这种方式有一个限制:无法给这些实例应用AOP。文章源自JAVA秀-https://www.javaxiu.com/6606.html
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),另外还可以通过自定义的范围来扩展。文章源自JAVA秀-https://www.javaxiu.com/6606.html
范围的注解可以应该在实现类、@Provides方法中,或在绑定的时候指定(优先级最高):文章源自JAVA秀-https://www.javaxiu.com/6606.html
@Singletonpublic class InMemoryTransactionLog implements TransactionLog { /* everything here should be threadsafe! */}// scopes apply to the binding source, not the binding targetbind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);@Provides @SingletonTransactionLog provideTransactionLog() { ...}
另外,Guice还有一种特殊的单例模式叫饥饿单例(相对于懒加载单例来说):文章源自JAVA秀-https://www.javaxiu.com/6606.html
// Eager singletons reveal initialization problems sooner,// and ensure end-users get a consistent, snappy experience.bind(TransactionLog.class).to(InMemoryTransactionLog.class).asEagerSingleton();
注入
依赖注入的要求就是将行为和依赖分离,它建议将依赖注入而非通过工厂类的方法去查找。注入的方式通常有构造器注入、方法注入、属性注入等。文章源自JAVA秀-https://www.javaxiu.com/6606.html
// 构造器注入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注解自动生成工厂来加强非注入参数的使用。文章源自JAVA秀-https://www.javaxiu.com/6606.html
// 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注解可以减少RealPaymentFactorypublic 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.0install(new FactoryModuleBuilder().implement(Payment.class, RealPayment.class).build(PaymentFactory.class));
最佳实践
最小化可变性:尽可能注入的是不可变对象;文章源自JAVA秀-https://www.javaxiu.com/6606.html
只注入直接依赖:不用注入一个实例来获取真正需要的实例,增加复杂性且不易测试;文章源自JAVA秀-https://www.javaxiu.com/6606.html
避免循环依赖文章源自JAVA秀-https://www.javaxiu.com/6606.html
避免静态状态:静态状态和可测试性就是天敌;文章源自JAVA秀-https://www.javaxiu.com/6606.html
采用@Nullable:Guice默认情况下禁止注入null对象;文章源自JAVA秀-https://www.javaxiu.com/6606.html
模块的处理必须要快并且无副作用文章源自JAVA秀-https://www.javaxiu.com/6606.html
在Providers绑定中当心IO问题:因为Provider不检查异常、不支持超时、不支持重试;文章源自JAVA秀-https://www.javaxiu.com/6606.html
不用在模块中处理分支逻辑文章源自JAVA秀-https://www.javaxiu.com/6606.html
尽可能不要暴露构造器文章源自JAVA秀-https://www.javaxiu.com/6606.html
PS:欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,欢迎转发分享给更多人。文章源自JAVA秀-https://www.javaxiu.com/6606.html
Java后端编程交流群已成立文章源自JAVA秀-https://www.javaxiu.com/6606.html
公众号运营至今,离不开小伙伴们的支持。为了给小伙伴们提供一个互相交流的平台,特地开通了官方交流群。扫描下方二维码备注 进群 或者关注公众号 Java后端编程 后获取进群通道。文章源自JAVA秀-https://www.javaxiu.com/6606.html
—————END—————文章源自JAVA秀-https://www.javaxiu.com/6606.html
推荐阅读:IDEA 永久注册码来了!!!分享5个免费的在线 SQL 数据库环境,简直太方便了!排名前 16 的 Java 工具类,哪个你没用过的?推荐一款国产开源监控系统,太强大了!!如何实现 MySQL 删除重复记录并且只保留一条?蚂蚁金服开源增强版 Spring Boot 的研发框架!最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。获取方式:关注公众号并回复 java 领取,更多内容陆续奉上。文章源自JAVA秀-https://www.javaxiu.com/6606.html
明天见(。・ω・。)ノ♡文章源自JAVA秀-https://www.javaxiu.com/6606.html

评论