Google 开源的依赖注入库,比 Spring 更小更快!
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支付,这样通过连接绑定就搞不定。
这时我们可以通过注解绑定来实现:
@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高级资料,涵盖了java、redis、mongodb、mysql、zookeeper、spring cloud、dubbo高并发分布式等教程,一共30g,需要自己领取。
传送门:https://mp.weixin.qq.com/s/igmojff-bbmq6ircgo3mqa