(译) JSR 330

与传统方法(例如构造函数,工厂和服务定位器(例如,JNDI))相比,此包说明了以最大化可重用性,可测试性和可维护性的方式获取对象的方法。这个过程称为依赖注入,对大多数复杂应用程序来说都是有益的。

接口总结 .
Provider<T> 提供 T 的实例
注解类型总结 .
Inject 标识可注入的构造器,方法,以及域
Named 基于字符串的限定符
Qualifier 标识限定符注解
Scope 标识范围注解
Singleton 标识一种只初始化一次的注入

关于 javax.inject 的说明

与传统方法(例如构造函数,工厂和服务定位器(例如,JNDI))相比,此包说明了以最大化可重用性,可测试性和可维护性的方式获取对象的方法。这个过程称为依赖注入,对大多数复杂应用程序来说都是有益的。

许多类型依赖于其他类型。例如,stopwatch 可能依赖于 timeSource。一个类型所依赖的多个类型被称作它的依赖项。在运行时查找使用的依赖项实例的过程称为解析依赖项。如果找不到这样的实例,则说明依赖不满足,并且程序是坏的。

在没有依赖注入的情况下,对象可以通过几种方式解析其依赖关系。它可以调用构造函数,将对象直接硬连接到其依赖项的实现和生命周期:

1
2
3
4
5
6
class StopSource timeSource;
Stopwatch(){
timeSource = new AtomicClock(...);
}
void start(){...}
long stop(){...}

如果需要更大的灵活性,对象可以调用工厂或服务定位器:

1
2
3
4
5
6
7
8
class Stopwatch {
final TimeSource timeSource;
Stopwatch () {
timeSource = DefaultTimeSource.getInstance();
}
void start() { ... }
long stop() { ... }
}

在决定使用这些传统的依赖解析方法的时候,程序员必须进行权衡。构造函数更简洁但更具限制性。工厂在某种程度上将客户端和实现分离,但需要样板代码。服务定位器进一步分离,但减少了编译时类型的安全性。这三种方法都禁止单元测试。例如,如果程序员使用工厂,那么针对依赖于工厂的代码的每个测试都必须模拟出工厂并记得自行清理本身或其他副作用:

1
2
3
4
5
6
7
8
9
10
11
void testStopwatch() {
TimeSource original = DefaultTimeSource.getInstance();
DefaultTimeSource.setInstance(new MockTimeSource());
try {
// Now, we can actually test Stopwatch.
Stopwatch sw = new Stopwatch();
...
} finally {
DefaultTimeSource.setInstance(original);
}
}

在实践中,支持这种模拟工厂的能力会产生更多的样板代码。 在多个依赖项之后模拟和清理的测试很快就会失控。更糟糕的是,程序员必须准确地预测将来需要多大的灵活性,否则就会遭受后果。如果程序员最初选择使用构造函数但稍后决定需要更多的灵活性,则程序员必须替换对构造函数的每个调用。 如果程序员在谨慎方面犯错误并且预先编写工厂,则可能会导致许多不必要的样板代码,增加噪声,复杂性和容易出错。

依赖注入解决了所有这些问题。程序员不用在调用一个构造器或者工厂,一个叫做依赖注入的工具将依赖传递给对象。

1
2
3
4
5
6
7
8
class Stopwatch {
final TimeSource timeSource;
@Inject Stopwatch(TimeSource TimeSource) {
this.TimeSource = TimeSource;
}
void start() { ... }
long stop() { ... }
}

注入器会进一步传递依赖到其它依赖直到它构建完成整个对象图。例如,假定程序员请求注入器创建一个 StopwatchWidget 实例:

1
2
3
4
5
/** GUI for a Stopwatch */
class StopwatchWidget {
@Inject StopwatchWidget(Stopwatch sw) { ... }
...
}

注入器可能会:

  1. 查找 TimeSource
  2. 使用 TimeSource 构建 一个 Stopwatch
  3. 使用 Stopwatch 构建 StopwatchWidget

这使程序员的代码干净,灵活,并且相对没有依赖相关的基础结构。

在单元测试中,程序员现在可以直接构造对象(没有注入器)并传入模拟依赖项。 程序员不再需要在每次测试中设置和拆除工厂或服务定位器。 这大大简化了我们的单元测试:

1
2
3
4
void testStopwatch() {
Stopwatch sw = new Stopwatch(new MockTimeSource());
...
}

单元测试复杂性的总减少量与单元测试数量和依赖性数量的乘积成正比。

此包提供了启用可移植类的依赖项注入注解,但它将外部依赖项配置留给了注入器实现。程序员注解构造函数,方法和字段以宣称其可注入性(构造函数注入在上面的示例中演示)。依赖注入器通过检查这些注释来标识类的依赖关系,并在运行时注入依赖关系。此外,注入器可以验证在构建时已满足所有依赖性。相反,服务定位器在运行时之前无法检测到不满足的依赖关系。

注射器实现可以采用多种形式。注入器可以使用 XML,注解,DSL(特定于域的语言)甚至纯Java代码来配置自身。注入器可以依赖于反射或代码生成。使用编译时代码生成的注入器甚至可能没有自己的运行时表示。其他注入器可能根本无法在编译或运行时生成代码。对于某些定义,“容器”可以是注入器,但是该包装规范旨在最小化对注入器实施的限制。

注解类型 Inject

1
2
3
4
@Target(value={METHOD,CONTRUCTOR,FIELD})
@Rention(value=RUNTIME)
@Documented
public @interface Inject

标识可注入的构造器,方法,以及字段。可能应用到静态和实例成员。一个可注入成员可能有着任意的访问修饰符(private,package-private,protected,public)。构造体首先被注入,然后是字段,最后是方法。父类中的字段和方法比子类中的字段和方法先被注入。同一类的字段之间和方法之间的注入顺序是未指定的。

可注入构造器被加上 @Inject 注解,并接受零个或多个依赖作为参数。

@Inject 可以应用于每个类的最多一个构造函数。

1
@Inject ConstructorModifiersopt SimpleTypeName(FormalParameterListopt) Throwsopt ConstructorBody

当没有其他构造函数存在时,@Inject 对于 public ,no-argument 构造函数是可选的。这使注入器能够调用默认构造函数。

1
@Injectopt Annotationsopt public SimpleTypeName() Throwsopt ConstructorBody

可注入字段:

  • 被加上 @Inject 注解
  • 不是 final
  • 可能有任何其它有效的名称
1
@Inject FieldModifiersopt Type VariableDeclarators;

可注入方法:

  • 被加上 @Inject 注解
  • 不是抽象的
  • 不声明它们自己的类型参数
  • 可能返回一个结果
  • 可能有任何其它有效的名称
  • 接收零个或多个依赖作为参数

@Inject MethodModifiersopt ResultType Identifier(FormalParameterListopt) Throwsopt MethodBody

注入器忽略注入方法的结果,但允许非 void 返回类型支持在其他上下文中使用该方法(例如,构建器样式的方法链)。

例子:

1
2
3
4
5
6
7
8
9
10
public class Car {
// Injectable constructor
@Inject public Car(Engine engine) { ... }

// Injectable field
@Inject private Provider<Seat> seatProvider;

// Injectable package-private method
@Inject void install(Windshield windshield, Trunk trunk) { ... }
}

使用 @Inject 注解的方法将覆盖另一个使用 @Inject 注解的方法,每个实例的每个注入请求只会注入一次。不会注入没有 @Inject 注解的方法,该方法将覆盖使用 @Inject 注解的方法。

注入使用 @Inject 注解的成员是必需的。虽然可注入成员可以使用任何可访问性修饰符(包括私有),但平台或注入限制(如安全限制或缺乏反射支持)可能会阻止注入非公开成员。

Qualifiers

限定词可以注解可注入的字段或参数,并与该类型组合,标识要注入的实现。限定词是可选的,当与注入器无关的类中的 @Inject 一起使用时,不应该有多个限定符注解单个字段或参数。以下示例中的限定符为粗体:

public class Car {
   @Inject private @Leather Provider<Seat> seatProvider;

    @Inject void install(@Tinted Windshield windshield,
           @Big Trunk trunk) { ... }
}

如果一个可注入方法覆盖另一个,则覆盖方法的参数不会自动从所覆盖的方法的参数继承限定词。

可注入值

对于给定类型T和可选限定符,注入器必须能够注入用户指定的类:

  • 赋值与 T 兼容
  • 并有一个可注入的构造函数。

例如,用户可能使用外部配置来选择 T 的实现。除此之外,注入的值取决于注入器实现及其配置。

循环依赖

检测和解决循环依赖关系留作注入器实现的练习。两个构造函数之间的循环依赖关系是一个明显的问题,但你可以在可注入字段或方法之间存在循环依赖关系:

1
2
3
4
5
6
class A {
@Inject B b;
}
class B {
@Inject A a;
}

构造 A 的实例时,一个老实的注入器实现可能进入一个无限循环,构造一个 B 的实例设置在 A 上,一个 B 的第二个实例设置在 B 上,另一个 B 实例设置在 A 的第二个实例上, 等等。

保守的注入器可能在构建时检测到循环依赖并产生错误,此时程序员可以通过分别注入Provider<A> 或 Provider<B> 而不是 A 或 B 来打破循环依赖。直接从它被注入的构造函数或方法调用提供者的get(),会破坏提供者分解循环依赖的能力。在方法或字段注入的情况下,确定其中一个依赖项(例如,使用单一范围)也可以启用有效的循环关系。

注解类型 Qualifier

1
2
3
4
@Target(value=ANNOTATION_TYPE)
@Rentention(value=RUNTIME)
@Doucumented
public @interface Qualifier

标识限定词注解。任何人都可以定义一个新的限定词。一个限定词注解是这样的:

  • 加上了@Qualifier,@Rentention(RUNTIME)的注解,通常也有 @Documented
  • 有属性值
  • 可能是公共 API 的一部分,非常类似于依赖类型,但与实现类型不同的是它不需要成为公有 API 的一部分。如果使用 @Target 注释,可能会限制使用。虽然此规范仅涵盖将限定词应用于字段和参数,但某些注入器配置可能在其他位置使用限定词注释(例如,在方法或类上)。

例如:

1
2
3
4
5
6
7
@java.lang.annotation.Documented
@java.lang.annotation.Retention(RUNTIME)
@javax.inject.Qualifier
public @interface Leather {
Color color() default Color.TAN;
public enum Color { RED, BLACK, TAN }
}

注解类型 Named

1
2
3
4
@Qualifier
@Documented
@Retention(value=RUNTIME)
public @interface Named

基于 String 的限定词

使用的例子:

1
2
3
4
5
public class Car {
@Inject @Name("driver") Seat dirverSeat;
@Inject @Name("passenger") Seat passengerSeat;
...
}
可选的元素总结
java.lang.string value 名词

value

public abstarct java.lang.String value

名称.

默认值:””

注解类型 Scope

1
2
3
4
@Target(value=ANNOTATION_TYPE)
@Retention(value=RUNTIME)
@Documented
public @interface Scope

标识范围注解。范围注解适用于包含可注入构造函数的类,并控制注入器如何重用该类型的实例。默认情况下,如果不存在范围注释,则注入器会创建一个实例(通过注入类型的构造函数),使用实例进行一次注入,然后忘记它。如果存在范围注解,则注入器可以保留实例以便在稍后的注入中重复使用。如果多个线程可以访问限定了范围的实例,则其实现应该是线程安全的。范围本身的实现由注入器决定。

在以下的例子中,范围注解 @Singleton 确保我们只有一个 Log 实例:

1
2
3
4
@Singleton
class Log {
void log(string message){...}
}

如果注入器在同一个类上遇到多个范围注解或它不支持的范围注解,则会报错

范围注解:

  • 加上了@Scope,@Rentention(RUNTIME)注解,通常也有 @Documented。
  • 不应该有属性。
  • 通常不是 @Inherited,因此范围与实现继承是正交的。
  • 如果加了 @Target 注解,可能会限制使用。虽然此规范仅涵盖将范围应用于类,但某些注入器配置可能在其他位置使用范围注解(例如,在工厂方法结果上)。

例如:

1
2
3
4
@java.lang.annotation.Documented
@java.lang.annotation.Retention(RUNTIME)
@javax.inject.Scope
public @interface RequestScoped {}

使用 @Scope 范围注解有助于注入器检测程序员在类上使用范围注解但忘记在注入器中配置范围的情况。保守的注入器会报错不去申请范围。

注解类型 Singleton

1
2
3
4
@Scope
@Documented
@Rententation(value=RUNTIME)
public @interface Singleton

标识这个类型上注入器只初始化一次。并且不可以继承。

接口 Provider

public interface Provider

提供 T 的实例。通常由注入器实现。任意类型的 T 都可以被注入,你也可以注入 Provider 。与直接注入 T 相比,注入 Provider 使以下成为可能:

  • 获取多个实例
  • 懒加载或可选地获取实例
  • 打破循环依赖
  • 抽象范围所以你能在范围内的较小的范围中查找实例。

例如:

1
2
3
4
5
6
7
class Car{
@Inject Car(Provider<Seat> seatProvider){
Seat driver = seatProvider.get();
Seat passenger = seatProvider.get();
...
}
}
方法总结
T get() 提供构造完全和注入好的T实例。

方法细节

get

T get()

提供构造完全和注入好的T实例。

抛出的异常:

java.lang.RuntimeException - 如果注入器在提供实例时遇到错误。例如,如果 T 上的可注入成员抛出异常,则注入器可以包装异常并将其抛给 get() 的调用者。调用者不应尝试处理此类异常,因为行为可能因注入器实现方式而异,甚至同一注入器的配置也不尽相同。

非典型前端coder wechat
想要随时Follow我的最新博客,可扫码关注我的公众号