依赖注入
最开始听到依赖注入这个名词,是在某个java八股文介绍springboot。
后来工作了一段时间深有感触,依赖管理是一个无法避免的工程问题。
任何项目时间一长,就会产生各种各样的新对象。 如果不借助依赖注入框架,会产生很多的样板代码,以及增加单测的难度。
接下来将基于guice来讲解依赖注入的需求,思想,样例
注入是什么
假设你需要设计一个简单的retry执行器,负责间隔几秒重试
final class RetryExecutor {
public final int second;
RetryExecutor(int second) {
this.second = second;
}
public void execute(Runnable block) {
try {
block.call();
} catch(Exception e) {
TimeUnit.SECONDS.sleep(second);
}
}
}
当你开始写单测的时候,发现单测运行时间依赖sleep。 这是一个很严重的问题:second越大,sleep的时间越长。
因此最好的做法是将sleep功能注入
final class RetryExecutor {
public final int second;
public final Sleeper sleeper;
RetryExecutor(int second, Sleeper sleeper) {
this.second = second;
this.sleeper = sleeper;
}
public void execute(Runnable block) {
try {
block.call();
} catch(Exception e) {
sleeper.sleep(second);
}
}
}
这样在单测的时候就可以注入一个sleeper,避免阻塞单测执行。
这个例子里,RetryExecutor依赖Sleeper,也可以说Sleeper注入了RetryExecutor。
注入框架
想象你是一个资深javaer,有一天突然厌恶了随地大小new。你发现
- 依赖实际是一个树
- 可以用2型文法描述这棵树
- 分析完2后,可以交给一个执行器去完成new,注入,new。直到完成初始化
恭喜你发明了一个依赖注入框架!
使用注入框架可以省略很多样板代码。
需要注意的是,注入只负责对象初始化,下面的场景无法覆盖:
- 服务启停依赖分析。 服务启停可以用guava的service
- 运行时new对象(工厂类)
guice提供了几个绑定方式, 最常用的是linked,provider。
optinal看个人,socpe以及named其实没有必要,徒增复杂度。 另外考虑到了代码审计,不会特意在代码中留dev后门,一般情况下也没必要集成测试。
guice绑定方式
guice提供了几种注入的绑定方式
linked bind
链式绑定。最基础的绑定,将实现类绑定到抽象接口
public class BillingModule extends AbstractModule {
@Provides
TransactionLog provideTransactionLog(DatabaseTransactionLog databaseTransactionLog) {
return databaseTransactionLog;
}
@Provides
DatabaseTransactionLog provideDatabaseTransactionLog(MySqlDatabaseTransactionLog impl) {
return impl;
}
}
bind annotation
注解绑定。如果一个接口被多个类实现,或者绑定多个同一类型的实例,需要以注解的形式做区分。
public class RealBillingService implements BillingService {
@Inject
public RealBillingService(@PayPal CreditCardProcessor processor,
TransactionLog transactionLog) {
...
}
guice提供了@Named注解来方便我们区分
public class RealBillingService implements BillingService {
@Inject
public RealBillingService(@Named("Checkout") CreditCardProcessor processor,
TransactionLog transactionLog) {
...
}
instance binding
绑定实例,字面意思。直接将值绑定到实例上。一般用在基础数据类型比如int,String
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 methods
如果需要注入一个自己new的对象,可以用这个方式
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
...
}
@Provides
static TransactionLog provideTransactionLog() {
DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
transactionLog.setJdbcUrl("jdbc:mysql://localhost/pizza");
transactionLog.setThreadPoolSize(30);
return transactionLog;
}
}
untargeted bindings
简单绑定。可以直接绑定一个类,而不通过接口实现。
bind(MyConcreteClass.class);
bind(AnotherConcreteClass.class).in(Singleton.class);
Constructor Bindings
构造器绑定。如果你想绑定一个lib的对象,无法直接为lib代码加上@Inject注解,构造器绑定就派上用场。
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
try {
bind(TransactionLog.class).toConstructor(
DatabaseTransactionLog.class.getConstructor(DatabaseConnection.class));
} catch (NoSuchMethodException e) {
addError(e);
}
}
}
Build-in Bindings
guice内置了一些注入比如java.util.log,平时基本用不上。
Just-in-time Bindings
guice支持通过@Inject静态分析依赖树,然后生成绑定和注入的代码。
这也被称为隐式绑定。
MultiBindings
guice为重复绑定设计了一些集合,目前没碰到场景。todo
限制绑定
针对lib guice的场景设计。如果guice项目依赖了一个guice lib,lib不希望暴露内部的注入。
限制绑定就派上用场。
// Modules annotated with this Permit can provide Network bindings.
@RestrictedBindingSource.Permit
@Retention(RetentionPolicy.RUNTIME)
@interface NetworkPermit {}
// Bindings with the @IpAddress qualifier annotation can only be provided by
// modules with the NetworkPermit annotation.
@RestrictedBindingSource(
explanation = "Please install NetworkModule instead of binding network bindings yourself.",
permits = {NetworkPermit.class})
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface IpAddress {}
// The RoutingTable binding can only be provided by modules annotated with the
// NetworkPermit annotation.
@RestrictedBindingSource(
explanation = "Please install NetworkModule instead of binding network bindings yourself.",
permits = {NetworkPermit.class})
public interface RoutingTable {
int getNextHopIpAddress(int destinationIpAddress);
}
@NetworkPermit
public final class NetworkModule extends AbstractModule {
@Provides @IpAddress int provideIp( ... ) { ... }
@Override
protected void configure() {
// RoutingModule is permitted to provide the RoutingTable binding because
// it is installed by NetworkModule, which is annotated with NetworkPermit
// - ie. it's enough for any module providing a binding (directly or
// indirectly) to have the right permit.
install(new RoutingModule());
}
}
private final RoutingModule extends AbstractModule {
@Provides RoutingTable provideRoutingTable( ... ) { ... }
}
guice的最佳实践
来自guice wiki的best practice。 google的开发风格,批判看待。
这里挑几个个人认为重要的展开讲讲。
1. 尽量不是用Injector
guice内置了injector注入。
这让我们能更加方便动态修改注入,但是guice无法对动态注入依赖分析。
2. 直接注入
如果依赖一个类,直接注入,没必要通过间接类的注入来访问。
这也引出了1,常常碰到这样的代码:
return context.getInjector().getInstance(AlertPreTestExecJob.class);
完全没必要。但是没这么绝对,如果这样写更有条理其实也没事。
3.使用@Nullable
guice的注入会检查null。因此如果想注入可空依赖,必须使用@Nullable注解
特殊的,guice可以识别targets为PARAMETER和FIELD的@Nullable
4. 避免副作用对象
todo 目前没碰到