TDD实现Spring(DI容器)

TDD 实现 DI 容器简介

TDD 的难点首先在于理解需求,并将需求分解为功能点。

以 Jakarta EE 中的 Jakarta Dependency Injection 为主要功能参考,并对其适当简化,以完成我们的目标

实现 DI 时参考 Jakarta Dependency Injection,其中的功能主要分为三部分:

  • 注入点的支持、组件的构造

  • 依赖的选择

  • 生命周期控制(多例和单例)

使用@Inject标注的方法或字段,被称为注入点

常见的注入方式有:构造函数注入、字段注入、方法注入

在 JSR330 中还包含两个可选的注入方式:静态方法的注入、静态字段的注入

如果不考虑易于测试的情况下,更倾向于构造函数注入

容器会找到被注入点,并找到所需的实例,再注入进来,来完成注入。

典型的错误是出现循环依赖的情况,JSR330 中规定了使用 Provider。

使用 Guice 演示 DI 容器如何使用,包含哪些功能

做解释的原因:在开发之前都需要澄清需求、理解需求

实际开发中,呈现需求的方式有:user story、PRD(Product Requirement Document,产品需求文档)等方式

在TDD中,也是不能直接上来就写测试的,也是需要先理解需求和上下文

Jakarta Dependency Injection 中没有规定而又常用的部分有:容器如何配置、容器层级结构以及生命周期回调。

  • 如何形成配置文件
  • 容器层级结构便于生命周期管理
  • 生命周期回调

这些功能步包含在 JRS330 中,更多的是在企业级环境中需要,所以不在当前项目的考虑范围中。

功能分解

对于组件构造部分,分解的任务大致如下:

  • 无需构造的组件:即直接将实例注册进容器

  • 如果注册的组件不可实例化,则抛出异常

    • 抽象类
    • 接口
  • 构造函数注入

    • 无依赖的组件应该通过默认构造函数生成组件实例
    • 有依赖的组件,通过 Inject 标注的构造函数生成组件实例
    • 如果所依赖的组件也存在依赖,那么需要对所依赖的组件也完成依赖注入
    • 如果组件有多于一个 Inject 标注的构造函数,则抛出异常
    • 如果组件需要的依赖不存在,则抛出异常
    • 如果组件间存在循环依赖,则抛出异常
  • 字段注入

    • 通过 Inject 标注将字段声明为依赖组件
    • 如果组件需要的依赖不存在,则抛出异常
    • 如果字段为 final 则抛出异常
    • 如果组件间存在循环依赖,则抛出异常
  • 方法注入

    • 通过 Inject 标注的方法,其参数为依赖组件

    • 通过 Inject 标注的无参数方法,会被调用

    • 按照子类中的规则,覆盖父类中的 Inject 方法

    • 如果组件需要的依赖不存在,则抛出异常

    • 如果方法定义类型参数,则抛出异常

    • 如果组件间存在循环依赖,则抛出异常

对于依赖选择部分,我分解的任务列表如下:

  • 对 Provider 类型的依赖

    • 注入构造函数中可以声明对于 Provider 的依赖
    • 注入字段中可以声明对于 Provider 的依赖
    • 注入方法中可声明对于 Provider 的依赖
  • 自定义 Qualifier 的依赖

    • 注册组件时,可额外指定 Qualifier

    • 注册组件时,可从类对象上提取 Qualifier

    • 寻找依赖时,需同时满足类型与自定义 Qualifier 标注

    • 支持默认 Qualifier——Named

对于生命周期管理部分,我分解的任务列表如下:

  • Singleton 生命周期

    • 注册组件时,可额外指定是否为 Singleton
    • 注册组件时,可从类对象上提取 Singleton 标注
    • 对于包含 Singleton 标注的组件,在容器范围内提供唯一实例
    • 容器组件默认不是 Single 生命周期
  • 自定义 Scope 标注

    • 可向容器注册自定义 Scope 标注的回调

新建项目

新建一个gradle项目,build.gradle.kts配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
plugins {
`java-library`
"jacoco"
}
repositories {
mavenCentral()
}
dependencies {
implementation("jakarta.inject:jakarta.inject-api:2.0.1")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.2")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2")
testRuntimeOnly("org.junit.platform:junit-platform-runner:1.8.2")
testImplementation("org.mockito:mockito-core:4.3.1")
testImplementation("jakarta.inject:jakarta.inject-tck:2.0.1")
}
tasks.withType<Test>() {
useJUnitPlatform()
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

开始红-绿-重构循环

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ContainerTest {

// 组件构造相关的测试类
@Nested
public class ComponentConstruction{

}

// 依赖选择相关的测试类
@Nested
public class DependenciesSelection{

}

// 生命周期管理相关的测试类
@Nested
public class LifecycleManagement{

}
}

先将要测试的内容使用@Nested注解隔离成不同的范围,这种结构可以帮助你更好地组织和编写测试。

在JUnit 5中,@Nested注解用于表示内部类,这些内部类可以作为特定测试的一部分。这种结构可以帮助你更好地组织和编写测试,特别是当你需要对一个类的不同方面或不同状态进行大量测试时。

使用@Nested注解的内部类可以有它们自己的测试方法,@BeforeEach@AfterEach方法,甚至它们自己的@BeforeAll@AfterAll方法。这使得你可以在每个内部类级别上设置和清理测试环境,从而为每个测试提供独立的环境。

如上代码中,ComponentConstruction类被标记为@Nested,这意味着它可以包含一组相关的测试,这些测试可以共享相同的初始化和清理代码。

将ComponentConstruction中的测试再细分为构造器注入、字段注入、方法注入等测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Nested
public class ComponentConstruction{

// TODO: instance
// TODO: abstract class
// TODO: interface

@Nested
public class ConstructorInjection{

}

@Nested
public class FieldInjection{

}

@Nested
public class MethodInjection{

}

}

给ConstructorInjection增加一些todo

1
2
3
4
5
6
@Nested
public class ConstructorInjection{
// TODO: No args constructor
// TODO: with dependencies
// TODO: A -> B -> C
}

TODO: instance

直接向容器中注册实例。

构造测试

新建测试,并使编译通过:

1
2
3
4
5
6
7
8
9
10
11
12
13
// TODO: instance
@Test
public void should_bind_type_to_a_specific_instance() {

Context context = new Context();

// 创建一个实现了 Component 接口的匿名内部类实例
Component instance = new Component() {
};
context.bind(Component.class, instance);

assertSame(instance, context.get(Component.class));
}

创建了一个匿名内部类(即 new Component() {}),它实现了 Component 接口。由于这是一个匿名内部类,所以它没有名字,但它的行为与任何其他实现了 Component 接口的类是一样的。

要使编译通过,需要创建Context和其中的bind、get方法:

1
2
3
4
5
6
7
8
9
10
public class Context {

public <ComponentType> void bind(Class<ComponentType> type, ComponentType instance) {

}

public <ComponentType> ComponentType get(Class<ComponentType> typeClass) {
return null;
}
}

编译通过,如果运行测试,那么这时会有异常:

1
2
3
4
5
6
org.opentest4j.AssertionFailedError: expected: <world.nobug.tdd.di.ContainerTest$ComponentConstruction$1@2dc9b0f5> but was: <null>
at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55)
at org.junit.jupiter.api.AssertSame.failNotSame(AssertSame.java:48)
at org.junit.jupiter.api.AssertSame.assertSame(AssertSame.java:37)
at org.junit.jupiter.api.AssertSame.assertSame(AssertSame.java:32)
at org.junit.jupiter.api.Assertions.assertSame(Assertions.java:2851)

快速实现

将bind的信息存入一个map中即可实现

1
2
3
4
5
6
7
8
9
10
11
12
public class Context {

private Map<Class<?>, Object> components = new HashMap<>();

public <ComponentType> void bind(Class<ComponentType> type, ComponentType instance) {
components.put(type, instance);
}

public <ComponentType> ComponentType get(Class<ComponentType> type) {
return (ComponentType) components.get(type);
}
}

接下来继续实现其他todo,一般我们先从happy path开始,这里先从构造器注入开始

TODO: No args constructor

向容器中注册一个类型,该类型有一个默认构造函数。

当需要从容器中获取get这个类型的实例时,容器应该调用这个默认构造函数以创建一个实例。

构造测试

1
2
3
4
5
6
7
8
9
10
11
12
// TODO: No args constructor
@Test
public void should_bind_type_to_a_class_with_default_constructor() {
Context context = new Context();

context.bind(Component.class, ComponentWithDefaultConstructor.class);

Component instance = context.get(Component.class);

assertNotNull(instance);
assertInstanceOf(ComponentWithDefaultConstructor.class, instance);
}

Context新增bind方法

1
2
3
4
public <ComponentType, ComponentImplementation extends ComponentType>
void bind(Class<ComponentType> type, Class<ComponentImplementation> implementation) {

}

快速实现

因为要支持两种bind方式,所以要快速实现并不容易。

当然如果在不计任何罪恶的情况下,也可以再新建一个Map保存这种bind形式的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Context {

private Map<Class<?>, Object> components = new HashMap<>();
private Map<Class<?>, Class<?>> componentImplementations = new HashMap<>();

public <ComponentType> void bind(Class<ComponentType> type, ComponentType instance) {
components.put(type, instance);
}

public <ComponentType> ComponentType get(Class<ComponentType> type) {
if (components.containsKey(type))
return (ComponentType) components.get(type);
Class<?> implementation = componentImplementations.get(type);
try {
// 获取到默认构造函数,并创建实例
return (ComponentType)implementation.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public <ComponentType, ComponentImplementation extends ComponentType>
void bind(Class<ComponentType> type, Class<ComponentImplementation> implementation) {
componentImplementations.put(type, implementation);
}
}

重构

两个map并不是一个合理的实现方式,并且还有if else,这些都是坏味道。需要重构,并且前面的测试已经证明了功能的可用性。有了测试的保证就可以进行安全的重构。

如何重构:将这两个map中的value值类型合并为使用同一个interface,或者说使用同一种形式的API。

在JSR330中已经提供了一个Provider

1
2
3
public interface Provider<T> {
T get();
}

其实这就是一个Factory

在注册时,将这两个注册的方法,分别的变成Provider

1
private Map<Class<?>, Provider<?>> providers = new HashMap<>();

Java8提供了与Provider接口类似的Supplier函数式接口,这里只是选用了JSR330的接口,功能是一样的

接下来就是进行逐步替换掉这两个Map

替换components

先替换第一个bind方法:

1
2
3
4
public <ComponentType> void bind(Class<ComponentType> type, ComponentType instance) {
components.put(type, instance);
providers.put(type, () -> instance);
}

对应的修改get方法,将用到components的地方替换为使用providers

1
2
3
4
5
6
7
8
9
10
public <ComponentType> ComponentType get(Class<ComponentType> type) {
if (providers.containsKey(type))
return (ComponentType) providers.get(type).get();
Class<?> implementation = componentImplementations.get(type);
try {
return (ComponentType)implementation.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

修改代码后,运行测试

紧接着移除,bind方法中的components语句,就会发现components的map就不需要使用了,可以将这个map移除

1
2
3
4
5
6
private Map<Class<?>, Class<?>> componentImplementations = new HashMap<>();
private Map<Class<?>, Provider<?>> providers = new HashMap<>();

public <ComponentType> void bind(Class<ComponentType> type, ComponentType instance) {
providers.put(type, () -> instance);
}

替换componentImplementations

同理,针对componentImplementations这个map做替换,替换完成后Context的现实如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Context {

private Map<Class<?>, Provider<?>> providers = new HashMap<>();

public <ComponentType> void bind(Class<ComponentType> type, ComponentType instance) {
providers.put(type, () -> instance);
}

public <ComponentType, ComponentImplementation extends ComponentType>
void bind(Class<ComponentType> type, Class<ComponentImplementation> implementation) {
providers.put(type, () -> {
try {
return implementation.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}

public <ComponentType> ComponentType get(Class<ComponentType> type) {
return (ComponentType) providers.get(type).get();
}
}

至此,就已经实现了一个基本的DI容器的结构,之后就是要围绕 DI 容器的基本结构,对其进行更多功能上的完善。

重构总结

在重构的时候,我采用的是增加一个平行实现(Parallel Implementation)。用平行实现替换原有功能,然后再删除原有实现的做法。

简单重构

在继续后面的功能之前,先梳理一下测试,进行一些简单的重构。

目前每一个测试中都需要构造一个新的Context,可以预见到后续的每一个测试也都需要构造Context。

重构测试,将构造新的Context的动作放到setup中,并移除掉后续方法中创建Context的语句。

1
2
3
4
5
6
Context context;

@BeforeEach
public void setUp(){
context = new Context();
}

重构测试,将Component接口,及其相关子类移动到ContainerTest的外部,方便阅读。

Snipaste_2024-08-08_16-20-52

TODO: with dependencies

构造被@Inject标注的构造函数的测试,并通过编译

构造测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// TODO: with dependencies
@Test
public void should_bind_type_to_a_class_with_inject_constructor() {
Dependency dependency = new Dependency() {
};
context.bind(Component.class, ComponentWithInjectConstructor.class);
context.bind(Dependency.class, dependency);

Component instance = context.get(Component.class);
assertNotNull(instance);
assertSame(dependency, ((ComponentWithInjectConstructor) instance).getDependency());
}

interface Dependency{
}

class ComponentWithInjectConstructor implements Component{
private Dependency dependency;

@Inject
public ComponentWithInjectConstructor(Dependency dependency){
// 注意,一定要记得赋值
this.dependency = dependency;
}

// 用于测试验证dependency是否被注入
public Dependency getDependency() {
return dependency;
}
}

简单重构

简单重构,重命名范型名称,简短一点

ComponetType -> Type

-> Implementation

快速实现

第一步

修改newInstance时的代码,创建时应该传入依赖的实例,但目前还是使用默认构造函数,所以还是依然会出错,但这只是第一步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public <Type, Implementation extends Type>
void bind(Class<Type> type, Class<Implementation> implementation) {
providers.put(type, () -> {
try {
Constructor<Implementation> injectConstructor = implementation.getConstructor();
// 根据构造函数的参数,获取依赖的实例
Object[] dependencies = Arrays.stream(injectConstructor.getParameters())
.map(p -> get(p.getType()))
.toArray(Object[]::new);
return injectConstructor.newInstance(dependencies);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}

第二步

提取获取构造器的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public <Type, Implementation extends Type>
void bind(Class<Type> type, Class<Implementation> implementation) {
providers.put(type, () -> {
try {
Constructor<Implementation> injectConstructor = getInjectConstructor(implementation);
// 根据构造函数的参数,获取依赖的实例
Object[] dependencies = Arrays.stream(injectConstructor.getParameters())
.map(p -> get(p.getType()))
.toArray(Object[]::new);
return injectConstructor.newInstance(dependencies);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}

获取带@Inject的构造器或默认构造器

1
2
3
4
5
6
7
8
9
10
11
12
private static <Type> Constructor<Type> getInjectConstructor(
Class<Type> implementation) {
Stream<Constructor<?>> injectConstructors = Arrays.stream(implementation.getConstructors())
.filter(c -> c.isAnnotationPresent(Inject.class));
return (Constructor<Type>) injectConstructors.findFirst().orElseGet(() -> {
try {
return implementation.getConstructor();
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
});
}

TODO: A -> B -> C

有传递性的依赖

构造测试

构造并运行测试,会发现测试直接通过,说明当前的生产代码已经满足了我们的功能需求,不需要修改生产代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TODO: A -> B -> C
@Test
public void should_bind_type_to_a_class_with_inject_transitive_dependencies() {
context.bind(Component.class, ComponentWithInjectConstructor.class);
context.bind(Dependency.class, DependencyWithInjectConstructor.class);
context.bind(String.class, "Hello World!");

Component instance = context.get(Component.class);
assertNotNull(instance);

Dependency dependency = context.get(Dependency.class);
assertNotNull(dependency);

assertEquals("Hello World!", ((DependencyWithInjectConstructor) dependency).getDependency());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class DependencyWithInjectConstructor implements Dependency{
// 直接使用字符串类型,不新建接口,简化开发
private String dependency;

@Inject
public DependencyWithInjectConstructor(String dependency){
this.dependency = dependency;
}

public String getDependency() {
return dependency;
}
}

何时处理sad path

到目前为止,部分happy path已经完成,还剩下一些sad path,这样我们就有了一个选择,就是继续做happy path(去做FieldInjection、MethodInjection)还是做sad path

两种选择都可以,但是有些不一样的地方。

应该在经过一定时间的happy path的任务编写后,应该转到sad path,前面的happy path是为了尽快确定我们的代码结构,同时sad path也会需要我们调整代码结构,所以应该及时在开发了一段时间的happy path需求后引入一些sad path来促进代码结构的变化。

TODO:multi inject constructors

有多个构造函数被@Inject注解标记的情况,JSR330中规定只能有一个构造函数被@Inject标注

构造测试

创建包含两个被@Inject注解标记的构造方法的类作为测试数据

1
2
3
4
5
6
7
8
9
10
class ComponentWithMultiInjectConstructors implements Component{

@Inject
public ComponentWithMultiInjectConstructors(String name, Double value){
}

@Inject
public ComponentWithMultiInjectConstructors(String name){
}
}

测试代码:

1
2
3
4
5
6
7
// TODO:multi inject constructors
@Test
public void should_throw_exception_if_multi_inject_constructors_provided() {
assertThrows(IllegalComponentException.class, () -> {
context.bind(Component.class, ComponentWithMultiInjectConstructors.class);
});
}

这里是在bind的时候校验是否异常,也可以在get的时候校验异常:

1
2
3
assertThrows(IllegalComponentException.class, () -> {
context.get(Component.class);
});

但是这里选择在bind是校验的原因是,可以及时短路,也会使后续的代码更加简单。

创建IllegalComponentException异常类

1
2
public class IllegalComponentException extends RuntimeException {
}

快速实现

在bind方法中增加校验

1
2
3
4
Constructor<?>[] injectConstructors =
Arrays.stream(implementation.getConstructors()).filter(c -> c.isAnnotationPresent(Inject.class))
.toArray(Constructor<?>[]::new);
if (injectConstructors.length > 1) throw new IllegalComponentException();

TODO: no default constructor and inject constructor

没有默认构造函数且没有被@Inject注解标注的构造函数的情况

构造测试

构造没有默认构造函数和被@Inject注解标注的构造函数的测试类

1
2
3
4
5
class ComponentWithNoInjectConstructorNorDefaultConstructor implements Component {

public ComponentWithNoInjectConstructorNorDefaultConstructor(String name) {
}
}

测试方法:

1
2
3
4
5
6
7
// TODO: no default constructor and inject constructor
@Test
public void should_throw_exception_if_no_inject_constructor_nor_default_constructor_provided() {
assertThrows(IllegalComponentException.class, () -> {
context.bind(Component.class, ComponentWithNoInjectConstructorNorDefaultConstructor.class);
});
}

快速实现

在bind方法中增加校验

1
2
3
if (injectConstructors.length < 1 &&
Arrays.stream(implementation.getConstructors()).noneMatch(c -> c.getParameterCount() == 0))
throw new IllegalComponentException();

重构

调整 bind 中对默认构造函数的校验逻辑

通过观察发现在 bind 中找校验构造函数是否合规的方法,和后面的 getInjectConstructor 的方法的逻辑是有部分重叠的,都需要获取到构造函数的列表。

在 getInjectConstructor 中会校验默认构造函数的情况,只需要在 getInjectConstructor 方法中抛出 IllegalComponentException 异常,并将 providers.put 方法中的 getInjectConstructor 方法提前到 put 方法之前,就可以移除掉 bind 方法中对默认构造函数的校验。

将 providers.put 方法中的 getInjectConstructor 方法提前到 put 方法之前,是因为 put 时只是创建一个匿名内部类,并不会执行 getInjectConstructor 方法,getInjectConstructor 方法是在 get 时调用。

调整 bind 中对多个被 Inject 标注的构造函数的校验逻辑

同理,也可将校验是否有多个被 Inject 标注的构造函数的逻辑放到 getInjectConstructor 方法中

这样就可以将 bind 中的校验代码移除,改写后的 getInjectConstructor 方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static <Type> Constructor<Type> getInjectConstructor(
Class<Type> implementation) {
List<Constructor<?>> injectConstructors = Arrays.stream(implementation.getConstructors())
.filter(c -> c.isAnnotationPresent(Inject.class)).toList();
if (injectConstructors.size() > 1) throw new IllegalComponentException();

return (Constructor<Type>) injectConstructors.stream().findFirst().orElseGet(() -> {
try {
return implementation.getConstructor();
} catch (NoSuchMethodException e) {
throw new IllegalComponentException();
}
});
}

TODO: dependencies not exist

组件中的依赖不存在的情况

构造测试

1
2
3
4
5
6
7
// TODO: dependencies not exist
@Test
public void should_throw_exception_if_dependency_not_found() {
context.bind(Component.class, ComponentWithInjectConstructor.class);

assertThrows(DependencyNotFoundException.class, () -> {context.get(Component.class);});
}

复用了 ComponentWithInjectConstructor

1
2
3
4
5
6
7
8
9
10
11
12
13
class ComponentWithInjectConstructor implements Component{
private Dependency dependency;

@Inject
public ComponentWithInjectConstructor(Dependency dependency){
this.dependency = dependency;
}

// 用于测试验证dependency是否被注入
public Dependency getDependency() {
return dependency;
}
}

创建 DependencyNotFoundException 异常类

1
2
public class DependencyNotFoundException extends RuntimeException {
}

运行测试,会抛异常:

1
2
3
4
5
6
7
8
9
10
11
Caused by: java.lang.NullPointerException: Cannot invoke "jakarta.inject.Provider.get()" because the return value of "java.util.Map.get(Object)" is null
at world.nobug.tdd.di.Context.get(Context.java:52)
at world.nobug.tdd.di.Context.lambda$bind$1(Context.java:27)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:992)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:575)
at java.base/java.util.stream.AbstractPipeline.evaluateToArrayNode(AbstractPipeline.java:260)
at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:616)
at world.nobug.tdd.di.Context.lambda$bind$3(Context.java:28)

实现

根据以上的异常,定位到的问题是:

1
2
3
public <Type> Type get(Class<Type> type) {
return (Type) providers.get(type).get();
}

注入依赖时,需要先 get 到对应依赖的实例,但是当前没有实例,这里会抛 NullPointerException 异常。

快速的实现方式是在 get 时校验实例是否存在:

1
2
3
4
public <Type> Type get(Class<Type> type) {
if (!providers.containsKey(type)) throw new DependencyNotFoundException();
return (Type) providers.get(type).get();
}

注意,在 bind 方法中,需要修改软化异常的代码,仅将跟反射调用相关的异常软化为 RuntimeException

image-20240809161723867

TODO: component does not exist

基于前面的测试,我们还可以想到有直接获取组件的情况。

上一个测试用例是通过依赖关系去组件不存在的情况,这个测试用例是直接取组件但是不存在的情况。

构造测试

在这个场景下,get 方法会返回 DependencyNotFoundException 异常,因为这个 get 方法也是一个直接对外的 API,直接抛 DependencyNotFoundException 很多时候都不太合理,而且这个异常的名称也不太合理。

按目前的编程风格,我们更倾向于这种情况返回一个 null,返回一个 Optional

1
2
3
4
5
// TODO: component does not exist
@Test
public void should_() {
Optional<Component> component = context.get_(Component.class);
}

上面的代码调用的是 get_,因为 get 方法在即对外开放也被内部多个地方调用,并且返回值也发生了变化,所以考虑定义一个新的方法。

这么做的化其实也是为了适应测试的需要在做重构。

为测试做重构

第一步,新建 get_ 方法:

1
2
3
4
5
6
7
8
9
public <Type> Type get(Class<Type> type) {
if (!providers.containsKey(type)) throw new DependencyNotFoundException();
return (Type) providers.get(type).get();
}

// 签名原来的 get 方法保持一致就可以了
public <Type> Optional<Type> get_(Class<Type> type) {
return null;
}

第二步,基于这个 get_ 方法,重构测试:

1
2
3
4
5
6
// TODO: component does not exist
@Test
public void should_return_empty_if_component_not_defined() {
Optional<Component> component = context.get_(Component.class);
assertTrue(component.isEmpty());
}

实现

第一步,修改 get_ 方法

1
2
3
public <Type> Optional<Type> get_(Class<Type> type) {
return Optional.ofNullable(providers.get(type)).map(provider -> (Type)provider.get());
}

运行测试,所有测试通过。

第二步,修改 get 方法,将 get 方法的实现委托给 get_ 方法:

1
2
3
public <Type> Type get(Class<Type> type) {
return get_(type).orElseThrow(DependencyNotFoundException::new);
}

第三步,inline get 方法,即可以将 get 方法移除掉

第四步,将 get_ 方法重命名为 get

第五步,移除部分 get 方法后的 .orElseThrow() ,修改为 .get()

TODO: cyclic dependencies

循环依赖的场景

希望在出现循环依赖时抛出指示循环依赖的异常

直接循环依赖

A -> B -> A

构造测试

这个 DependencyDependedOnComponent 依赖于 Component

1
2
3
4
5
6
7
8
class DependencyDependedOnComponent implements Dependency{
private Component component;

@Inject
public DependencyDependedOnComponent(Component component){
this.component = component;
}
}
1
2
3
4
5
6
7
8
// TODO: cyclic dependencies
@Test
public void should_throw_exception_if_cyclic_dependencies() {
context.bind(Component.class, ComponentWithInjectConstructor.class);
context.bind(Dependency.class, DependencyDependedOnComponent.class);

assertThrows(CyclicDependenciesException.class, () -> context.get(Component.class));
}

运行测试,抛出如下 StackOverflowError 异常:

1
2
3
4
5
6
7
8
9
10
11
12
Caused by: java.lang.StackOverflowError
at java.base/java.lang.reflect.Executable.getParameters(Executable.java:370)
at world.nobug.tdd.di.Context.lambda$bind$3(Context.java:28)
at world.nobug.tdd.di.Context.lambda$get$6(Context.java:54)
at java.base/java.util.Optional.map(Optional.java:260)
at world.nobug.tdd.di.Context.get(Context.java:54)
at world.nobug.tdd.di.Context.lambda$bind$1(Context.java:29)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:992)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:575)

出现这个异常的原因是,get 时会去递归调用 get 方法用于获取依赖的组件的实例。

实现

目前创建组件实例方式,是通过在 bind 时 put 一个类型对应的 Provider 工厂,并在 get 时使用这个工厂来创建实例。

在目前的情况下,我们是无法知道哪个哪个实例正在创建中的,所以就会一直递归执行 get 方法。

我们预期的实现方式是为需要构造的组件增加一个正在构造的标记,那么当第二次尝试构造这个组件时发现这个组件正在构造,那么就产生了循环依赖。

在我们的实现中是使用匿名的 Provider 来 new 对象。那么如果我们能识别出访问过两次同一个 Provider,那么就产生了循环依赖。

这里需要做的就是将匿名的 Provider,变成具体的类,并在这个类上保持一个是否正在创建的标志。

重构

将 bind 方法中的匿名内部类创建方法提取为函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public <Type, Implementation extends Type>
void bind(Class<Type> type, Class<Implementation> implementation) {
Constructor<Implementation> injectConstructor = getInjectConstructor(implementation);

providers.put(type, getTypeProvider(injectConstructor));
}

private <Type> Provider<Object> getTypeProvider(Constructor<Type> injectConstructor) {
return () -> getImplementation(injectConstructor); // 预期将变成,new xxxx(injectConstructor)的形式
}

private <Type> Type getImplementation(Constructor<Type> injectConstructor) {
try {
// 根据构造函数的参数,获取依赖的实例
Object[] dependencies = Arrays.stream(injectConstructor.getParameters())
.map(p -> get(p.getType()).orElseThrow(DependencyNotFoundException::new))
.toArray(Object[]::new);
return injectConstructor.newInstance(dependencies);
} catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}

重构

新建 Provider 的实现类:ConstructorInjectionProvider

1
2
3
4
5
6
7
8
9
10
11
12
class ConstructorInjectionProvider<T> implements Provider<T>{
private Constructor<T> injectConstructor;

public ConstructorInjectionProvider(Constructor<T> injectConstructor) {
this.injectConstructor = injectConstructor;
}

@Override
public T get() {
return getImplementation(injectConstructor);
}
}

修改获取 Provider 的方法

1
2
3
private <Type> Provider<Object> getTypeProvider(Constructor<Type> injectConstructor) {
return new ConstructorInjectionProvider(injectConstructor); // 变成,new xxxx(injectConstructor)的形式
}

运行测试,依然之后循环依赖的代码失败。

inline 提取的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public <Type, Implementation extends Type>
void bind(Class<Type> type, Class<Implementation> implementation) {
Constructor<Implementation> injectConstructor = getInjectConstructor(implementation);

providers.put(type, new ConstructorInjectionProvider(injectConstructor));
}

class ConstructorInjectionProvider<T> implements Provider<T>{
private Constructor<T> injectConstructor;

public ConstructorInjectionProvider(Constructor<T> injectConstructor) {
this.injectConstructor = injectConstructor;
}

@Override
public T get() {
try {
// 根据构造函数的参数,获取依赖的实例
Object[] dependencies = Arrays.stream(injectConstructor.getParameters())
.map(p -> Context.this.get(p.getType()).orElseThrow(DependencyNotFoundException::new))
.toArray(Object[]::new);
return injectConstructor.newInstance(dependencies);
} catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

实现

在 Provider 的实现类 ConstructorInjectionProvider 中增加 constructing标志位,以指示是否在构建中,并实现循环依赖的检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ConstructorInjectionProvider<T> implements Provider<T>{
private Constructor<T> injectConstructor;
private boolean constructing = false;

public ConstructorInjectionProvider(Constructor<T> injectConstructor) {
this.injectConstructor = injectConstructor;
}

@Override
public T get() {
// 如果在构建中就抛异常
if (constructing) throw new CyclicDependenciesException();
try {
constructing = true;
// 根据构造函数的参数,获取依赖的实例
Object[] dependencies = Arrays.stream(injectConstructor.getParameters())
.map(p -> Context.this.get(p.getType()).orElseThrow(DependencyNotFoundException::new))
.toArray(Object[]::new);
return injectConstructor.newInstance(dependencies);
} catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
} finally {
constructing = false;
}
}
}

传递性的循环依赖

A -> B -> C -> A

构造测试

1
2
3
4
5
6
7
8
@Test // A -> B -> C -> A
public void should_throw_exception_if_transitive_cyclic_dependencies() {
context.bind(Component.class, ComponentWithInjectConstructor.class);
context.bind(Dependency.class, DependencyDependedOnAnotherDependency.class);
context.bind(AnotherDependency.class, AnotherDependencyDependedOnComponent.class);

assertThrows(CyclicDependenciesException.class, () -> context.get(Component.class));
}

新增两个测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class AnotherDependencyDependedOnComponent implements AnotherDependency{
private Component component;

@Inject
public AnotherDependencyDependedOnComponent(Component component){
this.component = component;
}
}

class DependencyDependedOnAnotherDependency implements Dependency{
private AnotherDependency anotherDependency;

@Inject
public DependencyDependedOnAnotherDependency(AnotherDependency anotherDependency){
this.anotherDependency = anotherDependency;
}
}

运行测试,所有测试依然可以通过。

重构

整理代码位置

移动代码的位置,使容器的接口都集中到一起。

image-20240809184214486

优化异常信息

从API的角度来看,目前的异常处理部分返回的信息并不清晰,作为一个使用者,希望能从异常中获取到更多的有效信息。

DependencyNotFoundException

对于依赖不存在的情况,使用者希望明确知道是哪个依赖不存在。

直接依赖缺失的情况

修改测试用例,在异常中增加缺失的依赖的信息

1
2
3
4
5
6
7
8
9
10
11
// dependencies not exist
@Test
public void should_throw_exception_if_dependency_not_found() {
context.bind(Component.class, ComponentWithInjectConstructor.class);

DependencyNotFoundException exception = assertThrows(DependencyNotFoundException.class, () -> {
context.get(Component.class).get();
});

assertEquals(Dependency.class, exception.getDependency());
}

修改 DependencyNotFoundException 以适应需求

1
2
3
4
5
6
7
8
9
10
11
public class DependencyNotFoundException extends RuntimeException {
private Class<?> dependency;

public DependencyNotFoundException(Class<?> dependency) {
this.dependency = dependency;
}

public Class<?> getDependency() {
return dependency;
}
}

修改异常的定义后,需要修改抛出异常时的创建代码:

image-20240809185759277

编译通过后,运行测试,所有测试都通过。

传递性中的依赖缺失的情况

新增一个测试

1
2
3
4
5
6
7
8
9
10
11
@Test
public void should_throw_exception_if_transitive_dependency_not_found() {
context.bind(Component.class, ComponentWithInjectConstructor.class);
context.bind(Dependency.class, DependencyWithInjectConstructor.class); // 缺失 String 类型的依赖

DependencyNotFoundException exception = assertThrows(DependencyNotFoundException.class, () -> {
context.get(Component.class);
});

assertEquals(String.class, exception.getDependency());
}

在这种情况下,DependencyNotFoundException 异常中,只能返回缺失的依赖是哪个,但是并不知道是哪个组件缺失依赖。

所以,使用者还希望在 DependencyNotFoundException 中获取到缺失依赖的组件的信息。

修改测试代码,异常中增加缺失依赖的组件的信息:

image-20240809191402158

修改 DependencyNotFoundException,增加 component 属性信息和构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DependencyNotFoundException extends RuntimeException {
private Class<?> dependency;
private Class<?> component;

public DependencyNotFoundException(Class<?> dependency) {
this.dependency = dependency;
}

public DependencyNotFoundException(Class<?> component, Class<?> dependency) {
this.dependency = dependency;
this.component = component;
}

public Class<?> getDependency() {
return dependency;
}

public Class<?> getComponent() {
return component;
}
}

通过 Find Usages 找到,单个参数的构造函数在哪里被使用,这里是只被一处地方使用,我们现在需要将使用的地方修改为使用两个参数的构造函数

如果有多个地方使用了这个构造函数的话,建议通过工厂方法的方式替换掉这个构造函数

在抛出异常时返回缺失依赖的组件信息,由于创建 ConstructorInjectionProvider 时并没有传入组件的信息

所以需要修改 ConstructorInjectionProvider 记录组件的信息,在其中增加 componentType 字段信息,并相应的修改必要代码

image-20240809194930682

另外一种可行的方案是直接通过 injectConstructor 的 getDeclaringClass 方法,返回该构造器所属的类。

Snipaste_2024-08-09_19-43-11

但是这个方法返回的就不是 Dependency,而是其子类 DependencyWithInjectConstructor

1
2
Expected :interface world.nobug.tdd.di.Dependency
Actual :class world.nobug.tdd.di.DependencyWithInjectConstructor

如果使用这个方法的话就需要修改测试为:

1
assertEquals(DependencyWithInjectConstructor.class, exception.getComponent());

我们是将 DependencyWithInjectConstructor 绑定到 Dependency

1
context.bind(Dependency.class, DependencyWithInjectConstructor.class); 

所以,最好还是校验:

1
assertEquals(Dependency.class, exception.getComponent());

CyclicDependenciesException

同理,和 DependencyNotFoundException 类似,使用者希望从循环依赖异常中获得更多的异常信息,比如是哪两个组件之间出现了循环依赖或引发循环依赖的组件是哪一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test // A -> B -> A
public void should_throw_exception_if_cyclic_dependencies() {
context.bind(Component.class, ComponentWithInjectConstructor.class);
context.bind(Dependency.class, DependencyDependedOnComponent.class);

CyclicDependenciesException exception =
assertThrows(CyclicDependenciesException.class, () -> context.get(Component.class));

Set<Class<?>> classes = Sets.newSet(exception.getComponents());

assertEquals(2, classes.size());
assertTrue(classes.contains(Component.class));
assertTrue(classes.contains(Dependency.class));
}

修改异常,在异常中增加组件相互循环依赖的组件信息

1
2
3
4
5
6
public class CyclicDependenciesException extends RuntimeException{

public Class<?>[] getComponents() {
return new Class<?>[0]; // 并没有实际的功能,只是为了使编译通过
}
}

find usages 发现,只有在 Provider 的 get 方法中会抛出循环依赖的异常,那么需要在抛出异常时传入当前类型的信息

image-20240810095457438

修改异常类,创建构造函数,并增加保存循环依赖的容器

1
2
3
4
5
6
7
8
9
10
11
public class CyclicDependenciesException extends RuntimeException{
private Set<Class<?>> components = new HashSet<>();

public CyclicDependenciesException(Class<?> componentType) {
components.add(componentType);
}

public Class<?>[] getComponents() {
return components.toArray(Class<?>[]::new);
}
}

又因为,get 方法是一个递归调用的方法,所以第一次抛出循环依赖的异常是内层的 get 方法抛出的,那么外层的 get 方法不能吞掉/软化循环依赖的异常,并且需要在这个异常中增加外层的组件类型信息。

image-20240810101133637

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CyclicDependenciesException extends RuntimeException{
private Set<Class<?>> components = new HashSet<>();

public CyclicDependenciesException(Class<?> componentType) {
components.add(componentType);
}

public CyclicDependenciesException(Class<?> componentType, Class<?>[] components) {
this.components.add(componentType);
this.components.addAll(Set.of(components));
}

public Class<?>[] getComponents() {
return components.toArray(Class<?>[]::new);
}
}

补全具有传递性依赖的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test // A -> B -> C -> A
public void should_throw_exception_if_transitive_cyclic_dependencies() {
context.bind(Component.class, ComponentWithInjectConstructor.class);
context.bind(Dependency.class, DependencyDependedOnAnotherDependency.class);
context.bind(AnotherDependency.class, AnotherDependencyDependedOnComponent.class);

CyclicDependenciesException exception =
assertThrows(CyclicDependenciesException.class, () -> context.get(Component.class));

List<Class<?>> components = Arrays.stream(exception.getComponents()).toList();

assertEquals(3, components.size());
assertTrue(components.contains(Component.class));
assertTrue(components.contains(Dependency.class));
assertTrue(components.contains(AnotherDependency.class));
}

将依赖的检查提前到获取实例之前

目前对循环依赖的检查是在调用 get 方法从容器中获取实例时触发的,更好的方式是在 bind 时就校验是否存在循环依赖。

如果直接在 bind 中检查循环依赖的话,那么在当前类型在 bind 时必须保证其依赖的类型先被 bind,但是这对 API 的使用者来说是很不友好的,这样会要求使用者必须自己控制相互依赖的类型的 bind 顺序。

通常我们对于IOC容器的要求是:根据配置文件构建容器上下文之后,很少进行修改。

IOC 容器是有一个明确的生命周期的,所有配置文件都被 load 好了,然后把容器 build 出来,一但上下文 build 好了,很少要求对上下文进行修改。

所以还是要在获取容器时检查依赖。

所以预期修改后的结果大致是,从 context 中 get 一个 container,再从 container 中获取实例。

这个时候 context 就类似于一个 configuration。

这样就将构造的环境和真正构造好的对象的使用环节分开了,就是利用构造器模式来解决这个问题。

重构-将 Builder 和 Context 上下文分开

将 Context 改名为 ContextConfig

移动 get 方法

接着需要将 get 方法从 Context 中移动到其他地方,因为现在 ContextConfig 是要作为一个配置文件,不应该包含 get 实例的方法。

bind 方法可以视为设置配置文件的操作

最简单的做法是,先将 ContextConfig 实现一个 Context 接口

因为在我们当前的代码中,ContextConfig 即是配置文件也是上下文容器本身。

1
2
3
public class ContextConfig implements Context {
......
}

创建 Context 接口

1
2
public interface Context {
}

接着需要将 get 方法挪到 Context 接口中去

使用 Pull Members Up 重构方法,来挪动

image-20240810110504080

image-20240810110639340

挪动之后,Context 接口的变化:

1
2
3
public interface Context {
<Type> Optional<Type> get(Class<Type> type);
}

image-20240810110805138

接下来需要做的就是,让 get 方法不再直接调用 contextConfig 的内容,即需要将 get 方法从 config 中移除掉,但同时还要保持现在的功能。

第一查找替换的方式重构

创建一个获取 Context 的方法,这样才能在

预期要做的就是将 Context 中的 get 方法实现为和当前 ContextConfig 中的 get 方法一致。

1
2
3
4
5
6
7
8
public Context getContext() {
return new Context() {
@Override
public <Type> Optional<Type> get(Class<Type> type) {
return Optional.empty();
}
};
}

将 get 方法的实现提取为方法:

1
2
3
4
5
6
7
8
@Override
public <Type> Optional<Type> get(Class<Type> type) {
return getType(type);
}

private <Type> Optional<Type> getType(Class<Type> type) {
return Optional.ofNullable(providers.get(type)).map(provider -> (Type) provider.get());
}

将 getContext 方法的实现委托给上一步提取的 getType 方法

1
2
3
4
5
6
7
8
public Context getContext() {
return new Context() {
@Override
public <Type> Optional<Type> get(Class<Type> type) {
return getType(type);
}
};
}

inline 掉 getType 方法,这样 getContext 中的实现方法,就和 get 方法一致了

1
2
3
4
5
6
7
8
9
10
11
12
13
public Context getContext() {
return new Context() {
@Override
public <Type> Optional<Type> get(Class<Type> type) {
return Optional.ofNullable(providers.get(type)).map(provider -> (Type) provider.get());
}
};
}

@Override
public <Type> Optional<Type> get(Class<Type> type) {
return Optional.ofNullable(providers.get(type)).map(provider -> (Type) provider.get());
}

再将 get 方法的实现委给 getContext 方法:

1
2
3
4
@Override
public <Type> Optional<Type> get(Class<Type> type) {
return getContext().get(type);
}

接着可以移除掉 get 方法的 Override,也移除掉 ContextConfig 需要实现的接口:

image-20240810113656568

接着,inline 掉 get 方法,那么之前使用 get 方法的地方都变成了调用 getContext().get(type)

Snipaste_2024-08-10_11-39-45

那么,ContextConfig 中就只剩下两个 bind 方法,和 getContext 方法,这样就无法在修改上下文了。

这样,ContextConfig 就符合了我们将其用于配置上下文的要求。

这样就调整了它对外的接口,并将实现了 Config 和实际的 Context 容器的使用做了分离。

目前存在的问题

经过上面的重构,就可以在 getContext 来进行必要的检查,比如检查循环依赖、依赖是否有缺失,等等情况。

当前,在 Provider 内部需要为当前组件注入依赖时,都需要从容器中查找依赖的实例(获取容器的方式都是调用 getContext 方法),但是,现在 getContext 时都会创建一个新的 Context,这不符合实际使用的要求。

image-20240810115120039

但是当前并不能从 Provider 中获取到容器上下文的实例,并且也无法在创建 Provider 时传入容器实例(此时容器还未创建)

那么只能通过在调用 get 方法时,传入已经存在的 Context

但是我们现在实现的 Provider 是 jakarta.inject.Provider 其中的 get 方法是一个无参方法,无法满足我们的需求,那么我们就需要创建一个有参的函数式接口。

新建 Provider 接口

1
2
3
interface ComponentProvider<T> {
T get(Context context);
}

创建完之后,需要使用这个 ComponentProvider 来替换所有用到 Provider 的地方。

最直接的修改方式就是 人工手动的来做这个替换。

如果一定要按照严格的重构去做的话就需要平行的一步步替换,即先增加并逐步替换掉功能,再移除掉旧的功能。

使用重构式的方式可以保证在代码量比较大的时候仍然能使代码修改成功。

逐步重构

新建一个 componentProviders

1
2
private Map<Class<?>, Provider<?>> providers = new HashMap<>();
private Map<Class<?>, ComponentProvider<?>> componentProviders = new HashMap<>();

修改 bind 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
public <Type> void bind(Class<Type> type, Type instance) {
providers.put(type, () -> instance);
componentProviders.put(type, context -> instance);
}

public <Type, Implementation extends Type>
void bind(Class<Type> type, Class<Implementation> implementation) {
Constructor<Implementation> injectConstructor = getInjectConstructor(implementation);

providers.put(type, new ConstructorInjectionProvider(type, injectConstructor));
componentProviders.put(type, new ConstructorInjectionProvider(type, injectConstructor));
}

修改 ConstructorInjectionProvider 的实现,通过实现两个接口来实现后续的平行替换。

image-20240812095213493

将 get 方法的实现提取为函数 getT,并将里面的 getContext() 抽取为函数参数

使用 Ctrl + Alt + P 将 getContext() 提取为参数

那么 ComponentProvider 的实现如下:

1
2
3
4
@Override
public T get(Context context) {
return getT(context);
}

接着,将 getT 方法 inline 并将 Provider 的实现修改为委托给 ComponentProvider 的 get 方法:

ComponentProvider 的 get 方法 inline:

image-20240812100841607

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
public T get() {
return get(getContext());
}

@Override
public T get(Context context) {
if (constructing) throw new CyclicDependenciesException(componentType);
try {
constructing = true;
// 根据构造函数的参数,获取依赖的实例
Object[] dependencies = Arrays.stream(injectConstructor.getParameters())
.map(p -> {
Class<?> type = p.getType();
return context.get(type)
.orElseThrow(() -> new DependencyNotFoundException(
componentType, p.getType()));
})
.toArray(Object[]::new);
return injectConstructor.newInstance(dependencies);
} catch (CyclicDependenciesException e) {
Class<?>[] components = e.getComponents();
throw new CyclicDependenciesException(componentType, components);
} catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
} finally {
constructing = false;
}
}

inline getT 方法后,getT 方法就没有地方使用了,可以删除了

接着,移除掉所有使用 providers 的代码

1
private Map<Class<?>, Provider<?>> providers = new HashMap<>();

并将容器中的 Provider 改为 componentProviders,并在 get 时,传入当前的容器上下文 Context

Snipaste_2024-08-12_10-27-52

经过上面的重构,我们已经具备了在 bind 时检查依赖的能力。

这里还可以将 componProviders 重命名为 providers

在获取容器时检查依赖缺失的情况

目前对依赖缺失的检查是在 get 时进行的。

image-20240812104907259

构造测试

那么,需要在获取容器时时检查,只需要将 .get(Component.class) 方法移除就可以了

注意,这里可以同时修改传递性依赖缺失的测试用例

Snipaste_2024-08-12_11-40-11

移除后运行测试,测试不通过:

1
org.opentest4j.AssertionFailedError: Expected world.nobug.tdd.di.DependencyNotFoundException to be thrown, but nothing was thrown.

快速实现

需要在 getContext 时检查依赖:

image-20240812110730534

实现:在 bind 时同时记录注册的组件需要哪些依赖的类型,并在创建 Context 之前校验所有组件的依赖的类型是否都已经注册到容器中了

新建一个字段记录组件的依赖:

1
private Map<Class<?>, List<Class<?>>> dependencies = new HashMap<>();

在 bind 时记录组件需要的依赖的类型:

image-20240812111635721

在创建容器上下文之前,先检查所有注册的组件所需要的所有依赖是否都已经注册到容器中:

image-20240812112111509

1
2
3
4
5
6
for (Class<?> component : dependencies.keySet()) { // 遍历所有需要注册到容器中的组件
for (Class<?> dependency : dependencies.get(component)) { // 获取当前遍历的组件的所有依赖的类型,并遍历这些依赖的类型
if (!componentProviders.containsKey(dependency)) // 检查容器中是否已经注册了这些依赖的类型
throw new DependencyNotFoundException(component, dependency);
}
}

在没有循环依赖的情况下,直接检查依赖的类型是否注册到容器是有效的。

同理,对于间接的依赖缺失的测试,也需要修改

Snipaste_2024-08-12_11-40-11

移除.get(Component.class) ,运行测试,测试也会通过。

简单重构-简化命名

将 componentProviders 重命名为 providers

将 contextConfig 重命名为 config

在获取容器时检查循环依赖的情况

构造测试

同理这里也是需要移除.get(Component.class)

循环依赖的测试也有两个,一个是直接循环依赖,一个是间接/传递循环依赖,都需要修改测试

快速实现

这里的实现原理就是基于图算法:给定一个图的连接表,寻找图上是否存在环。

深度优先遍历,检查是否会重复回到某个节点。

1
2
3
4
5
6
7
8
9
10
// 深度优先遍历 检查 component 的依赖的访问记录
// visiting 保存正在被访问的记录,如果发现正在被访问的记录再次被访问,说明存在循环依赖
private void checkDependencies(Class<?> component, Stack<Class<?>> visiting) {
for (Class<?> dependency : dependencies.get(component)) {
if (visiting.contains(dependency)) throw new CyclicDependenciesException(visiting);
visiting.push(dependency);
checkDependencies(dependency, visiting);
visiting.pop();
}
}

getContext 时深度优先遍历,检查每一个组件的依赖链上是否有环

image-20240812140847037

运行测试,should_throw_exception_if_transitive_dependency_not_found 会有空指针异常,异常发生在

image-20240812142838411

因为在这个测试中,String 类型并没有注册到容器中,即没有执行 bind(String.class, "Hello")方法,所以递归到 dependencies.get(String.class) 时,会返回 null,就会引发空指针异常。

修改,提前判断依赖是否存在,如果不存在就不必再进行下一步的递归了,避免了空指针异常。

Snipaste_2024-08-12_14-42-35

那么以下的代码,就是重复了,可以移除了

image-20240812144651351

将 for 循环的代码改成 foreach 形式

Snipaste_2024-08-12_14-50-35

移除获取实例时(get时)往外抛异常的代码

因为在创建容器前就已经做了依赖相关的检查,所以就不需要在 ConstructorInjectionProvider 的 get 方法中再往外抛异常了

image-20240812145737955

注意,这里移除掉 orElseThrow 后需要调用 get方法,否在会报 IllegalArgumentException。

移掉掉 get 方法中的异常校验后,代码如下:

Snipaste_2024-08-12_15-02-46

接着,移除掉异常类中不在使用的构造方法:

image-20240812150633641

再移除些不在使用的代码:

Snipaste_2024-08-12_15-09-43

这个字段现在只在构造方法中使用,也可以移除掉。

重构-将 dependencies 移入 providers

目前,我们可以观察到在providers中添加数据时同步也会在dependencies中添加数据

你会发现 dependencies 和 providers 一直是伴生的,这实际上意味着,dependencies 是 providers 的额外信息。

这就是一个代码的坏味道,我们需要将其重构的更加高内聚,需要将 dependencies 的关系,放回到 providers 当中去。

Snipaste_2024-08-12_15-15-52

给 ComponentProvider 接口增加 getDependencies 方法:

1
2
3
4
5
interface ComponentProvider<T> {
T get(Context context);

List<Class<?>> getDependencies();
}

实现接口,绑定实例时,已经无法使用lambda表达式,应该使用匿名类

image-20240812153821742

修改 ConstructorInjectionProvider 中的 getDependencies 方法的实现,实现为:

Snipaste_2024-08-12_15-42-40 Snipaste_2024-08-12_15-43-10

通过 提取方法 + inline 的重构方式实现。

接着,需要将使用 dependencies 的地方,修改为通过 providers 来获取。

目前使用到 dependencies 的地方就是在创建容器前对依赖缺失、循环依赖的校验上。

可以观察到 providers 和 dependencies 的 key 是一样的,所以,所有对于 dependencies 的 key 访问都可以修改为对 providers 的 key 访问。

需要修改的代码如下,分别将其修改为对 providers 的调用

每改动一处跑一次测试,通过小步持续跑测试的方式是改进

image-20240812160514288

修改后:

Snipaste_2024-08-12_16-10-12

接着需要移除 dependencies,观察发现,目前 dependencies 只会用来保存数据,所以可以直接将其移除。

可以观察到 getInjectConstructor 除了用于构造 ConstructorInjectionProvider 之外,没有其他的用处。

那么,可以将这个方法,移动到 ConstructorInjectionProvider 里面去,然后在其构造函数中直接调用就好了,这也是一种让代码变得高内聚的方式。

使用 Move Members 的重构方式移动

image-20240812162507968

Snipaste_2024-08-12_16-26-15

接着会发现,对这个方法的调用,变成了如下形式:

Snipaste_2024-08-12_16-26-45

inline 一下会发现,new ConstructorInjectionProvider 时,调用了一个 ConstructorInjectionProvider 的静态方法,这也是一种很无聊的做法(坏味道)

image-20240812162800695

那么只需要将 ConstructorInjectionProvider 的构造方法修改为:

1
2
3
public ConstructorInjectionProvider(Class<T> component) {
this.injectConstructor = getInjectConstructor(component);
}

重构-减少ContextConfig的代码量

将 ConstructorInjectionProvider 从 ContextConfig 中移除形成一个新的单元(组件)

image-20240812164601445

image-20240812164702951

Snipaste_2024-08-12_16-51-08

Field Injection

如何构造测试

这节个人感觉比较重要的就是对于同样的功能,在不同上下文环境下对测试风格的选择方式问题。 在某些情况下,不同的风格传递的信息或者说知识是不太一样的。而伴随你不同风格的选择可能直接影响后续功能实现的难易程度。TDD主要的难点还是在于设计,在于你对知识的理解,究竟是以一种怎样的方式呈现出来。

对于重构过后的代码,如何以何种方式来完成测试?

根据不同环境下会有不同的考量,甚至在同样的功能中选择不同风格的测试

测试不仅仅是测试,测试中还蕴含着知识的传递

字段注入

  • 通过 Inject 标注将字段声明为依赖组件
  • 如果组件需要的依赖不存在,则抛出异常
  • 如果字段为 final 则抛出异常
  • 如果组件间存在循环依赖,则抛出异常
1
2
3
4
// TODO: field injection
// TODO: throw exception if dependency not found
// TODO: throw exception if filed is final
// TODO: throw exception if cyclic dependency

我们把 ConstructorInjectionProvider 从 ContextConfig 中分离出来,也可以说我们的架构改变了,原来我们可以说是一个单体的结构,没有组件和组件间的交互。

字段注入应该是有如下形式的类:

1
2
3
4
class ComponentWithFieldInjection {
@Inject
Dependency dependency;
}

即,这个类中包含一个被 @Inject 标注的字段。

如果还是按照之前的形式构造测试的话,我们会构造出如下测测试:

1
2
3
4
5
6
7
8
9
10
11
// TODO: field injection
@Test
public void should_inject_dependency_via_field() {
Dependency dependency = new Dependency() {
};
config.bind(Dependency.class, dependency);
config.bind(ComponentWithFieldInjection.class, ComponentWithFieldInjection.class);
ComponentWithFieldInjection component = config.getContext().get(ComponentWithFieldInjection.class).get();

assertSame(dependency, component.dependency);
}

但是在之前的课上也讲过,如果你的架构变了,那么你的任务也可以变。也可以用一些更小范围的测试去测。

所以,另外的写法呢就可以是如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void should_create_component_with_field_injection() {
Context context = Mockito.mock(Context.class);
Dependency dependency = Mockito.mock(Dependency.class);
Mockito.when(context.get(eq(Dependency.class)))
.thenReturn(Optional.of(dependency)); // Provider 内部需要使用context.get方法获取依赖

ConstructorInjectionProvider<ComponentWithFieldInjection> provider =
new ConstructorInjectionProvider<>(ComponentWithFieldInjection.class);
ComponentWithFieldInjection component = provider.get(context); // 会返回一个实例

assertSame(dependency, component.dependency);
}

所以,在重构过程中,随着架构的变化,你实现测试的选择也会有所不同。

对比以上两个测试,可以发现,第一个测试是一个更完整的、范围更大的端到端的功能测试,而第二个它更多的是集中在被我们抽离出来的单元本身,从某种意义上来讲,第二个测试更接近传统意义上的单元测试。

这两个测试并没有什么差别,只是选择的粒度和范围不同而已。也就是对功能上下文、功能点进行了进一步分解。

并不是功能架构拆分之后,就应该按照更小的粒度来做测试。

因为测试不仅仅是测试,其中还蕴含着功能上下文中的知识,如果使用测试替身的方式构造测试的话,就需要在测试中管理知识,有可能让新人不会很容易理解。

所以在构造测试方法时不仅包含测试的成本,也隐含着测试传递的知识。

再看看这两种测试策略在其他的功能上下文中有什么不一样。

1
2
3
4
5
6
7
// TODO: throw exception if dependency not found
@Test
public void should_throw_exception_if_filed_dependency_not_found() {
config.bind(ComponentWithFieldInjection.class, ComponentWithFieldInjection.class);

assertThrows(DependencyNotFoundException.class, () -> config.getContext());
}

如果缩小测试范围,仅针对单元本身做测试的话,测试应该会被构造成如下形式:

1
2
3
4
5
6
7
@Test
public void should_include_field_dependency_in_dependencies() {
ConstructorInjectionProvider<ComponentWithFieldInjection> provider =
new ConstructorInjectionProvider<>(ComponentWithFieldInjection.class);

assertArrayEquals(new Class<?>[]{Dependency.class}, provider.getDependencies().toArray(Class<?>[]::new));
}

为什么是构造成这种形式呢?

从源码我们可知,对依赖的检查,只需要从 provider 的 getDependencies 获取到其所需的所有的正确的依赖就可以进行正确的检查了。

后续的检查是在 getContext 时,即创建容器时(new 之前)做的,检查时需要获取到每一个组件的依赖。

所以只要 getDependencies 能返回正确的结果,那么就可以保证后续依赖缺失和循环依赖检查的代码能正确实现。

image-20240813102203192

但是目前这个方法只返回了构造函数参数所需的依赖,那么后续实现只需要在这个方法返回中增加 field 中所需的依赖即可。

image-20240813102811774

你会发现,实际上无论是 dependency not found 还是 循环依赖,实际上都是在 ContextConfig 中去实现的。

同理,循环依赖的情况:

1
2
3
4
5
6
7
8
9
10
11
12
// TODO: throw exception if cyclic dependency
class DependencyWithFieldInjection implements Dependency{
@Inject
ComponentWithFieldInjection component;
}
@Test
public void should_throw_exception_when_filed_has_cyclic_dependencies() {
config.bind(ComponentWithFieldInjection.class, ComponentWithFieldInjection.class);
config.bind(Dependency.class, DependencyWithFieldInjection.class);

assertThrows(CyclicDependenciesException.class, () -> config.getContext());
}

其实这里 DependencyWithFieldInjection 可以不必实现 Dependency

如果减小测试的粒度,我们会发现循环依赖的测试,其实和依赖不存在的测试是一样的:

因为检查依赖不存在和检查循环依赖都是在 checkDependencies 方法中做的

1
2
3
4
5
6
7
@Test
public void should_include_field_dependency_in_dependencies_() {
ConstructorInjectionProvider<ComponentWithFieldInjection> provider =
new ConstructorInjectionProvider<>(ComponentWithFieldInjection.class);

assertArrayEquals(new Class<?>[]{Dependency.class}, provider.getDependencies().toArray(Class<?>[]::new));
}

所以对TODO: throw exception if dependency not foundTODO: throw exception if cyclic dependency 的测试任务可以合并为一个任务,比如:TODO: provide dependencies information for field injection,即:依赖中应包含 Inject Field 声明的依赖

1
2
3
4
5
6
7
8
// TODO: provide dependencies information for field injection
@Test
public void should_include_field_dependency_in_dependencies() {
ConstructorInjectionProvider<ComponentWithFieldInjection> provider =
new ConstructorInjectionProvider<>(ComponentWithFieldInjection.class);

assertArrayEquals(new Class<?>[]{Dependency.class}, provider.getDependencies().toArray(Class<?>[]::new));
}

经过取舍,我们选择保留以下的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ComponentWithFieldInjection {
@Inject
Dependency dependency;
}
// TODO: field injection
@Test
public void should_inject_dependency_via_field() {
Dependency dependency = new Dependency() {
};
config.bind(Dependency.class, dependency);
config.bind(ComponentWithFieldInjection.class, ComponentWithFieldInjection.class);
ComponentWithFieldInjection component = config.getContext().get(ComponentWithFieldInjection.class).get();

assertSame(dependency, component.dependency);
}

// TODO: provide dependencies information for field injection
@Test
public void should_include_field_dependency_in_dependencies() {
ConstructorInjectionProvider<ComponentWithFieldInjection> provider =
new ConstructorInjectionProvider<>(ComponentWithFieldInjection.class);

assertArrayEquals(new Class<?>[]{Dependency.class}, provider.getDependencies().toArray(Class<?>[]::new));
}

实现 should_inject_dependency_via_field

1
2
3
4
5
6
7
8
9
10
11
// TODO: field injection
@Test
public void should_inject_dependency_via_field() {
Dependency dependency = new Dependency() {
};
config.bind(Dependency.class, dependency);
config.bind(ComponentWithFieldInjection.class, ComponentWithFieldInjection.class);
ComponentWithFieldInjection component = config.getContext().get(ComponentWithFieldInjection.class).get();

assertSame(dependency, component.dependency);
}

运行测试,会有因为无法获取到构造函数(NoSuchMethodException)而抛出 IllegalComponentException 异常

Snipaste_2024-08-13_11-02-35

首先需要将 ComponentWithFieldInjection 修改为静态内部类

1
2
3
4
static class ComponentWithFieldInjection {
@Inject
Dependency dependency;
}

这里也可以将这个类定义为最顶层的类,就和前面构造的 Component 、Dependency 一样,那么修改的代码就要少一点。

并且需要使用 getDeclaredConstructor 来获取构造函数。

1
return implementation.getDeclaredConstructor();

ComponentWithFieldInjection 是一个非静态内部类(即它是在另一个类的内部定义的类,并且不带有 static 关键字),它的构造函数需要调用外部类的构造函数。如果是静态内部类,则不需要这样做。

对于非静态内部类(即没有使用 static 关键字修饰的内部类),其构造函数实际上是私有的,并且会附加一个对外部类实例的引用。这意味着,即使您没有显式定义构造函数,编译器也会为您生成一个私有的构造函数,该构造函数接受一个外部类的实例作为参数。

假设您有一个外部类 OuterClass 和一个非静态内部类 ComponentWithFieldInjection,如下所示:

1
2
3
4
5
6
7
public class OuterClass {
private Dependency dependency;

public class ComponentWithFieldInjection {
private Dependency dependency;
}
}

在这种情况下,您不能直接通过 getConstructor 获取到 ComponentWithFieldInjection 的构造函数,因为该构造函数是私有的,并且它实际上接受一个 OuterClass 实例作为参数。

为了获取非静态内部类的构造函数,您需要使用 getDeclaredConstructor 并且指定参数类型,如下所示:

1
2
3
4
5
6
try {
Constructor<ComponentWithFieldInjection> constructor =
OuterClass.class.getDeclaredConstructor(OuterClass.class);
} catch (NoSuchMethodException e) {
// 处理异常
}

请注意,上述代码中的 getDeclaredConstructor 要求您知道构造函数的确切签名。由于构造函数是私有的,您还需要调用 setAccessible(true) 来允许访问它:

1
constructor.setAccessible(true);

但是,通常情况下,您并不需要直接通过反射来创建非静态内部类的实例。通常的做法是通过外部类的实例来创建内部类的实例。例如:

1
2
OuterClass outerInstance = new OuterClass();
ComponentWithFieldInjection component = outerInstance.new ComponentWithFieldInjection();

总结一下,对于非静态内部类,您不能直接使用 getConstructor 来获取其构造函数,而需要使用 getDeclaredConstructor 并且可能需要调用 setAccessible(true) 来访问私有构造函数。

在Java中,getDeclaredConstructorgetConstructor都是用于通过反射来获取类的构造函数的方法,但它们之间存在一些重要的区别:

  1. getConstructor:

    • 这个方法是从Class类继承来的,用于获取公开的构造函数。
    • 它可以获取到类的公共构造函数,包括从父类继承来的构造函数。
    • 如果类中有多个公共构造函数,你可以通过指定参数类型数组来获取特定的构造函数。
    • 如果类没有公共构造函数或者没有匹配给定参数类型的构造函数,这个方法会抛出NoSuchMethodException
  2. getDeclaredConstructor:

    • 这个方法也是从Class类继承来的,但它用于获取类中声明的构造函数,无论这些构造函数是否是公共的。
    • 它可以获取到类本身的构造函数,包括私有的、受保护的、包私有的以及公共的构造函数,但不会获取从父类继承的构造函数。
    • 同样地,你可以通过指定参数类型数组来获取特定的构造函数。
    • 如果类中没有声明匹配给定参数类型的构造函数,这个方法会抛出NoSuchMethodException

示例

假设我们有一个类MyClass:

1
2
3
4
public class MyClass {
private MyClass() { } // 私有构造函数
public MyClass(String name) { } // 公共构造函数
}

我们可以这样使用这两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try {
Class<MyClass> clazz = MyClass.class;

// 获取公共构造函数
Constructor<MyClass> publicConstructor = clazz.getConstructor(String.class);

// 获取声明的构造函数(包括私有的)
Constructor<MyClass> declaredPrivateConstructor = clazz.getDeclaredConstructor();

// 访问私有构造函数
declaredPrivateConstructor.setAccessible(true);
MyClass instance = declaredPrivateConstructor.newInstance();

} catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
e.printStackTrace();
}

总结

  • getConstructor 主要用于获取类的公共构造函数。
  • getDeclaredConstructor 主要用于获取类中声明的所有构造函数,包括非公共的构造函数。

如果你想要获取一个类中的所有构造函数,不论它们的访问级别如何,应该使用getDeclaredConstructors方法,而不是单个构造函数的版本。

获取到构造函数之后,就可以创建实例,因为依赖是字段不是构造函数的参数,所以还需要知道有哪些被 @Inject 标注的字段

Snipaste_2024-08-13_11-40-48

获取到字段,并创建好实例后就需要给字段赋值:

image-20240813114315788

即,根据依赖的字段的类型,从容器中获取该类型的实例,并赋值到该字段中。

运行测试,测试通过。

实现 provide dependencies information for field injection

1
2
3
4
5
6
7
8
// TODO: provide dependencies information for field injection
@Test
public void should_include_field_dependency_in_dependencies() {
ConstructorInjectionProvider<ComponentWithFieldInjection> provider =
new ConstructorInjectionProvider<>(ComponentWithFieldInjection.class);

assertArrayEquals(new Class<?>[]{Dependency.class}, provider.getDependencies().toArray(Class<?>[]::new));
}

只需要在 getDependencies 方法的返回结果中增加字段注入的依赖:

1
2
3
4
5
6
@Override
public List<Class<?>> getDependencies() {
Stream<? extends Class<?>> b = injectFields.stream().map(Field::getType);
Stream<? extends Class<?>> a = Arrays.stream(injectConstructor.getParameters()).map(Parameter::getType);
return Stream.concat(a, b).collect(Collectors.toList());
}

对 Subclass 的支持

新建一个子类:

1
2
static class SubclassWithFieldInjection extends ComponentWithFieldInjection {
}

构造测试

1
2
3
4
5
6
7
8
9
10
@Test
public void should_inject_dependency_via_superclass_inject_filed() {
Dependency dependency = new Dependency() {
};
config.bind(Dependency.class, dependency);
config.bind(SubclassWithFieldInjection.class, SubclassWithFieldInjection.class);
SubclassWithFieldInjection component = config.getContext().get(SubclassWithFieldInjection.class).get();

assertSame(dependency, component.dependency);
}

运行测试:

1
2
Expected :world.nobug.tdd.di.ContainerTest$ComponentConstruction$FieldInjection$2@479cbee5
Actual :null

这是因为,当前在取注入的字段时,只取了当前类的字段:

1
2
3
4
private static <T> List<Field> getInjectFields(Class<T> component) {
return Arrays.stream(component.getDeclaredFields())
.filter(f -> f.isAnnotationPresent(Inject.class)).toList();
}

实际上,还需要获取到父类的字段,可以通过递归的方式,找到父类的所有的注入字段:

1
2
3
4
5
6
7
8
9
10
private static <T> List<Field> getInjectFields(Class<T> component) {
List<Field> injectFields = new ArrayList<>();
Class<?> current = component;
while (current != Object.class) {
injectFields.addAll(Arrays.stream(current.getDeclaredFields())
.filter(f -> f.isAnnotationPresent(Inject.class)).toList());
current = current.getSuperclass();
}
return injectFields;
}

Method Injection

方法注入

  • 通过 Inject 标注的方法,其参数为依赖组件

  • 通过 Inject 标注的无参数方法,会被调用

  • 按照子类中的规则,覆盖父类中的 Inject 方法

  • 如果组件需要的依赖不存在,则抛出异常

  • 如果方法定义类型参数,则抛出异常

  • 如果组件间存在循环依赖,则抛出异常

方法测试的任务列表:

1
2
3
4
5
// TODO inject method with no dependencies will be called
// TODO inject method with dependencies will be injected
// TODO override inject method from superclass
// TODO include dependencies from inject methods
// TODO throw exception if type parameter defined

无参方法注入

定义一个带有无参方法注入的类

1
2
3
4
5
6
7
8
static class InjectMethodWithNoDependencies {
boolean called = false; // 用于验证方法是否被调用

@Inject
void install() {
called = true;
}
}

定义测试:

1
2
3
4
5
6
7
8
// TODO: inject method with no dependencies will be called
@Test
public void should_call_inject_method_with_no_dependencies() {
config.bind(InjectMethodWithNoDependencies.class, InjectMethodWithNoDependencies.class);
InjectMethodWithNoDependencies instance = config.getContext().get(InjectMethodWithNoDependencies.class).get();

assertTrue(instance.called);
}

实现,同理先找到并记录所有被 @Inject 标注的方法

Snipaste_2024-08-13_14-51-24

获取实例时,调用这些方法注入依赖:

image-20240813151931496

有参方法注入

新建有参方法注入类:

1
2
3
4
5
6
7
8
static class InjectMethodWithDependencies {
Dependency dependency;

@Inject
void install(Dependency dependency) {
this.dependency = dependency;
}
}

构造测试

1
2
3
4
5
6
7
8
9
10
11
// TODO: inject method with dependencies will be injected
@Test
public void should_call_inject_method_with_dependencies() {
Dependency dependency = new Dependency() {
};
config.bind(Dependency.class, dependency);
config.bind(InjectMethodWithDependencies.class, InjectMethodWithDependencies.class);
InjectMethodWithDependencies instance = config.getContext().get(InjectMethodWithDependencies.class).get();

assertSame(dependency, instance.dependency);
}

运行测试,直接通过,说明不需要修改生产代码。

对依赖的检查

检查依赖是否存在、是否存在循环依赖

构造测试,减小测试的粒度,直接对 ConstructorInjectionProvider 进行测试:

1
2
3
4
5
6
7
8
// TODO: include dependencies from inject methods
@Test
public void should_include_method_dependency_in_dependencies() {
ConstructorInjectionProvider<InjectMethodWithDependencies> provider =
new ConstructorInjectionProvider<>(InjectMethodWithDependencies.class);

assertArrayEquals(new Class<?>[]{Dependency.class}, provider.getDependencies().toArray(Class<?>[]::new));
}

同理需要在 getDependencies 方法中增加返回方法注入点所需的依赖参数。

1
2
3
4
5
6
7
8
@Override
public List<Class<?>> getDependencies() {
Stream<? extends Class<?>> b = injectFields.stream().map(Field::getType);
Stream<? extends Class<?>> a = Arrays.stream(injectConstructor.getParameters()).map(Parameter::getType);
Stream<Class<?>> c = injectMethods.stream().flatMap(m -> Arrays.stream(m.getParameterTypes()));
Stream<Class<?>> concat = Stream.concat(a, b);
return Stream.concat(concat, c).collect(Collectors.toList());
}

使用 flatMap 的原因:

由于 getParameterTypes() 返回的是一个 Class<?>[] 数组,每次调用都会返回多个元素,而不是单个元素。因此,如果你直接使用 map 操作来转换这些数组,你将得到一个由多个 Class<?>[] 组成的流,而不是一个扁平化的流,其中包含所有的 Class<?> 对象。

为了解决这个问题,你需要使用 flatMap 操作来“展平”这些数组,将它们合并成一个单一的流,这样你就可以继续对这个流进行操作,比如收集结果到一个列表中。

父类和子类的方法注入

方法注入的难点,主要是在子类和父类之间方法注入的调用关系

子类注册时,需要调用父类的注入点方法

新建父子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TODO: override inject method from superclass
static class SuperClassWithInjectMethod {
boolean superCalled = false;
@Inject
void install() {
superCalled = true;
}
}
static class SubclassWithInjectMethod extends SuperClassWithInjectMethod {
boolean subCalled = false;
@Inject
void installAnother() {
subCalled = true;
}
}

构造测试:

1
2
3
4
5
6
7
8
@Test
public void should_inject_dependencies_via_inject_method_from_superclass() {
config.bind(SubclassWithInjectMethod.class, SubclassWithInjectMethod.class);
SubclassWithInjectMethod instance = config.getContext().get(SubclassWithInjectMethod.class).get();

assertTrue(instance.superCalled);
assertTrue(instance.subCalled);
}

运行测试,测试不通过,父类的注入方法不会被调用,superCalled 为 false:

1
2
Expected :true
Actual :false

原因是,获取方法注入点时,只获取了当前类的方法:

1
2
3
4
5
private List<Method> getInjectMethods(Class<T> component) {
return Arrays.stream(component.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(Inject.class))
.toList();
}

类似于获取父类字段注入点的逻辑,通过递归找到父类注入方法的形式获取:

1
2
3
4
5
6
7
8
9
10
11
private List<Method> getInjectMethods(Class<T> component) {
Class<T> current = component;
List<Method> injectMethods = new ArrayList<>();
while (current != Object.class) {
injectMethods.addAll(Arrays.stream(current.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(Inject.class))
.toList());
current = (Class<T>) current.getSuperclass();
}
return injectMethods;
}

子类注册时,先调用父类的注入点方法

在注册子类时,不仅要调用父类的注入点方法,而且需要让父类的注入点方法优先于子类的注入点方法调用

修改父子类的内部状态,通过数值确定调用的顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TODO: override inject method from superclass
static class SuperClassWithInjectMethod {
int superCalled = 0;
@Inject
void install() {
superCalled = 1;
}
}
static class SubclassWithInjectMethod extends SuperClassWithInjectMethod {
int subCalled = 0;
@Inject
void installAnother() {
subCalled = superCalled + 1;
}
}

修改测试:

1
2
3
4
5
6
7
8
@Test
public void should_inject_dependencies_via_inject_method_from_superclass() {
config.bind(SubclassWithInjectMethod.class, SubclassWithInjectMethod.class);
SubclassWithInjectMethod instance = config.getContext().get(SubclassWithInjectMethod.class).get();

assertEquals(1, instance.superCalled);
assertEquals(2, instance.subCalled);
}

运行测试,测试不通过,说明子类先于父类执行注入点方法。

因为,子类的注入点方法是先于父类的注入点方法加入到方法列表的,这里最简单的实现就是将方法列表 reverse 倒序一下:

1
2
3
4
5
6
7
8
9
10
11
12
private List<Method> getInjectMethods(Class<T> component) {
Class<T> current = component;
List<Method> injectMethods = new ArrayList<>();
while (current != Object.class) {
injectMethods.addAll(Arrays.stream(current.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(Inject.class))
.toList());
current = (Class<T>) current.getSuperclass();
}
Collections.reverse(injectMethods);
return injectMethods;
}

Override 注入点方法的情况

@Inject 的描述中,关于方法的注入有如下的限制:

A method annotated with @Inject that overrides another method annotated with @Inject will only be injected once per injection request per instance. A method with no @Inject annotation that overrides a method annotated with @Inject will not be injected.

被 @Inject 注解的方法如果覆盖了另一个同样被 @Inject 注解的方法,则在每次实例的注入请求中只会被注入一次。没有 @Inject 注解的方法如果覆盖了一个被 @Inject 注解的方法,则不会被注入。

说明:这里的调用一次是指只调用子类中的方法,不会调用父类的方法。

子类覆盖的方法被 Inject 标注

修改测试的父类:

更好的方法是子类和父类设置不同的值,使用++递增的话,不能很明确的知道是应该调用哪个方法。这里应该调用的是子类中的方法。

1
2
3
4
5
6
7
8
// TODO: override inject method from superclass
static class SuperClassWithInjectMethod {
int superCalled = 0;
@Inject
void install() {
superCalled++;
}
}

构造测试,验证父类的注入点方法被带有Inject方法覆盖时,父类的注入点方法不会被调用,只会调用一次子类的调用点方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
static class SubclassWithOverrideInjectMethod extends SuperClassWithInjectMethod {
@Inject
void install() {
super.install();
}
}
@Test
public void should_only_call_once_if_subclass_override_superclass_inject_method_with_inject() {
config.bind(SubclassWithOverrideInjectMethod.class, SubclassWithOverrideInjectMethod.class);
SubclassWithOverrideInjectMethod instance = config.getContext().get(SubclassWithOverrideInjectMethod.class).get();

assertEquals(1, instance.superCalled);
}

实现,在获取注入点方法时,需要判断如果父类的注入点方法被覆盖,那么可以将其过滤掉,即可以不用被调用。

因为这里是先找到子类的方法,所以可以从目前找到的方法中再判断是否有和当前类(父类)同名、同签名的方法(即覆盖的方法),有则过滤掉。

image-20240813170732077

子类覆盖的方法未被 Inject 标注

这里子类和父类的方法应该要都不会被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static class SuperClassWithInjectMethod {
int superCalled = 0;
@Inject
void install() {
superCalled++;
}
}
static class SubclassWithOverrideInjectMethodWithoutInject extends SuperClassWithInjectMethod {
void install() {
super.install();
}
}
@Test
public void should_only_call_once_if_subclass_override_superclass_inject_method_without_inject() {
config.bind(SubclassWithOverrideInjectMethodWithoutInject.class, SubclassWithOverrideInjectMethodWithoutInject.class);
SubclassWithOverrideInjectMethodWithoutInject instance = config.getContext().get(SubclassWithOverrideInjectMethodWithoutInject.class).get();

assertEquals(0, instance.superCalled);
}

测试不通过,所以就是有一个被 Inject 标注的方法被加入到了注入点列表中。

子类的方法中需要调用 super.install(); 应该是因为该方法被覆盖,调用时执行的是子类的方法。

所以要解决这个问题,还是要避免将父类的被 Inject 标注的方法加入的注入点方法列表。

实现:

image-20240813175302973

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private List<Method> getInjectMethods(Class<T> component) {
Class<T> current = component;
List<Method> injectMethods = new ArrayList<>();
while (current != Object.class) {
injectMethods.addAll(Arrays.stream(current.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(Inject.class))
.filter(m -> injectMethods.stream().noneMatch(im -> im.getName().equals(m.getName()) &&
Arrays.equals(im.getParameterTypes(), m.getParameterTypes())))
.filter(m -> Arrays.stream(component.getDeclaredMethods())
.filter(m1 -> !m1.isAnnotationPresent(Inject.class))
.noneMatch(m1 -> m1.getName().equals(m.getName()) &&
Arrays.equals(m1.getParameterTypes(), m.getParameterTypes())))
.toList());
current = (Class<T>) current.getSuperclass();
}
Collections.reverse(injectMethods);
return injectMethods;
}

Sad Path

目前,还剩下几个 sad path,完成这几个 sad path 那么注入部分就大体上完成了。

  • 如果注册的组件不可实例化,则抛出异常

    • 抽象类

    • 接口

1
2
// TODO: abstract class
// TODO: interface
  • 字段注入
    • 如果字段为 final 则抛出异常
1
// TODO: throw exception if filed is final
  • 方法注入
    • 如果方法定义类型参数,则抛出异常
1
2
3
4
5
// TODO: throw exception if type parameter defined

/**
在 Inject 的注释中有一个 do not declare type parameters of their own. 的注释,即不要在方法上声明类型参数
*/

在Java中,类型参数(Type Parameter)是泛型编程的基础概念之一。它允许你在定义类或方法时使用一种占位类型的机制,这种占位类型可以在使用这些类或方法时具体化为实际的类型。类型参数通常用于实现泛型类、接口或方法,以便它们可以处理多种数据类型而不需要为每种类型重复代码。

类型参数通常用一个大写字母表示,如 E, T, K 等,但也可以使用任何有效的标识符。例如,在定义一个泛型类时,你可以这样写:

1
2
3
4
5
6
7
8
9
10
11
public class Box<T> {
private T item;

public void set(T item) {
this.item = item;
}

public T get() {
return item;
}
}

在这个例子中,T 就是一个类型参数。当你创建 Box 类的实例时,你需要指定 T 的实际类型,例如:

1
2
Box<String> stringBox = new Box<>(); // T is String
Box<Integer> intBox = new Box<>(); // T is Integer

这里,T 分别被具体化为 StringInteger 类型。

类型参数还可以有边界限制,这意味着你可以指定一个类型参数必须是某个特定类的子类或者实现某个特定接口。例如:

1
2
3
public class Box<T extends Comparable<T>> {
// ...
}

这表示 T 必须实现 Comparable 接口。这样,你就可以在类内部安全地调用 TcompareTo 方法。

注册抽象类

创建抽象类:

这里使用构造器注入,其实这里不用实现 Component 也是可以的

1
2
3
4
5
abstract class AbstractComponent implements Component{
@Inject
public AbstractComponent() {
}
}

构造测试:

1
2
3
4
@Test
public void should_throw_exception_if_component_is_abstract() {
assertThrows(IllegalComponentException.class, () -> new ConstructorInjectionProvider<>(AbstractComponent.class));
}

实现,创建 ConstructorInjectionProvider 时校验其是否为抽象类

1
2
3
4
5
6
7
public ConstructorInjectionProvider(Class<T> component) {
if (Modifier.isAbstract(component.getModifiers())) throw new IllegalComponentException();

this.injectConstructor = getInjectConstructor(component);
this.injectFields = getInjectFields(component);
this.injectMethods = getInjectMethods(component);
}

注册接口

不需要创建新类,直接可以注册 Component 接口

构造测试:

1
2
3
4
5
// TODO: interface
@Test
public void should_throw_exception_if_component_is_interface() {
assertThrows(IllegalComponentException.class, () -> new ConstructorInjectionProvider<>(Component.class));
}

运行测试,直接通过,因为接口的 Modifier 本身就是抽象的。

字段注入时,字段为 final 时

新建测试类:

1
2
3
4
static class FinalInjectField {
@Inject
final Dependency dependency = null;
}

新建测试:

1
2
3
4
@Test
public void should_throw_exception_if_field_is_final() {
assertThrows(IllegalComponentException.class, () -> new ConstructorInjectionProvider<>(FinalInjectField.class));
}

实现:

可以在 getInjectFields 中检查并抛出异常,也可以在 ConstructorInjectionProvider 构造函数中检查并抛出异常,这里是在构造函数中检查。

1
2
3
4
5
6
7
8
9
public ConstructorInjectionProvider(Class<T> component) {
if (Modifier.isAbstract(component.getModifiers())) throw new IllegalComponentException();

this.injectConstructor = getInjectConstructor(component);
this.injectFields = getInjectFields(component);
this.injectMethods = getInjectMethods(component);

if (injectFields.stream().anyMatch(f -> Modifier.isFinal(f.getModifiers()))) throw new IllegalComponentException();
}

方法定义类型参数

新建类

1
2
3
4
5
static class InjectMethodWithTypeParameter {
@Inject
<T> void install() {
}
}

构造测试:

1
2
3
4
@Test
public void should_throw_exception_if_method_has_type_parameter() {
assertThrows(IllegalComponentException.class, () -> new ConstructorInjectionProvider<>(InjectMethodWithTypeParameter.class));
}

实现:

image-20240814102238573

重构测试代码

目前,我们的测试是按照构造器注入、字段注入、方法注入的方式组织的。但是我们的生产代码的架构是有调整的。

这就造成了生产代码和测试之间存在一些不一致的情况。

也造成了随着TDD的进行,我们前后实现类似功能的测试会因为重构导致的生产代码的变化而变化,比如,在构造器注入中我们对依赖的检查是完整的端到端的功能测试,而经过重构后,我们后面的字段注入和方法注入都是更细粒度的测试。

我们通过TDD的测试,可以还原整个开发流程,但是从结果上看,这并不意味着我们得到了最好的一整套测试用例。

所以我们就需要使用重构的方式,对我们的测试代码进行重构,以得到更好的组织形式的测试。

这才能保证,我们在TDD之后我们能得到结构优秀的代码,同时在测试中真实反映代码的意图,而不仅仅是单纯的展现我们实现功能的过程。

代码不仅仅是资产,也是负债。需要持续不断的维护。

删除不必要的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// dependencies not exist
@Test
public void should_throw_exception_if_dependency_not_found() {
config.bind(Component.class, ComponentWithInjectConstructor.class);

DependencyNotFoundException exception = assertThrows(DependencyNotFoundException.class, () -> {
config.getContext();
});

assertEquals(Dependency.class, exception.getDependency());
assertEquals(Component.class, exception.getComponent());
}
@Test
public void should_throw_exception_if_transitive_dependency_not_found() {
config.bind(Component.class, ComponentWithInjectConstructor.class);
config.bind(Dependency.class, DependencyWithInjectConstructor.class); // 缺失 String 类型的依赖

DependencyNotFoundException exception = assertThrows(DependencyNotFoundException.class, () -> {
config.getContext();
});

assertEquals(String.class, exception.getDependency());
assertEquals(Dependency.class, exception.getComponent());
}

对于这两个依赖不存在的测试,后一个测试是没有必要的,因为在我们的生产代码进行重构后,对依赖的检查不在要求?????

移动部分测试到新的测试上下文

对于一下三个对依赖进行检查的测试,目前在 ConstructorInjection 上下文中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// dependencies not exist
@Test
public void should_throw_exception_if_dependency_not_found() {
config.bind(Component.class, ComponentWithInjectConstructor.class);

DependencyNotFoundException exception = assertThrows(DependencyNotFoundException.class, () -> {
config.getContext();
});

assertEquals(Dependency.class, exception.getDependency());
assertEquals(Component.class, exception.getComponent());
}


// cyclic dependencies
@Test // A -> B -> A
public void should_throw_exception_if_cyclic_dependencies() {
config.bind(Component.class, ComponentWithInjectConstructor.class);
config.bind(Dependency.class, DependencyDependedOnComponent.class);

CyclicDependenciesException exception =
assertThrows(CyclicDependenciesException.class, () -> config.getContext());

Set<Class<?>> classes = Sets.newSet(exception.getComponents());

assertEquals(2, classes.size());
assertTrue(classes.contains(Component.class));
assertTrue(classes.contains(Dependency.class));
}
@Test // A -> B -> C -> A
public void should_throw_exception_if_transitive_cyclic_dependencies() {
config.bind(Component.class, ComponentWithInjectConstructor.class);
config.bind(Dependency.class, DependencyDependedOnAnotherDependency.class);
config.bind(AnotherDependency.class, AnotherDependencyDependedOnComponent.class);

CyclicDependenciesException exception =
assertThrows(CyclicDependenciesException.class, () -> config.getContext());

List<Class<?>> components = Arrays.stream(exception.getComponents()).toList();

assertEquals(3, components.size());
assertTrue(components.contains(Component.class));
assertTrue(components.contains(Dependency.class));
assertTrue(components.contains(AnotherDependency.class));
}

可以将这三个测试移动到一个新的上下文中,这个上下文要体现出我们的意图,这里我们定义一个名为 DependencyCheck 的测试上下文。

1
2
3
4
@Nested
public class DependencyCheck {

}

将前文提到的三个测试移动到这个上下文中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Nested
public class DependencyCheck {

// dependencies not exist
@Test
public void should_throw_exception_if_dependency_not_found() {
config.bind(Component.class, ComponentWithInjectConstructor.class);

DependencyNotFoundException exception = assertThrows(DependencyNotFoundException.class, () -> {
config.getContext();
});

assertEquals(Dependency.class, exception.getDependency());
assertEquals(Component.class, exception.getComponent());
}


// cyclic dependencies
@Test // A -> B -> A
public void should_throw_exception_if_cyclic_dependencies() {
config.bind(Component.class, ComponentWithInjectConstructor.class);
config.bind(Dependency.class, DependencyDependedOnComponent.class);

CyclicDependenciesException exception =
assertThrows(CyclicDependenciesException.class, () -> config.getContext());

Set<Class<?>> classes = Sets.newSet(exception.getComponents());

assertEquals(2, classes.size());
assertTrue(classes.contains(Component.class));
assertTrue(classes.contains(Dependency.class));
}
@Test // A -> B -> C -> A
public void should_throw_exception_if_transitive_cyclic_dependencies() {
config.bind(Component.class, ComponentWithInjectConstructor.class);
config.bind(Dependency.class, DependencyDependedOnAnotherDependency.class);
config.bind(AnotherDependency.class, AnotherDependencyDependedOnComponent.class);

CyclicDependenciesException exception =
assertThrows(CyclicDependenciesException.class, () -> config.getContext());

List<Class<?>> components = Arrays.stream(exception.getComponents()).toList();

assertEquals(3, components.size());
assertTrue(components.contains(Component.class));
assertTrue(components.contains(Dependency.class));
assertTrue(components.contains(AnotherDependency.class));
}

}

重构 ConstructorInjection 上下文

以下的这两个方法,与当前的架构不一致,

image-20240814114238683

抽取出 getBind 方法,修改这两个方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// sad path
// multi inject constructors
@Test
public void should_throw_exception_if_multi_inject_constructors_provided() {
assertThrows(IllegalComponentException.class, () -> {
getBind(ComponentWithMultiInjectConstructors.class);
});
}

private void getBind(Class<? extends Component> implementation) {
config.bind(Component.class, implementation);
}

// no default constructor and inject constructor
@Test
public void should_throw_exception_if_no_inject_constructor_nor_default_constructor_provided() {
assertThrows(IllegalComponentException.class, () -> {
getBind(ComponentWithNoInjectConstructorNorDefaultConstructor.class);
});
}

将 getBind的实现修改为:

1
2
3
private void getBind(Class<? extends Component> implementation) {
new ConstructorInjectionProvider<>(implementation);
}

inline getBind 方法

inline 后需要稍微调整一下,比如移除不必要的型转

1
2
3
4
5
6
7
8
9
10
11
12
// sad path
// multi inject constructors
@Test
public void should_throw_exception_if_multi_inject_constructors_provided() {
assertThrows(IllegalComponentException.class, () -> new ConstructorInjectionProvider<>(ComponentWithMultiInjectConstructors.class));
}

// no default constructor and inject constructor
@Test
public void should_throw_exception_if_no_inject_constructor_nor_default_constructor_provided() {
assertThrows(IllegalComponentException.class, () -> new ConstructorInjectionProvider<>(ComponentWithNoInjectConstructorNorDefaultConstructor.class));
}

修改之后,这几个测试就有了一样的结构:

image-20240814115236417

在 ConstructorInjection 中增加对依赖校验的测试

增加这个测试为了统一与FieldInjection、MethodInjection测试上下文的测试组织形式,使得这几个测试上下文都保持一个一致的结构。

1
2
3
4
5
6
7
@Test
public void should_include_dependency_from_inject_constructor() {
ConstructorInjectionProvider<ComponentWithInjectConstructor> provider =
new ConstructorInjectionProvider<>(ComponentWithInjectConstructor.class);

assertArrayEquals(new Class<?>[]{Dependency.class}, provider.getDependencies().toArray(Class<?>[]::new));
}

创建 InjectionTest 并移出 ContainerTest

将关于注入的三个测试上下文,放入一个 InjectionTest 上下文中,用于后续将其移出 ContainerTest,以形成一个独立的测试类,减少ContainerTest 类中的代码数量,便于理解。

Snipaste_2024-08-14_12-03-14

因为当前上下文中,大量依赖 config,所以也需要将 setUp的代码移入 InjectionTest

接着需要将 InjectionTest 移出 ContainerTest,这里对 InjectionTest 执行两次 Move Inner Class to Upper Level 重构,就可以将其从 ContainerTest 中移出:

image-20240814134740365

最终移出到测试目录的最顶层:

image-20240814135151138

重构 InjectionTest

目前在 InjectionTest 的测试上下文中,存在不同的测试粒度

image-20240814135657701

在同一个测试上下文中,我们最好希望它们的测试粒度是一样的。

这里,我们就希望将对 config 粒度的功能测试,都能重构为对 ConstructorInjectionProvider 粒度的单元测试。

观察:

image-20240814151410275

对于 config 的测试都需要执行 config.bind(XXX) + config.getContext().get(XXX).get() 方法来获取一个组件,所以这里重构方向就是将 config.getContext().get(XXX).get() 方法,修改为通过创建 ConstructorInjectionProvider 的方式来获取一个组件。

先将 config.bind(XXX) + config.getContext().get(XXX).get() 提取为一个通用的方法。

第一步,因为需要支持很多类型,这里先提取参数:

Snipaste_2024-08-14_15-26-55

再提取方法:

Snipaste_2024-08-14_15-28-06

1
2
3
4
5
private Component getComponent(Class<Component> type, Class<ComponentWithDefaultConstructor> implementation) {
config.bind(type, implementation);
Component instance = config.getContext().get(type).get();
return instance;
}

提取的方法的签名无法支持我们其他代码的类型,需要做一些范型调整:

1
2
3
4
5
private <T, I extends T> T getComponent(Class<T> type, Class<I> implementation) {
config.bind(type, implementation);
T instance = config.getContext().get(type).get();
return instance;
}

将提取的参数先 inline 回去:

image-20240814153437033

inline 后,这段代码就会变成如下形式:

image-20240814153602000

观察其他测试代码,可以发现,有很多相似的代码,可以改为调用上一步提取出的 getComponent,比如:

image-20240814153744147

image-20240814153851651

接下来就是,逐步将这些代码替换为调用 getComponent 来获取组件。

替换完成后,我们还会发现,这些测试代码中很多都使用了一个 Dependency 实例:

Snipaste_2024-08-14_15-59-28

可以将这个放到 setUp 中去:

image-20240814160224365

接着就可以逐个移除掉测试用例中的创建并bind dependecy 的代码。

接着,替换掉 getComponent中的实现,就可以通过 ConstructorInjectionProvider 返回一个实例,如下所示,只要 执行 provider.get 方法就可以返回实例,但是这里的问题是,该方法需要一个 context 容器作为参数:

Snipaste_2024-08-14_16-12-35

可以通过测试替身的方式创建 context 容器,并且我们知道,provider 使用这个 context 是用来从容器中获取 provider 需要的依赖的,也就是 provider 需要调用 context 的 get 方法。并且当前 provider 需要的依赖类型就是 Dependency。

所以 setUp 可以是实现为:

image-20240814162037571

并把 getComponent 方法的实现修改为:

1
2
3
4
private <T, I extends T> T getComponent(Class<T> type, Class<I> implementation) {
ConstructorInjectionProvider<I> provider = new ConstructorInjectionProvider<>(implementation);
return provider.get(context);
}

同样的,也可为 Dependency 创建测试替身:

image-20240814162619203

如果下面的测试抛异常的话,需要修改一下测试替身的返回数据:

image-20240814163326742

接着,将 getComponent 方法 inline 一下:

image-20240814163812035

至此,原来使用 config 获取实例的方法,都变成了使用 ConstructorInjectionProvider 来获取实例。也就是说我们绝大多数的测试的粒度都调整到了 ConstructorInjectionProvider 之上。

现在,只剩下一个地方在使用 config :

image-20240814164435758

稍微调整一下这个测试,我们会发现,把 config 删掉也不会有什么影响:

image-20240814165056094

同样的,现在 setUp 中的 config 也没什么用了:

image-20240814165358387

删掉 config 后,测试依然通过。就此,config 就与我们的测试上下文彻底无关了。

另外呢,还可以将在 ContainerTest 中定义的类也移动到 InjectionTest中,或在 InjectionTest 中重新定义并使用在这个测试上下文中使用的类。

比如说 ComponentWithDefaultConstructor 只在 InjectionTest 中被使用,但是却是在 ContainerTest 定义,好的做法是将其移动到 InjectionTest 中,但是这里最好还是重新定义一个新的类(不必实现任何接口),因为这个 ComponentWithDefaultConstructor 还实现了 Component 接口。这个需要自己去实现。

另外,以下的这两个测试其实功能是一样的,下面的测试可以删掉

image-20240814170235565

测试文档化

我们说测试应该是文档,但是文档不应该是我们实现的过程,因为在TDD中测试就是实现过程中的里程碑。

对于TDD来说,测试天然并不是文档,测试是实现过程中的里程碑(或记录)。需要将测试变为文档是需要经过很多努力的。

只有在这个过程中间我们将我们需要知识和需要表达的内容进行足够的提取和刻意地组织,才能使测试变成一个文档。

因为TDD的测试主要是一种里程碑,帮助我们驱动开发的,它并不是真的站在软件测试的角度上去写的。

开发人员所写的测试,和测试人员所希望看到的测试的类型其实是不同的。测试人员更多的是关注测试的完备性、对条件的覆盖。这两种测试之间是存在鸿沟的,需要刻意的调整和梳理。

一旦我们把TDD测试的功能写完,其实我们可以通过扩展(不能讲是重构了),把它 Convert 成一个更接近于测试需要的测试。

因为这个时候测试的骨架已经形成,我们只需要把它变成参数化或是数据驱动的方式去做,使测试可以覆盖更大范围的场景。

对测试进行分类分组、保持一致的命名,使其更加文档化。

文档化 InjectionTest

统一命名

统一测试命名,使其更加文档化:

1
should_bind_type_to_a_class_with_default_constructor

修改为

1
should_call_default_constructor_if_no_inject_constructor

因为 bind 是针对 config 描述的测试

再一个:

1
should_bind_type_to_a_class_with_inject_constructor

修改为:

1
should_inject_dependency_via_inject_constructor

细化分组

将 ConstructorInjection、FieldInjection、MethodInjection 分别再按 Injection 和 IllegalInjectXXX 进行分组。

具体的结果,请查看 commit id 为 a9590a2a 的 commit 记录。

image-20240814175622797

当然,视情况,还可以进行更细一步的分组。

文档化 ContainerTest

目前 ContainerTest 中还剩下的测试有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
ContextConfig config;

@BeforeEach
public void setUp(){
config = new ContextConfig();
}

// 组件构造相 关的测试类
@Nested
public class ComponentConstruction{

// instance
@Test
public void should_bind_type_to_a_specific_instance() {
// 创建一个实现了 Component 接口的匿名内部类实例
Component instance = new Component() {
};
config.bind(Component.class, instance);

assertSame(instance, config.getContext().get(Component.class).get());
}

// component does not exist
@Test
public void should_return_empty_if_component_not_defined() {
Optional<Component> component = config.getContext().get(Component.class);
assertTrue(component.isEmpty());
}

@Nested
public class DependencyCheck {

// dependencies not exist
@Test
public void should_throw_exception_if_dependency_not_found() {
config.bind(Component.class, ComponentWithInjectConstructor.class);

DependencyNotFoundException exception = assertThrows(DependencyNotFoundException.class, () -> {
config.getContext();
});

assertEquals(Dependency.class, exception.getDependency());
assertEquals(Component.class, exception.getComponent());
}


// cyclic dependencies
@Test // A -> B -> A
public void should_throw_exception_if_cyclic_dependencies() {
config.bind(Component.class, ComponentWithInjectConstructor.class);
config.bind(Dependency.class, DependencyDependedOnComponent.class);

CyclicDependenciesException exception =
assertThrows(CyclicDependenciesException.class, () -> config.getContext());

Set<Class<?>> classes = Sets.newSet(exception.getComponents());

assertEquals(2, classes.size());
assertTrue(classes.contains(Component.class));
assertTrue(classes.contains(Dependency.class));
}
@Test // A -> B -> C -> A
public void should_throw_exception_if_transitive_cyclic_dependencies() {
config.bind(Component.class, ComponentWithInjectConstructor.class);
config.bind(Dependency.class, DependencyDependedOnAnotherDependency.class);
config.bind(AnotherDependency.class, AnotherDependencyDependedOnComponent.class);

CyclicDependenciesException exception =
assertThrows(CyclicDependenciesException.class, () -> config.getContext());

List<Class<?>> components = Arrays.stream(exception.getComponents()).toList();

assertEquals(3, components.size());
assertTrue(components.contains(Component.class));
assertTrue(components.contains(Dependency.class));
assertTrue(components.contains(AnotherDependency.class));
}

}

}

这些测试都是在 Context 上下文之上的测试,我们也可以将这些测试移动到一个独立的测试类中,比如 ContextTest 中。

参考 DependencyCheck 的测试分组,这里也可以将前两个测试归类到一个名为 TypeBinding 的分类中:

image-20240815115724375

1
should_return_empty_if_component_not_defined

改名为:

1
should_retrieve_empty_for_unbind_type

TypeBinding 注入方式参数化

在 TypeBinding 分类中增加一个测试,并且使用参数化,将一个测试泛化为多个测试,分别测试根据:构造器注入、字段注入和方法注入的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 将一个测试泛化为多个测试,分别测试根据:构造器注入、字段注入和方法注入的情况
@ParameterizedTest(name = "supporting {0}")
@MethodSource
public void should_bind_type_to_an_injectable_component(Class<? extends Component> componentType) {
Dependency dependency = new Dependency() {
};
config.bind(Dependency.class, dependency);
config.bind(Component.class, componentType); // 参数化测试不同的注入方式

Optional<Component> component = config.getContext().get(Component.class);

assertTrue(component.isPresent());
assertSame(dependency, component.get().dependency());
}

public static Stream<Arguments> should_bind_type_to_an_injectable_component() {
return Stream.of(
Arguments.of(Named.of("Constructor Injection", TypeBinding.ConstructorInjection.class)),
Arguments.of(Named.of("Field Injection", TypeBinding.FieldInjection.class)),
Arguments.of(Named.of("Method Injection", TypeBinding.MethodInjection.class))
);
}


static class ConstructorInjection implements Component {
private Dependency dependency;

@Inject
public ConstructorInjection(Dependency dependency) {
this.dependency = dependency;
}

@Override
public Dependency dependency() {
return dependency;
}
}

static class FieldInjection implements Component {
@Inject
Dependency dependency; // 目前不支持注入私有字段

@Override
public Dependency dependency() {
return dependency;
}
}

static class MethodInjection implements Component {
private Dependency dependency;

@Inject
public void install(Dependency dependency) {
this.dependency = dependency;
}

@Override
public Dependency dependency() {
return dependency;
}
}

注意,需要修改一下 Component 的定义,增加默认方法:

1
2
3
interface Component{
default Dependency dependency() {return null;}
}

DependencyCheck 参数化

同理将 DependencyCheck 中的三个测试分别参数化。

依赖缺失

测试三种不同的注入方式是否满足依赖缺失的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// dependencies not exist
@ParameterizedTest
@MethodSource
public void should_throw_exception_if_dependency_not_found(Class<? extends Component> componentType) {
config.bind(Component.class, componentType);

DependencyNotFoundException exception = assertThrows(DependencyNotFoundException.class, () -> {
config.getContext();
});

assertEquals(Dependency.class, exception.getDependency());
assertEquals(Component.class, exception.getComponent());
}

public static Stream<Arguments> should_throw_exception_if_dependency_not_found() {
return Stream.of(
Arguments.of(Named.of("Constructor Injection", DependencyCheck.MissingDependencyConstructor.class)),
Arguments.of(Named.of("Field Injection", DependencyCheck.MissingDependencyField.class)),
Arguments.of(Named.of("Method Injection", DependencyCheck.MissingDependencyMethod.class))
);
}

static class MissingDependencyConstructor implements Component{
@Inject
public MissingDependencyConstructor(Dependency dependency) {
}
}

static class MissingDependencyField implements Component {
@Inject
Dependency dependency;
}

static class MissingDependencyMethod implements Component {
@Inject
public void install(Dependency dependency) {
}
}

image-20240815135153837

直接循环依赖

测试不同的注入方式的组合是否满足循环依赖的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// cyclic dependencies
// A -> B -> A
@ParameterizedTest(name = "cyclic dependency between {0} and {1}")
@MethodSource
public void should_throw_exception_if_cyclic_dependencies(Class<? extends Component> componentType,
Class<? extends Dependency> dependencyType) {
config.bind(Component.class, componentType);
config.bind(Dependency.class, dependencyType);

CyclicDependenciesException exception =
assertThrows(CyclicDependenciesException.class, () -> config.getContext());

Set<Class<?>> classes = Sets.newSet(exception.getComponents());

assertEquals(2, classes.size());
assertTrue(classes.contains(Component.class));
assertTrue(classes.contains(Dependency.class));
}

public static Stream<Arguments> should_throw_exception_if_cyclic_dependencies() {
List<Arguments> arguments = new ArrayList<>();
for (Named component : List.of(Named.of("Constructor Injection", DependencyCheck.CyclicComponentInjectConstructor.class),
Named.of("Field Injection", DependencyCheck.CyclicComponentInjectField.class),
Named.of("Method Injection", DependencyCheck.CyclicComponentInjectMethod.class))) {
for (Named dependency : List.of(Named.of("Constructor Injection", DependencyCheck.CyclicDependencyInjectConstructor.class),
Named.of("Field Injection", DependencyCheck.CyclicDependencyInjectField.class),
Named.of("Method Injection", DependencyCheck.CyclicDependencyInjectMethod.class))) {
arguments.add(Arguments.of(component, dependency));
}
}
return arguments.stream();
}

static class CyclicComponentInjectConstructor implements Component {
@Inject
public CyclicComponentInjectConstructor(Dependency dependency) {
}
}

static class CyclicComponentInjectField implements Component {
@Inject
Dependency dependency;
}

static class CyclicComponentInjectMethod implements Component {
@Inject
public void install(Dependency dependency) {
}
}

static class CyclicDependencyInjectConstructor implements Dependency {
@Inject
public CyclicDependencyInjectConstructor(Component component) {
}
}

static class CyclicDependencyInjectField implements Dependency {
@Inject
Component component;
}

static class CyclicDependencyInjectMethod implements Dependency {
@Inject
public void install(Component component) {
}
}

image-20240815135326871

间接循环依赖

测试不同的注入方式的组合是否能满足间接循环依赖的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// A -> B -> C -> A
@ParameterizedTest(name = "transitive cyclic dependency between {0}, {1} and {2}")
@MethodSource
public void should_throw_exception_if_transitive_cyclic_dependencies(Class<? extends Component> componentType,
Class<? extends Dependency> dependencyType,
Class<? extends AnotherDependency> anotherDependencyType) {
config.bind(Component.class, componentType);
config.bind(Dependency.class, dependencyType);
config.bind(AnotherDependency.class, anotherDependencyType);

CyclicDependenciesException exception =
assertThrows(CyclicDependenciesException.class, () -> config.getContext());

List<Class<?>> components = Arrays.stream(exception.getComponents()).toList();

assertEquals(3, components.size());
assertTrue(components.contains(Component.class));
assertTrue(components.contains(Dependency.class));
assertTrue(components.contains(AnotherDependency.class));
}

public static Stream<Arguments> should_throw_exception_if_transitive_cyclic_dependencies() {
List<Arguments> arguments = new ArrayList<>();
for (Named component : List.of(Named.of("Constructor Injection", DependencyCheck.CyclicComponentInjectConstructor.class),
Named.of("Field Injection", DependencyCheck.CyclicComponentInjectField.class),
Named.of("Method Injection", DependencyCheck.CyclicComponentInjectMethod.class))) {
for (Named dependency : List.of(Named.of("Constructor Injection", DependencyCheck.CyclicDependencyInjectConstructorWithAnotherDependency.class),
Named.of("Field Injection", DependencyCheck.CyclicDependencyInjectFieldWithAnotherDependency.class),
Named.of("Method Injection", DependencyCheck.CyclicDependencyInjectMethodWithAnotherDependency.class))) {
for (Named anotherDependency : List.of(Named.of("Constructor Injection", DependencyCheck.CyclicDependencyInjectConstructorWithComponent.class),
Named.of("Field Injection", DependencyCheck.CyclicDependencyInjectFieldWithComponent.class),
Named.of("Method Injection", DependencyCheck.CyclicDependencyInjectMethodWithComponent.class))) {
arguments.add(Arguments.of(component, dependency, anotherDependency));
}
}
}
return arguments.stream();
}

static class CyclicDependencyInjectConstructorWithAnotherDependency implements Dependency {
@Inject
public CyclicDependencyInjectConstructorWithAnotherDependency(AnotherDependency anotherDependency) {
}
}

static class CyclicDependencyInjectFieldWithAnotherDependency implements Dependency {
@Inject
AnotherDependency anotherDependency;
}

static class CyclicDependencyInjectMethodWithAnotherDependency implements Dependency {
@Inject
public void install(AnotherDependency anotherDependency) {
}
}

static class CyclicDependencyInjectConstructorWithComponent implements AnotherDependency {
@Inject
public CyclicDependencyInjectConstructorWithComponent(Component component) {
}
}

static class CyclicDependencyInjectFieldWithComponent implements AnotherDependency {
@Inject
Component component;
}

static class CyclicDependencyInjectMethodWithComponent implements AnotherDependency {
@Inject
public void install(Component component) {
}
}

image-20240815135441506

移动 ContainerTest 中的部分测试用例类

ContainerTest 中的部分测试用例类现在只会在 InjectionTest 中使用,应该使用 Move Class 方法重构,将这些类移动到 InjectionTest 中。

image-20240815140852626

整理好之后,测试目录的代码结构如下所示:

image-20240815141206661

总结

这里的 ContextTest 中的测试更接近于 API,所以也适合参数化进而更加文档化。

对于TDD来说,测试天然并不是文档,测试是实现过程中的里程碑(或记录)。需要将测试变为文档是需要经过很多努力的。

只有在这个过程中间我们将我们需要知识和需要表达的内容进行足够的提取和刻意地组织,才能使测试变成一个文档。

因为TDD的测试主要是一种里程碑,帮助我们驱动开发的,它并不是真的站在软件测试的角度上去写的。

开发人员所写的测试,和测试人员所希望看到的测试的类型其实是不同的。测试人员更多的是关注测试的完备性、对条件的覆盖。这两种测试之间是存在鸿沟的,需要刻意的调整和梳理。

一旦我们把TDD测试的功能写完,其实我们可以通过扩展(不能讲是重构了),把它 Convert 成一个更接近于测试需要的测试。

因为这个时候测试的骨架已经形成,我们只需要把它变成参数化或是数据驱动的方式去做,就可以使测试覆盖更大范围的场景。

重构生产代码

目前,我们的生产代码主要集中在 ContextConfig 和 ConstructorInjectionProvider 中。

重构 ContextConfig

这两个类里面的功能不多,如果非要重构的话

1
2
3
4
5
interface ComponentProvider<T> {
T get(Context context);

List<Class<?>> getDependencies();
}

改写为:

1
2
3
4
5
6
7
interface ComponentProvider<T> {
T get(Context context);

default List<Class<?>> getDependencies() {
return List.of();
}
}

将其改为一个函数式的接口,即将 getDependencies 方法定义为接口的默认方法。

那么就可以将下面的代码改写为使用lambda:

image-20240814190544963

image-20240814190717435

重构 ConstructorInjectionProvider

首先,重命名 ConstructorInjectionProvider 的名字,因为现在不仅仅是关于构造器的注入,而是所有的 Injection 都在里面。

重命名为 InjectionProvider

其他问题就是易读性比较差,并且还有一些重复。

很多地方都需要判断是否被 Inject注解标记,这里有比较多的重复代码:

image-20240814193426209

先提取方法:

image-20240814193907824

因为不止是用在 Field,还需要支持构造函数、方法注入等场景的判断,就需要将这个方法修改为支持范型的方法。

因为 isAnnotationPresent 方法是在一个接口(公共的基类)上定义的:

image-20240814194214679

这里就可以把这个范型转换为 AnnotatedElement,那么这个方法的定义就是:

1
2
3
4
private static <T extends AnnotatedElement> Stream<T> injectable(T[] declaredFields) {
return Arrays.stream(declaredFields)
.filter(f -> f.isAnnotationPresent(Inject.class));
}

接着,使用这个函数替换掉判断被Inject标注的构造函数、字段和方法的代码。

提取判断子类覆盖父类的方法:

image-20240814195736380

提取方法,判断子父类方法都被Inject标注

image-20240814200226834

提取方法,判断子类没有被 Inject 标注

image-20240814200548328

稍微调整一下,可以得到相似的代码:

image-20240814202044074

提取方法:

image-20240814202352826

同样的为了支持Method,需要范型化,而Method 和 Contructor 具有同一个基类 Executable:

image-20240814201633717

image-20240814201652190

所以,将提取的方法修改为:

1
2
3
4
private static <T> Object[] toDependencies(Context context, Executable executable) {
return Arrays.stream(executable.getParameterTypes())
.map(t -> context.get(t).get()).toArray();
}

修改 Method 的方法:

image-20240814202727618

将 getArray inline 一下可以实现替换,inline 掉一些方法和变量后,变为:

Snipaste_2024-08-14_20-30-15

为了使这块代码看上去更一致,也可以将 Feild 获取依赖的代码提取为方法:

image-20240814203236723

提取方法,用函数名表达含义,起到了注释的作用,下面的这段代码是用于获取默认构造函数的:

Snipaste_2024-08-14_20-35-47

Snipaste_2024-08-14_20-38-27

结构类似的方法,如何优化?

以下的两个方法结构非常类似,只是实现不同

image-20240814204107708

把中间的地方变成一个算法,通过Lambda,把要变化的部分传进去。

先把中间变化的部分提取为方法:

image-20240814205149449

先修改 getInjectFields 方法:

其中使用一个 function 来引用 getList 方法

image-20240814212142865

同样的,在 getInjectMethods 方法中也用一个 function 来间接引用 getList 方法

image-20240814212502434

接着,我们可以发现,getInjectMethods 和 getInjectFields 的代码几乎是一样的,除了部分变量的范型不同。

image-20240814212621229

如果,我们将这两段都提取成一个同名方法,你会发现这两个方法是一样的,除了范型不一样。后续我们希望做到的就是将这两个方法重构为一个方法。

仅范型不一样,所以会报错。这里需要先给其中一个方法改为不同的名字。这里先把报红的方法名修改为 traverse1

image-20240814213050948

接着,修改其中一个方法的签名,也满足两个方法的要求。这里修改 traverse。

将 component 的类型从 Class<T> 修改为 Class<?>,然后使用范型参数 T 来支持同时接收 Field 和 Method 的参数,并将 injectFields 变量重命名为更加中性的 members ,并将 function 重命名为含义更丰富的 finder

image-20240815093415791

使用 traverse 替换掉 traverse1 的调用。之后就可以将 traverse1 删掉。

稍微调整,并并通过 inline 重构下面的代码,让其变得更加简洁点:

image-20240815095344716

其中

1
Arrays.stream(injectConstructor.getParameters()).map(Parameter::getType);

可以修改为:

1
Arrays.stream(injectConstructor.getParameterTypes());

另外

1
.collect(Collectors.toList())

修改为:

1
.toList()

再 inline 这些变量:

image-20240815095814592

增加新功能-支持注入Provider

截至到目前为止,我们实现的功能基本上和2003年左右的DI注入容器的功能是差不多的。

2003年的 PicoContainer 就基本上和我们当前的功能差不多,Spring 的话还多了更多的对 Configuration 的支持。

接下来需要增加依赖选择相关的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 依赖选择相关的测试类
@Nested
public class DependenciesSelection{

@Nested
public class ProviderType {
// Context
// TODO: could get Provider<T> from context

// InjectionProvider
// TODO:support inject constructor
// TODO: support inject field
// TODO: support inject method
}

@Nested
public class Qualifier{

}

}

根据任务所属不同的上下文,可以将这些任务列表放到不同的测试中。

1
2
// Context
// TODO: could get Provider<T> from context

放到 ContextTest 中的 TypeBinding 中

1
2
3
4
// InjectionProvider
// TODO:support inject constructor
// TODO: support inject field
// TODO: support inject method

分别放到 InjectionTest 中的 ConstructorInjection、FieldInjection、MethodInjection 中。

从 Context 中获取 Provider

实现从 Context 中获取 Provider 的功能,是为了后续实现注入 Provider 的功能。

作用:

DI(Dependency Injection,依赖注入)容器中的 Provider 模式是一种常见的设计模式,用于延迟实例化依赖项。使用 Provider 注入有以下几个主要用途:

1. 延迟实例化

Provider 允许你在运行时决定何时创建依赖项的实例。这对于那些耗时较长的初始化过程或资源密集型对象非常有用。例如,如果你有一个数据库连接池,你可能不希望在应用程序启动时就创建所有的连接,而是等到真正需要的时候再创建。

2. 控制依赖的生命周期

通过 Provider,你可以控制依赖项的生命周期。例如,你可以配置一个 Provider 使得每次请求都创建一个新的实例(即每次都需要一个全新的对象),或者复用同一个实例(单例模式)。这有助于管理内存使用和资源分配。

3. 测试友好

Provider 使测试变得更加容易。在单元测试或集成测试中,你可以轻松地为依赖项提供不同的实现或模拟对象,而不必修改生产代码。

4. 动态配置

使用 Provider 可以让你在运行时根据不同的配置或环境动态地改变依赖项的行为。例如,在开发环境中使用本地数据库,而在生产环境中使用远程数据库。

5. 解耦

Provider 的使用有助于降低代码之间的耦合度。依赖项的创建逻辑与业务逻辑分离,使得代码更易于维护和扩展。

示例

假设你有一个 UserService 类,它依赖于一个 DatabaseConnection 对象。你可以使用 Provider 来管理这个依赖关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface DatabaseConnection {
void connect();
void disconnect();
}

public class UserService {
private final Provider<DatabaseConnection> dbConnectionProvider;

public UserService(Provider<DatabaseConnection> dbConnectionProvider) {
this.dbConnectionProvider = dbConnectionProvider;
}

public void performOperation() {
DatabaseConnection connection = dbConnectionProvider.get();
connection.connect();
// 执行业务逻辑
connection.disconnect();
}
}

在这个例子中,UserService 依赖于一个 DatabaseConnectionProvider。每当需要连接到数据库时,UserService 就会调用 dbConnectionProvider.get() 来获取一个新的连接。这使得 UserService 可以灵活地处理连接的创建和关闭,同时也简化了单元测试的实现。

总结

使用 Provider 注入可以提高代码的灵活性和可测试性,同时还能有效地管理依赖项的生命周期。这在大型应用中特别有用,因为它可以帮助减少内存消耗和提高性能。

我们预期的功能大致如下所示,即希望能从 Context 中获取指定类型的 Provider,但是目前 Java 的范型不支持这种语法。

Provider 是:jakarta.inject.Provider

image-20240815144345057

要想实现这个功能,需要先定义一个范型的包装类型:

1
2
3
4
5
6
static abstract class TypeLiteral<T> {
public ParameterizedType getType() {
return (ParameterizedType) ((ParameterizedType)(getClass().getGenericSuperclass()))
.getActualTypeArguments()[0];
}
}

ParameterizedType 是 Java 泛型类型的一种表示形式,用于描述带有类型参数的类型(例如 List<String>

如何使用:

1
2
3
4
5
6
7
8
9
10
@Test
@Disabled
public void java_api() {
Component component = new Component() {
};
ParameterizedType type = new TypeLiteral<Provider<Component>>() {}.getType();

assertEquals(Provider.class, type.getRawType());
assertEquals(Component.class, type.getActualTypeArguments()[0]);
}

所以测试,应该如下:

image-20240815155136365

在 Context 接口中创建这个 get 方法:

image-20240815155834612

接着在 ContextConfig 中快速实现这个方法,使编译通过:

image-20240815160013631

现在运行测试,是无法通过的。

实现:

1
2
3
4
5
6
@Override
public Optional get(ParameterizedType type) {
Class<?> componentType = (Class<?>)type.getActualTypeArguments()[0];
return Optional.ofNullable(providers.get(componentType))
.map(provider -> (Provider<Object>) () -> provider.get(this));
}

.map(provider -> (Provider<Object>) () -> provider.get(this)): 如果 providers.get(componentType) 不为 null,那么这里会将找到的提供者转换为一个新的 Provider<Object> 实例。这个 lambda 表达式创建了一个新的函数,当被调用时,它通过调用原始提供者的 get 方法来获取一个对象实例。注意这里进行了类型转换 (Provider<Object>),这表示期望的结果是一个能够提供 Object 类型的提供者。

sad path

1
2
3
4
5
6
7
8
9
10
11
@Test
public void should_not_retrieve_provider_bind_type_as_unsupported_container() {
Component component = new Component() {
};
config.bind(Component.class, component);
Context context = config.getContext();

ParameterizedType type = new TypeLiteral<List<Component>>(){}.getType();

assertFalse(context.get(type).isPresent());
}

实现:

1
2
3
4
5
6
7
8
@Override
public Optional get(ParameterizedType type) {
// 直接校验范型类型是否为 Provider
if (type.getRawType() != Provider.class) return Optional.empty();
Class<?> componentType = (Class<?>)type.getActualTypeArguments()[0];
return Optional.ofNullable(providers.get(componentType))
.map(provider -> (Provider<Object>) () -> provider.get(this));
}

support provider inject constructor

构造测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// TODO:support inject constructor
static class ProviderInjectConstructor {
Provider<Dependency> dependency;

@Inject
public ProviderInjectConstructor(Provider<Dependency> dependency) {
this.dependency = dependency;
}
}

@Test
public void should_inject_provider_via_inject_constructor() {
ProviderInjectConstructor instance = new InjectionProvider<>(ProviderInjectConstructor.class).get(context);

assertNotNull(instance.dependency);
assertSame(dependencyProvider, instance.dependency);
}

因为 InjectionTest 是使用测试替身来进行测试的,所以这里同时还要设置测试替身和 setUp:

image-20240815170531302

运行测试会在 InjectionProvider 中报异常:

image-20240815170616411

因为这里只能按 Class 的类型获取实例

需要修改为:

同时至此 Class 和 ParameterizedType 类型

1
2
3
4
5
6
7
8
private static <T> Object[] toDependencies(Context context, Executable executable) {
return Arrays.stream(executable.getParameters()).map(
p -> {
Type type = p.getParameterizedType();
if (type instanceof ParameterizedType) return context.get((ParameterizedType) type).get();
return context.get((Class<?>) type).get();
}).toArray();
}

运行测试,通过。

support provider inject method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// TODO: support inject method
static class ProviderInjectMethod {
Provider<Dependency> dependency;

@Inject
public void install(Provider<Dependency> dependency) {
this.dependency = dependency;
}
}

@Test
public void should_inject_provider_via_inject_method() {
ProviderInjectMethod instance = new InjectionProvider<>(ProviderInjectMethod.class).get(context);

assertNotNull(instance.dependency);
assertSame(dependencyProvider, instance.dependency);
}

运行测试,直接通过。

support provider inject field

构建测试

1
2
3
4
5
6
7
8
9
10
11
12
13
// support provider inject field
static class ProviderInjectField {
@Inject
Provider<Dependency> dependency;
}

@Test
public void should_inject_provider_via_inject_field() {
ProviderInjectField instance = new InjectionProvider<>(ProviderInjectField.class).get(context);

assertNotNull(instance.dependency);
assertSame(dependencyProvider, instance.dependency);
}

运行测试,以下代码会报错:

1
2
3
private static Object toDependency(Context context, Field field) {
return context.get(field.getType()).get();
}

同样是查找依赖的代码异常,修改为:

1
2
3
4
5
private static Object toDependency(Context context, Field field) {
Type type = field.getGenericType();
if (type instanceof ParameterizedType) return context.get((ParameterizedType) type).get();
return context.get(field.getType()).get();
}

遗漏的任务-Provider 依赖的检查

同理,注入 Provider 时也需要检查依赖缺失、循环依赖的情况。

目前,对依赖的检查需要调用 getDependencies 接口,而这里的 getDependencies 接口依然返回的是 Class 类型。我们从容器中寻找依赖时,目前分为两种情况,分别是 Class 和 ,所以这里对依赖缺失或循环依赖的检查可能会出现问题。

1
2
3
4
5
6
7
interface ComponentProvider<T> {
T get(Context context);

default List<Class<?>> getDependencies() {
return List.of();
}
}

以依赖缺失为例,在 ContextTest 中增加测试用例,增加一个参数值:

image-20240816093648383

1
2
3
4
5
static class MissingDependencyProviderConstructor implements Component {
@Inject
public MissingDependencyProviderConstructor(Provider<Dependency> dependency){
}
}

运行,会有一个异常:

image-20240816093851722

我们期望提示是 Dependency 未找到,而不是 Provider 未找到,或者关于谁的 Provider 未找到,因为当我要求修正错误的时候,也不会去 bind 一个 Provider,而是 bind 一个 Dependency。

在回看 getDependencies 方法,我们期望这里能返回 Class 和 ParameterizedType 的公共接口,即 Type 接口。

1
2
3
4
5
6
7
interface ComponentProvider<T> {
T get(Context context);

default List<Class<?>> getDependencies() {
return List.of();
}
}

这里不能直接修改,需要使用先增加新功能再替换旧功能的方式重构,以下就是预取我们要实现的方式,使用 getDependencyTypes 替换掉 getDependencies:

1
2
3
4
5
6
7
8
9
10
11
interface ComponentProvider<T> {
T get(Context context);

default List<Class<?>> getDependencies() {
return List.of();
}

default List<Type> getDependencyTypes() {
return List.of();
}
}

补充两个测试用例:

image-20240816101405554

将这两个测试用例转换为实际的任务测试:

分别在 ConstructorInjection、FieldInjection、MethodInjection 中的 Injection 中增加以下测试

1
2
3
// TODO:should include dependency type from inject constructor
// TODO:should include dependency type from inject field
// TODO:should include dependency type from inject method

因为修改涉及的步骤比较长,先注释掉测试用例:

Snipaste_2024-08-16_10-26-52

获取构造器中的依赖的测试:

1
2
3
4
5
6
7
8
// TODO:should include dependency type from inject constructor
@Test
public void should_include_dependency_type_from_inject_constructor() {
InjectionProvider<ProviderInjectConstructor> provider =
new InjectionProvider<>(ProviderInjectConstructor.class);

assertArrayEquals(new Type[]{dependencyProviderType}, provider.getDependencyTypes().toArray(Type[]::new));
}

参考前面的 getDependencies 方法,该方法并不检查依赖缺失和循环依赖,但是该方法保证了后续检查的正确性。

所以这里的 getDependencyTypes 也是同理。

实现,在 InjectionProvider 中实现这个 getDependencyTypes 方法,与 getDependencies 类似:

image-20240816110219942

同理,构造字段注入时获取 Provider 依赖类型的测试:

1
2
3
4
5
6
7
8
// TODO:should include dependency type from inject field
@Test
public void should_include_provider_dependency_type_from_inject_field() {
InjectionProvider<ProviderInjectField> provider =
new InjectionProvider<>(ProviderInjectField.class);

assertArrayEquals(new Type[]{dependencyProviderType}, provider.getDependencyTypes().toArray(Type[]::new));
}

实现:

image-20240816111741157

同理,构造方法注入时获取 Provider 依赖类型的测试:

1
2
3
4
5
6
7
// TODO:should include dependency type from inject method
@Test
public void should_include_provider_dependency_type_from_inject_method() {
InjectionProvider<ProviderInjectMethod> provider = new InjectionProvider<>(ProviderInjectMethod.class);

assertArrayEquals(new Type[]{dependencyProviderType}, provider.getDependencyTypes().toArray(Type[]::new));
}

实现:

image-20240816112722818

完成 getDependencyTypes 后,就是要使用 getDependencyTypes 来完成依赖缺失的检查。

Provider 检查依赖缺失

恢复,ContextTest 依赖缺失中的测试用例:

1
2
3
4
5
static class MissingDependencyProviderConstructor implements Component {
@Inject
public MissingDependencyProviderConstructor(Provider<Dependency> dependency){
}
}

Snipaste_2024-08-16_11-30-20

我们知道,目前检查依赖,并且使用了 getDependencies 方法的地方是 ContextConfig 中的 checkDependencies 方法,这里我们希望将使用 getDependencies 改为使用 getDependencyTypes

image-20240816113629005

先提取方法:

image-20240816113543754

使用 Type 替换 Class<?> 并且属于 Class 类型的逻辑依然保持不变:

image-20240816114253710

被 Provider 包装的类型,需要获取到被包装的依赖的类型,并传递给 DependencyNotFoundException

image-20240816114722694

目前我们仅实现了对依赖缺失的检查,并没有实现循环依赖的检查(实际上引入 Provider 就解除了循环依赖)。

同理,在 ContextTest 中增加字段注入、方法注入时检查依赖缺失的测试用例。

虽然我们已经知道这两个测试会通过,但是还是需要增加这两个测试用例,因为 ContextTest 测试的是比较对外的 API,需要完善测试文档化的诉求。

image-20240816115431932

1
2
3
4
5
6
7
8
9
10
static class MissingDependencyProviderField implements Dependency {
@Inject
Provider<Dependency> dependency;
}

static class MissingDependencyProviderMethod implements Dependency {
@Inject
public void install(Provider<Dependency> dependency){
}
}

运行测试,直接通过,不用修改生产代码。

Provider 检查循环依赖

1
2
3
4
5
6
7
8
9
10
11
12
static class CyclicDependencyProviderInjectConstructor implements Dependency {
@Inject
public CyclicDependencyProviderInjectConstructor(Provider<Component> component) {
}
}
@Test
public void should_not_throw_exception_if_cyclic_dependencies_with_provider() {
config.bind(Component.class, CyclicComponentInjectConstructor.class);
config.bind(Dependency.class, CyclicDependencyProviderInjectConstructor.class);

assertTrue(config.getContext().get(Component.class).isPresent());
}

其中 CyclicComponentInjectConstructor 已经存在

1
2
3
4
5
static class CyclicComponentInjectConstructor implements Component {
@Inject
public CyclicComponentInjectConstructor(Dependency dependency) {
}
}

这里的依赖关系是:Compontent.class -> Dependency.class -> Provider<Compontent>

因为 Provider<Compontent>config.bind(Component.class, CyclicComponentInjectConstructor.class); 时就已经创建,所以这里的依赖循环就解除了。

同理 Provider<Compontent> -> Provider<Dependency> -> Provider<Compontent> 也是如此,引入 Provider 后依赖的循环就解除了:

1
2
3
4
5
6
7
8
9
10
11
12
static class CyclicComponentProviderInjectConstructor implements Component {
@Inject
public CyclicComponentProviderInjectConstructor(Provider<Dependency> dependency) {
}
}
@Test
public void should_not_throw_exception_if_cyclic_dependencies_with_providers() {
config.bind(Component.class, CyclicComponentProviderInjectConstructor.class);
config.bind(Dependency.class, CyclicDependencyProviderInjectConstructor.class);

assertTrue(config.getContext().get(Component.class).isPresent());
}

重构

获取依赖时的重复代码:

image-20240816142714821

先修改一下,修改后就完全一样了,可以使用提取方法的重构。

image-20240816142854828

提取方法后:

image-20240816143124700

提取后,然后也可以有选择的 inline 掉部分代码,简化代码。

观察 ComponentProvider 中的 getDependencies 发现,这个方法只在测试中用到。

image-20240816143615349

我们将测试中的调用替换为 getDependencyTypes 发现也没有什么问题。因为 getDependencyTypes 返回的 Type 是 Class 的父接口。

所以可以把这所有 getDependencies 的调用修改为调用 getDependencyTypes,之后可以删除 getDependencies。

同时,将 Class 类型替换为 Type:

Snipaste_2024-08-16_14-44-20

重构对 Type 类型的判断

目前 Context 中有两个接口,分别支持不同的类型:

1
2
3
4
5
public interface Context {
<Type> Optional<Type> get(Class<Type> type);

Optional get(ParameterizedType type);
}

为了支持这两种不同的类型,需要在两个类中的代码的各处做不同的判断,多个 if – else

image-20240816145441076

image-20240816145519993

那么当需要对这种结构的类型做修改的话,很可能就会发生散弹式修改

很多时候,我们对代码不是很满意,但是又不知道如何下手修改。

这个时候可以考虑将相关功能的散落在各处的坏味道的代码集中到同一个上下文中。

比如说,InjectionProvider 中的 toDependency 方法是根据类型判断调用 Context 接口中的哪个方法的:

1
2
3
4
private static Object toDependency(Context context, Type type) {
if (type instanceof ParameterizedType) return context.get((ParameterizedType) type).get();
return context.get((Class<?>) type).get();
}

那么可以将这个实现移动到 Context 接口的默认方法中去:

1
2
3
4
5
6
7
8
9
10
public interface Context {
<Type> Optional<Type> get(Class<Type> type);

Optional get(ParameterizedType type);

default Optional getType(Type type) {
if (type instanceof ParameterizedType) return get((ParameterizedType) type);
return get((Class<?>) type);
}
}

那 toDependency 就可以改为:

1
2
3
private static Object toDependency(Context context, Type type) {
return context.getType(type).get();
}

运行测试,会有异常,这是在 InjectionTest 使用测试替身引起的,我们之前的测试替身是调用的是 Context 的 get 方法,但是修改代码后调用的是 Context 的 getType 方法,所以需要同步修改测试替身,修改为调用 getType 方法。

image-20240816151832369

用测试替身还是用真实的待测组件?

当你接口约定稳定的时候,那么用 stub 会更简单。所以测试替身需要知道待测组件内部的实现,当内部实现修改时,可能造成测试失败。所以这种使用测试替身的伦敦学派测试,对重构的影响比较大。

之后在 ContextConfig 中实现这个方法,就可以将接口的默认方法恢复为未实现的普通方法:

image-20240816152614060

至此,我们就将对这两类型的判断相关的代码,都移动到了 ContextConfig 这个上下文中。

再然后,查看一下还有哪里在使用 Context 的 get 接口,可以发现,除了 ContextConfig 中使用外,就是在测试方法中使用。

我们把这些测试中使用 get 的地方都改成 getType,也不会异常。

那么 get 方法就只在 ContextConfig 中被使用了,这样就可以把 Context 接口中的 get 方法移除掉,并移除到 ContextConfig 中的 Override 注解并且设置为 private,只保留一个 getType 方法作为对外的API:

Snipaste_2024-08-16_15-43-32

再将 getType 重命名为 get。

至此,我们就实现了使用一个接口替换为原来的两个接口的效果。

继续重构,先使用函数来代替注释

Snipaste_2024-08-16_15-53-25

这里的 ContainerType 的含义是比如:List<>、Provider<> 这些容器。

重命名方法:

image-20240816160853947

提取方法

Snipaste_2024-08-16_16-09-16

在修改一下提取的方法的参数:

1
2
3
private static Class<?> getComponentType(Type type) {
return (Class<?>) ((ParameterizedType)type).getActualTypeArguments()[0];
}

再修改一下 checkDependencies 中的代码:

image-20240816161944872

至此我们会发现,整个 ContextConfig 就是围绕两个不同的 Type 来做判断,并实现功能的。

出现这种情况的话,通常都意味着封装失败。造成这种情况都是因为我们使用了一些我们无法修改,无法增加行为的类和接口(可能是由其他框架或库提供的,也可能是JDK中的)。

在实践中,有些时候不要使用原始类型(Primitive Type),并不是指不使用 int 而是使用 Integer,而是说所有我们无法修改的类都是原始类型。

封装 Type 类型的判断逻辑

因为我们使用的是原始类型,在我们的上下文中代表某个概念。这种概念一般会有概念缺失(Concept Missing)。

不仅仅是在代码层面上重构,其实是要从模型的角度上重构。正是因为使用了这种内容缺失的概念,以至于每次使用到这种概念的时候,需要对它进行复杂的判断。

对于这种概念缺失的优化呢,就是使用封装,一般有两种封装方式,分别是:行为封装、数据封装。

????

这里使用数据封装

对代码进行稍微的整理,会发现,这些对 Type 进行判断的代码中,都会包含 componentType 或 ContainerType 或两者同时包含。

image-20240816172206505

新建一个封装类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static class Ref {
private Type container;
private Class<?> component;

Ref(ParameterizedType type) {
this.container = type.getRawType();
this.component = (Class<?>) type.getActualTypeArguments()[0];
}

Ref(Class<?> component) {
this.component = component;
}

static Ref of(Type type) {
if (type instanceof ParameterizedType) return new Ref((ParameterizedType) type);
return new Ref((Class<?>) type);
}

public Type getContainer() {
return container;
}

public Class<?> getComponent() {
return component;
}
}

接着就可以使用 Ref 来代替 type 的表示。

image-20240816172730553

这两个方法,只有在 Context 的 get 方法中被调用,因为使用了 Ref 代替了两种不同的类型,所以不需要分两个方法进行判断了,这里先 inline 这个两个方法。

inline 并整理一下代码后,得到下面的代码结构:

image-20240816173312128

其中

1
if (isContainerType(type)) { ... }

的判断,我们应该把其作为 Ref 的知识,封装到 Ref 中,在 Ref 新增 接口:

1
2
3
public boolean isContainer() {
return container != null;
}

那么这个判断语句就可以改为:

1
if (ref.isContainer())

同理,使用 Ref 代替 checkXXXDependencies 中的 type 引用:

image-20240816174437894

inline 这两个方法,inline 并调整简化代码后,变成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void checkDependencies(Class<?> component, Stack<Class<?>> visiting) {
for (Type dependency : providers.get(component).getDependencyTypes()) {
Ref ref = Ref.of(dependency);
// 如果依赖的类型不存在,就提前停止递归
if (!providers.containsKey(ref.getComponent())) throw new DependencyNotFoundException(component, ref.getComponent());
if (!ref.isContainer()) {
if (visiting.contains(ref.getComponent())) throw new CyclicDependenciesException(visiting);
visiting.push(ref.getComponent());
checkDependencies(ref.getComponent(), visiting);
visiting.pop();
}
}
}

移除代码:

Snipaste_2024-08-16_17-56-56

将 Ref 从内部类中移出。

Context 使用 Ref 对外提供访问

我们希望在 Context接口中,使用 Ref 代替 Type:

1
2
3
4
5
public interface Context {
Optional get(Type type);

Optional get(Ref ref);
}

抽取 get 方法,并Override

image-20240817094941953

这里也可以将 get(Type type) 方法改为default

1
2
3
4
5
6
7
public interface Context {
default Optional get(Type type) {
return get(Ref.of(type));
}

Optional get(Ref ref);
}

将 Ref 移入 Context 中。

尝试直接inline Optional get(Type type)方法。

inline 后,使用测试替身的地方又会报错,原因是:inline 后 eq 的位置是错误的,需要人工修改一下

image-20240817100143695

修改为:

image-20240817100436322

此外还需要在 Ref 中增加 equals 和 hashCode方法。

至此,对 Context 的访问都是通过 Ref 参数访问。

接着,需要将 ComponentProvider 中的 getDependencyTypes 方法修改为返回 List

1
2
3
4
5
6
7
interface ComponentProvider<T> {
T get(Context context);

default List<Type> getDependencyTypes() {
return List.of();
}
}

同理,也需要新增,再替换

1
2
3
4
5
6
7
8
9
10
11
interface ComponentProvider<T> {
T get(Context context);

default List<Type> getDependencyTypes() {
return List.of();
}

default List<Context.Ref> getDependencies() {
return getDependencyTypes().stream().map(Context.Ref::of).toList();
}
}

查看 getDependencyTypes 在哪里被使用,并尝试人工替换

image-20240817101707744

修改测试中的使用,把所有的使用修改为类似以下形式:

image-20240817102416259

接着就是要将旧的 List<Type> getDependencyTypes() 移出掉。

实现 getDependencies 方法

image-20240817103155965

inline 并删除 getDependencyTypes 实现

image-20240817103405954

getDependencies 还是要保留默认实现

1
2
3
4
5
6
7
interface ComponentProvider<T> {
T get(Context context);

default List<Context.Ref> getDependencies() {
return List.of();
}
}

如何让接口 API 变得更友好

目前的 API 功能都是正确的,但是从使用者的角度看就并不友好

1
2
3
4
5
6
public interface Context {

Optional get(Ref ref);

....
}

现在 Context 中 get 方法的入参和返回值都是不带范型的。

那么有些时候,就还需要使用者自己来做型转

image-20240817110150540

增加范型支持:

Snipaste_2024-08-17_11-10-00

那么这个时候再 get 时,就能直接指示类型

image-20240817111102232

因为:

1
Class<Component> component1 = Component.class;

现在就让 API 变得更容易,减少不必要的型转。

在 get 方法中增加对范型的支持

image-20240819105209273

上面的范型是对Class<?>的参数提供的支持,那么如何支持ContainerType的情况呢?

目前支持 ContainerType 的参数的方法是:

1
2
3
4
5
6
static Ref of(Type type) {
if (type instanceof ParameterizedType) {
return new Ref((ParameterizedType) type);
}
return new Ref((Class<?>) type);
}

并且在使用时,还需要用自定义的 TypeLiteral 包装一下:

image-20240817112312997

一个可能的方法是,将 Ref 和 TypeLiteral 做一个整合,以达到类似如下的使用效果:

1
context.get(new Context.Ref<Provider<Component>>() {});

这里是创建一个匿名的 Ref 实例,如果我们获取去到这个实例的类型并为 Ref 中必要的字段赋值,可以避免用户在时使用自己构造TypeLiteral。

这里需要先创建一个无参构造函数,并在函数中获取到这个实例的范型类型,并根据范型类型赋值 Ref 的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected Ref () {
Type type = ((ParameterizedType)(getClass().getGenericSuperclass())).getActualTypeArguments()[0];
init(type);
}

private void init(Type type) {
if (type instanceof ParameterizedType) {
this.container = ((ParameterizedType) type).getRawType();
this.component = (Class<?>) ((ParameterizedType) type).getActualTypeArguments()[0];
} else {
this.component = (Class<?>) type;
}
}

这里提取了 init 方法,那么也可以将原来的有参构造函数的实现委托给 init 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Ref(ParameterizedType type) {
init(type);
}

Ref(Class<ComponentType> component) {
init(component);
}

protected Ref () {
Type type = ((ParameterizedType)(getClass().getGenericSuperclass())).getActualTypeArguments()[0];
init(type);
}

private void init(Type type) {
if (type instanceof ParameterizedType) {
this.container = ((ParameterizedType) type).getRawType();
this.component = (Class<?>) ((ParameterizedType) type).getActualTypeArguments()[0];
} else {
this.component = (Class<?>) type;
}
}

那么测试类中使用时,就修改为:

image-20240817115223561

随后就可以删除 TypeLiteral 了。

Qualifier

自定义 Qualifier 的依赖

  • 注册组件时,可额外指定 Qualifier

  • 注册组件时,可从类对象上提取 Qualifier

  • 寻找依赖时,需同时满足类型与自定义 Qualifier 标注

  • 支持默认 Qualifier——Named

将上面的功能点,细分为多个测试任务

归属于 ContextTest 上下文中的任务,分别有关于 TypeBinding 和 DependencyCheck 的任务:

TypeBinding 的任务:

1
2
3
4
5
6
@Nested
public class WithQualifier {
// TODO binding component with qualifier
// TODO binding component with qualifiers
// TODO throw illegal component if illegal qualifier
}

DependencyCheck 的任务:

1
2
3
4
5
@Nested
public class WithQualifier {
// TODO dependency missing if qualifier not match
// TODO check cyclic dependencies with qualifier
}

归属于 InjectionTest 上下文中的任务,分别有关于 ConstructorInjection、FieldInjection、MethodInjection 的任务:

分别都有如下的测试任务:

1
2
3
4
5
@Nested
class WithQualifier {
// TODO inject with qualifier
// TODO throw illegal component if illegal qualifier given to injection point
}

binding component with qualifier

component 分为两种情况,分别是:instance 和 component。

绑定 instance

先实现 instance 的情况,创建测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
// TODO binding component with qualifier
@Test
public void should_bind_instance_with_qualifier() {
Component component = new Component() {
};
config.bind(Component.class, component, new NamedLiteral("ChosenOne"));

Context context = config.getContext();

Component chosenOne =
context.get(Context.Ref.of(Component.class, new NamedLiteral("ChosenOne"))).get();
assertSame(component, chosenOne);
}

需要新建 NamedLiteral

1
2
3
4
5
6
record NamedLiteral(String value) implements jakarta.inject.Named {
@Override
public Class<? extends Annotation> annotationType() {
return jakarta.inject.Named.class;
}
}

并且需要在 ContextConfig 中增加包含三个参数的 bind 方法

1
2
public <Type> void bind(Class<Type> type, Type instance, Annotation qualifier) {
}

在 Ref 中增加两个参数的 of 方法:

1
2
3
static <ComponentType> Ref<ComponentType> of(Class<ComponentType> component, Annotation qualifier) {
return null;
}

使得编译通过。

实现

目前,保存 providers 的 map 中的 key 只有 class,我们现在需要的是 type 和 qualifier 两个参数共同组合成的 key

image-20240819104016654

自定义一个 record 来封装 type 和 qualifier 的组合。

1
2
record Component(Class<?> type, Annotation qualifier) {
}

并新建一个 map 来保存

1
private Map<Component, ComponentProvider<?>> components = new HashMap<>();

实现 bind 方法:

1
2
3
public <Type> void bind(Class<Type> type, Type instance, Annotation qualifier) {
components.put(new Component(type, qualifier), context -> instance);
}

get 时需要判断 Ref 中是否是 Qualifier 的情况

可以在在 Ref 中增加一个字段 qualifier ,并相应的在构造方法中增加字段:

1
2
3
4
5
6
7
8
9
10
private Annotation qualifier;

Ref(Type type, Annotation qualifier) {
init(type);
this.qualifier = qualifier;
}

public Annotation getQualifier() {
return qualifier;
}

新增分支,从 Context 中获取实例

Snipaste_2024-08-19_11-18-32

运行测试,通过。

绑定组件

构造测试

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void should_bind_component_with_qualifier() {
Dependency dependency = new Dependency() {
};
config.bind(Dependency.class, dependency);
config.bind(ConstructorInjection.class, ConstructorInjection.class, new NamedLiteral("ChosenOne"));

Context context = config.getContext();

ConstructorInjection chosenOne =
context.get(Context.Ref.of(ConstructorInjection.class, new NamedLiteral("ChosenOne"))).get();
assertSame(dependency, chosenOne.dependency());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
static class ConstructorInjection implements Component {
private Dependency dependency;

@Inject
public ConstructorInjection(Dependency dependency) {
this.dependency = dependency;
}

@Override
public Dependency dependency() {
return dependency;
}
}

需要增加一个对应的 bind 方法:

1
2
3
4
public <Type, Implementation extends Type>
void bind(Class<Type> type, Class<Implementation> implementation, Annotation qualifier) {
components.put(new Component(type, qualifier), new InjectionProvider(implementation));
}

运行测试,通过。

binding component with qualifiers

绑定、注册组件的时候,可以指定多个 qualifier,但是只能根据一个取,这是规范规定的。

绑定实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TODO binding component with qualifiers
@Test
public void should_bind_instance_with_multi_qualifiers() {
Component component = new Component() {
};
config.bind(Component.class, component, new NamedLiteral("ChosenOne"), new NamedLiteral("AnotherOne"));

Context context = config.getContext();

Component chosenOne = context.get(Context.Ref.of(Component.class, new NamedLiteral("ChosenOne"))).get();
Component anotherOne = context.get(Context.Ref.of(Component.class, new NamedLiteral("AnotherOne"))).get();

assertSame(component, anotherOne);
assertSame(chosenOne, anotherOne);
}

将 bind 方法的 qualifier 参数修改为可变数组,使编译通过:

1
2
3
public <Type> void bind(Class<Type> type, Type instance, Annotation... qualifiers) {
components.put(new Component(type, qualifiers[0]), context -> instance);
}

实现,bind 时分别注册每一 qualifier

1
2
3
4
public <Type> void bind(Class<Type> type, Type instance, Annotation... qualifiers) {
for (Annotation qualifier : qualifiers)
components.put(new Component(type, qualifier), context -> instance);
}

绑定组件

构造测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void should_bind_component_with_multi_qualifiers() {
Dependency dependency = new Dependency() {
};
config.bind(Dependency.class, dependency);
config.bind(ConstructorInjection.class, ConstructorInjection.class, new NamedLiteral("ChosenOne"),
new NamedLiteral("AnotherOne"));

Context context = config.getContext();

ConstructorInjection chosenOne =
context.get(Context.Ref.of(ConstructorInjection.class, new NamedLiteral("ChosenOne"))).get();
ConstructorInjection anotherOne =
context.get(Context.Ref.of(ConstructorInjection.class, new NamedLiteral("AnotherOne"))).get();

assertSame(dependency, chosenOne.dependency());
assertSame(dependency, anotherOne.dependency());
}

实现,对应的为 bind 方法的 qualifier 参数修改为可变数组,并分别注册每一个 qualifier :

1
2
3
4
5
public <Type, Implementation extends Type>
void bind(Class<Type> type, Class<Implementation> implementation, Annotation... qualifiers) {
for (Annotation qualifier : qualifiers)
components.put(new Component(type, qualifier), new InjectionProvider(implementation));
}

重构 ContextConfig 的内部实现

位置支持 Qualifier 我们目前已经有很多方法存在平行实现(实现方法非常类似),如果继续完成其他测试,那么会产生更多的平行实现。这里我们选择先重构。

components 替换掉 providers

接下来,就是要用 components 替换掉 providers

1
2
3
private Map<Class<?>, ComponentProvider<?>> providers = new HashMap<>();

private Map<Component, ComponentProvider<?>> components = new HashMap<>();

重构之前先看一下有哪些地方使用了 providers

Snipaste_2024-08-19_14-16-56

这里使用的主要地方是 getContext 和 checkDependencies。

先看 getContext

Snipaste_2024-08-19_14-20-19

先将其中使用到 providers 的位置提取为方法:

1
2
3
4
private <ComponentType> ComponentProvider<?> getComponentProvider(Context.Ref<ComponentType> ref) {
// components.get(new Component(ref.getComponent(), ref.getQualifier()));
return providers.get(ref.getComponent());
}

上面注释的代码,就是我们预期想要的实现。

逐步将使用到 providers 的地方用 components 替换掉。

bind 方法中替换:

1
2
3
4
5
6
7
8
9
10
public <Type> void bind(Class<Type> type, Type instance) {
// providers.put(type, (ComponentProvider<Type>) context -> instance);
components.put(new Component(type, null), context -> instance);
}

public <Type, Implementation extends Type>
void bind(Class<Type> type, Class<Implementation> implementation) {
// providers.put(type, new InjectionProvider(implementation));
components.put(new Component(type, null), new InjectionProvider(implementation));
}

getContext 方法替换:

image-20240819144934546

image-20240819145015046

至此移除不在使用的 providers 即可。

观察以下两个 bind 方法,可以合并成一个

1
2
3
4
5
6
7
8
public <Type> void bind(Class<Type> type, Type instance) {
components.put(new Component(type, null), context -> instance);
}

public <Type> void bind(Class<Type> type, Type instance, Annotation... qualifiers) {
for (Annotation qualifier : qualifiers)
components.put(new Component(type, qualifier), context -> instance);
}

合并后为:

1
2
3
4
5
public <Type> void bind(Class<Type> type, Type instance, Annotation... qualifiers) {
if (qualifiers.length == 0) components.put(new Component(type, null), context -> instance);
for (Annotation qualifier : qualifiers)
components.put(new Component(type, qualifier), context -> instance);
}

建议不合并,减少一个 if 也可以有一个更清晰的对外接口。

其他坏味道

可以观察到,很多使用 Ref 的地方都需要转换为 Component

image-20240819150104089

好像 Ref 和 Component 之间存在某种关系。

其实,Ref 就应该将 Class 和 Qualifier 封装为一个整体,而不是再将这两个拆散。

一个更合理的实现是,使用一个 Component 来替换掉 Class<?> componentAnnotation qualifier 字段:

1
2
3
4
5
6
class Ref<ComponentType> {
private Type container;
// private ContextConfig.Component component;
private Class<?> component;
private Annotation qualifier;
}

这里先将 Ref 重名为 ComponentRef,并从 Context 中移出,作为一个外部类。

也将 Component 从 ContextConfig 中移出,作为一个外部类。

由于原来已经有一个 Component 用于作为测试用例的类,这里先将原来的 Component 修改为 TestComponent,以避免因重名造成的异常。

在 ComponentRef 中增加 component 字段,并将原来的 component 字段重命名为 componentType:

1
2
3
4
5
6
public class ComponentRef<ComponentType> {
private Type container;
private Component component;
private Class<?> componentType;
private Annotation qualifier;
}

增加一个 component 字段,相应的给 init 中增加一个 qualifier 的参数,并在其中为 component 赋值。

1
2
3
4
5
6
7
8
9
10
private void init(Type type, Annotation qualifier) {
if (type instanceof ParameterizedType) {
this.container = ((ParameterizedType) type).getRawType();
this.componentType = (Class<?>) ((ParameterizedType) type).getActualTypeArguments()[0];
this.component = new Component(componentType, qualifier);
} else {
this.componentType = (Class<?>) type;
this.component = new Component(componentType, qualifier);
}
}

那么所有之前通过 ref 来 new Component 都可以替换为 ref.component().

image-20240819155305967

还可以发现,getQualifier 已无处使用,可以移除,qualifier 字段也无处使用,可以移除。

同时也可将 getComponentType 的实现替换为如下:

1
2
3
public Class<?> getComponentType() {
return component.type();
}

接着,期望移除 component,先找到 component 在哪里被使用。

一个地方是在 equals 和 hashCode 方法中被使用,先重新生成这两个方法,新生成的方法不要使用 componentType 字段。

其他使用的地方就是在 init 方法中,修改 init 方法为:

1
2
3
4
5
6
7
8
9
private void init(Type type, Annotation qualifier) {
if (type instanceof ParameterizedType) {
this.container = ((ParameterizedType) type).getRawType();
Class<?> componentType = (Class<?>) ((ParameterizedType) type).getActualTypeArguments()[0];
this.component = new Component(componentType, qualifier);
} else {
this.component = new Component((Class<?>) type, qualifier);
}
}

移除 componentType 字段。

重构测试

测试文档化

从测试文档化的角度来讲,下面的两个测试是不需要的。

image-20240819161618368

这两个测试,是驱动我们开发的记录,但是不应该作为最终的文档形式。

自定义 Qualifier

1
2
3
4
5
6
7
8
9
10
11
12
@java.lang.annotation.Documented
@java.lang.annotation.Retention(RUNTIME)
@jakarta.inject.Qualifier
@interface AnotherOne {
}

record AnotherOneLiteral() implements AnotherOne {
@Override
public Class<? extends Annotation> annotationType() {
return AnotherOne.class;
}
}

bind 时使用自定义注解:

image-20240819162836708

非法的 Qualifier

验证的是非 Qualifier 注解标记的情况。

需要先创建一个非 Qualifier 的注解的包装

1
2
3
4
5
6
record TestLiteral() implements Test {
@Override
public Class<? extends Annotation> annotationType() {
return Test.class;
}
}
  • 构造绑定实例的测试:
1
2
3
4
5
6
7
// TODO throw illegal component if illegal qualifier
@Test
public void should_throw_exception_if_illegal_qualifier_given_to_instance() {
TestComponent component = new TestComponent() {
};
assertThrows(IllegalComponentException.class, () -> config.bind(TestComponent.class, component, new TestLiteral()));
}

实现,绑定时检查是否包含非Qualifier的注解:

1
2
3
4
5
6
public <Type> void bind(Class<Type> type, Type instance, Annotation... qualifiers) {
if (Arrays.stream(qualifiers).anyMatch(q -> !q.annotationType().isAnnotationPresent(Qualifier.class)))
throw new IllegalComponentException();
for (Annotation qualifier : qualifiers)
components.put(new Component(type, qualifier), context -> instance);
}
  • 构造绑定组件的测试:
1
2
3
4
5
@Test
public void should_throw_exception_if_illegal_qualifier_given_to_component() {
assertThrows(IllegalComponentException.class,
() -> config.bind(ConstructorInjection.class, ConstructorInjection.class, new TestLiteral()));
}

实现,同样的绑定时检查是否包含非Qualifier的注解:

1
2
3
4
5
6
7
public <Type, Implementation extends Type>
void bind(Class<Type> type, Class<Implementation> implementation, Annotation... qualifiers) {
if (Arrays.stream(qualifiers).anyMatch(q -> !q.annotationType().isAnnotationPresent(Qualifier.class)))
throw new IllegalComponentException();
for (Annotation qualifier : qualifiers)
components.put(new Component(type, qualifier), new InjectionProvider(implementation));
}

依赖检查

Qualifier 标记的依赖不存在

测试指定了 Qualifier 的依赖的情况:

如果依赖的参数被 Qualifier 标记,那么从 容器中获取的依赖也必须被 Qualifier 标记,否则也要抛出 DependencyNotFoundException 异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// TODO dependency missing if qualifier not match
@Test
public void should_throw_exception_if_dependency_not_found_with_qualifier() {

config.bind(Dependency.class, new Dependency() {
});
config.bind(InjectConstructor.class, InjectConstructor.class);

assertThrows(DependencyNotFoundException.class, () -> config.getContext());

}

static class InjectConstructor {
@Inject
public InjectConstructor(@AnotherOne Dependency dependency) {
}
}

目前的依赖检查的实现是基于 CompronentProvider 中的 getDependencies 方法。

image-20240819171549770

但是目前获取依赖时并没有区分被 Qualifier 标记的情况。所以想要实现依赖缺失的检查还需要修改 CompronentProvider 中的 getDependencies 方法。

这样就衍生出,include qualifier with dependency 的测试。这属于 InjectionTest 测试上下文的范畴。

分别在 InjectionTest 的 ConstructorInjection、FieldInjection、MethodInjection 中的 WithQualifier 测试分组中增加任务:

Snipaste_2024-08-19_17-25-58

构造函数注入被 Qualifier 标注的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
// TODO include qualifier with dependency
static class InjectConstructor {
@Inject
public InjectConstructor(@Named("ChosenOne") Dependency dependency) {
}
}
@Test
public void should_include_qualifier_with_dependency() {
InjectionProvider<InjectConstructor> provider = new InjectionProvider<>(InjectConstructor.class);

assertArrayEquals(new ComponentRef[]{ComponentRef.of(Dependency.class, new NamedLiteral("ChosenOne"))},
provider.getDependencies().toArray(ComponentRef[]::new));
}

实现,需要修改以下代码:

1
2
3
4
5
6
7
8
@Override
public List<ComponentRef> getDependencies() {
return Stream.concat(
Stream.concat(Arrays.stream(injectConstructor.getParameters()).map(Parameter::getParameterizedType),
injectFields.stream().map(Field::getGenericType)),
injectMethods.stream().flatMap(m -> Arrays.stream(m.getGenericParameterTypes())))
.map(ComponentRef::of).toList();
}

目前并没有考虑标注的情况。

修改 Provider 中的实现,获取依赖时在返回的 ComponentRef 应包含注解的信息。

image-20240819180342402

此外,判断 Named 于 NamedLiteral 是否相等,还需要修改 NamedLiteral 中的 equals 方法,因为是测试,所以只需要一个简单的实现。

如果是生产代码中,应该根据 Annotation 中规范的方式编写。

1
2
3
4
5
@Override
public boolean equals(Object o) {
if (o instanceof jakarta.inject.Named named) return value.equals(named.value());
return false;
}

运行测试通过。

同时,should_throw_exception_if_dependency_not_found_with_qualifier 测试也将通过。

DependencyNotFoundException 信息优化

目前 DependencyNotFoundException 中并不包含 Qualifier 注解的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DependencyNotFoundException extends RuntimeException {
private Class<?> dependency;
private Class<?> component;

public DependencyNotFoundException(Class<?> component, Class<?> dependency) {
this.dependency = dependency;
this.component = component;
}

public Class<?> getDependency() {
return dependency;
}

public Class<?> getComponent() {
return component;
}
}

如果我们还需要知道异常中注解的信息,可以修改测试,增加如下校验:

image-20240819183725309

在 DependencyNotFoundException 中新建两个方法,分别用于获取造成异常的 component 和 dependency 的信息。

DependencyNotFoundException 重构为:

增加两个 Component 类型的字段和对应的 getter,以及构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class DependencyNotFoundException extends RuntimeException {
private Class<?> dependency;
private Class<?> component;
private Component dependencyComponent;
private Component componentComponent;

public DependencyNotFoundException(Class<?> component, Class<?> dependency) {
this.dependency = dependency;
this.component = component;
}

public DependencyNotFoundException(Component componentComponent, Component dependencyComponent) {
this.dependencyComponent = dependencyComponent;
this.componentComponent = componentComponent;
}

public Class<?> getDependency() {
return dependency;
}

public Class<?> getComponent() {
return component;
}

public Component getDependencyComponent() {
return this.dependencyComponent;
}

public Component getComponentComponent() {
return this.componentComponent;
}
}

同样的 AnotherOneLiteral 也需要实现 equals 方法,否则 AnotherOneLiteral 和 AnotherOne 的比较会异常

1
2
3
4
5
6
7
8
9
10
11
record AnotherOneLiteral() implements AnotherOne {
@Override
public Class<? extends Annotation> annotationType() {
return AnotherOne.class;
}

@Override
public boolean equals(Object o) {
return o instanceof AnotherOne;
}
}

实现,找到唯一会创建 DependencyNotFoundException 的地方是 ContextConfig 中的 checkDependencies 方法,修改为:

image-20240819190900039

运行测试,should_throw_exception_if_dependency_not_found_with_qualifier 将通过。

但是,因为修改了返回的 DependencyNotFoundException 中的信息,所以之前创建的检查依赖缺失的测试将失败:

image-20240819191232231

因为当前异常中的 dependency 和 component 并没有被赋值:

Snipaste_2024-08-19_19-13-36

修改 DependencyNotFoundException 方法返回的信息:

image-20240819191622688

删除未使用的字段的构造函数。

更进一步,将使用 getDependency 和 getComponent 方法的地方,替换为使用 getDependencyComponent 和 getComponentComponent:

image-20240819191940010

至此,就没有地方使用 getDependency 和 getComponent 方法。可移除,移除后可以对 DependencyNotFoundException 中的字段和方法进行重命名。

最终,DependencyNotFoundException 重构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DependencyNotFoundException extends RuntimeException {
private Component dependency;
private Component component;

public DependencyNotFoundException(Component component, Component dependency) {
this.dependency = dependency;
this.component = component;
}

public Component getDependency() {
return this.dependency;
}

public Component getComponent() {
return this.component;
}
}

循环依赖

构造测试:

我们期望: A -> @AnotherOne A -> @Named A 不构成循环依赖,因为 Qualifier + 类型 的组合构造类型的key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// TODO check cyclic dependencies with qualifier
// A -> @AnotherOne A -> @Named A
static class AnotherOneDependency implements Dependency {
@Inject
public AnotherOneDependency(@jakarta.inject.Named("ChosenOne") Dependency dependency) {
}
}
static class NotCyclicDependency implements Dependency {
@Inject
public NotCyclicDependency(@AnotherOne Dependency dependency) {
}
}
@Test
public void should_not_throw_exception_if_component_with_same_type_tagged_with_different_qualifier() {
Dependency instance = new Dependency() {
};
config.bind(Dependency.class, instance, new NamedLiteral("ChosenOne"));
config.bind(Dependency.class, AnotherOneDependency.class, new AnotherOneLiteral());
config.bind(Dependency.class, NotCyclicDependency.class);

assertDoesNotThrow(() -> config.getContext());
}

这里还需要为 NamedLiteral 创建 hashCode 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
record NamedLiteral(String value) implements jakarta.inject.Named {
@Override
public Class<? extends Annotation> annotationType() {
return jakarta.inject.Named.class;
}

@Override
public boolean equals(Object o) {
if (o instanceof jakarta.inject.Named named) return value.equals(named.value());
return false;
}

@Override
public int hashCode() {
return "value".hashCode() * 127 ^ value.hashCode();
}
}

hashCode 方法需要根据 Annotation 的规范创建:

image-20240819195133888

运行测试,将抛出 CyclicDependenciesException

找到抛出 CyclicDependenciesException 的代码,目前只有 ContextConfig 中的 checkDependencies 方法会抛出 CyclicDependenciesException

image-20240819195927082

原因是 visiting 栈中只包含 Class 的信息,没有 Qualifier 注解相关的信息。

那么需要把 Stack 中的类型由 Class<?> 改为 Component,并将使用 visiting 的地方都改为 Component:

image-20240819200809441

image-20240819200844752

运行测试,通过。

ComponentProvider 检查 Qualifier 依赖

检查 getDependencies 获取依赖时,返回正确的依赖,前面已经完成了构造器注入 Qualifier 依赖的检查。还需要检查字段注入和方法注入 Qualifier 依赖时的检查。

这里先检查方法注入时的情况。

构造测试,位于 InjectionTest.MethodInjection.WithQualifier中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TODO include qualifier with dependency
static class InjectMethod {
@Inject
void install(@Named("ChosenOne") Dependency dependency) {

}
}
@Test
public void should_include_dependency_with_qualifier() {
InjectionProvider<InjectMethod> provider = new InjectionProvider<>(InjectMethod.class);

assertArrayEquals(new ComponentRef[]{ComponentRef.of(Dependency.class, new NamedLiteral("ChosenOne"))},
provider.getDependencies().toArray(ComponentRef[]::new));
}

实现:

image-20240819202905248

构造字段注入 Qualifier 依赖的测试:

1
2
3
4
5
6
7
8
9
10
11
12
// TODO include qualifier with dependency
static class InjectField {
@Inject
@Named("ChosenOne") Dependency dependency;
}
@Test
public void should_include_dependency_with_qualifier() {
InjectionProvider<InjectField> provider = new InjectionProvider<>(InjectField.class);

assertArrayEquals(new ComponentRef[]{ComponentRef.of(Dependency.class, new NamedLiteral("ChosenOne"))},
provider.getDependencies().toArray(ComponentRef[]::new));
}

实现:

image-20240819204156897

inject with qualifier

实现注入被 Qualifier 标记的依赖的功能。

构造器注入被 Qualifier 标记的依赖

通过构造函数注入的方式,注入被 Qualifier 标记的组件,构造以下测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// TODO inject with qualifier
@Test
public void should_inject_dependency_with_qualifier_via_constructor() {
InjectionProvider<InjectConstructor> provider = new InjectionProvider<>(InjectConstructor.class);

InjectConstructor instance = provider.get(context);
assertSame(dependency, instance.dependency);
}

static class InjectConstructor {
Dependency dependency;
@Inject
public InjectConstructor(@Named("ChosenOne") Dependency dependency) {
this.dependency = dependency;
}
}

运行测试,测试会通过。

这是假阴性。

因为使用了测试替身,在测试替身构造的结果和生产代码都没发生改变的情况下,就不会出现错误。就会导致假阴性。

image-20240820092043644

无法直接修改 setUp 中的行为,因为现在直接修改的话,会产生大量的错误。

需要在当前测试中,重置 sutUp 中的行为(预期),也就是重新设置测试夹具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Nested
class WithQualifier {

@BeforeEach
public void setUp() {
Mockito.reset(context);
Mockito.when(context.get(eq(ComponentRef.of(Dependency.class, new NamedLiteral("ChosenOne"))))).thenReturn(Optional.of(dependency));
}
// TODO inject with qualifier
@Test
public void should_inject_dependency_with_qualifier_via_constructor() {
InjectionProvider<InjectConstructor> provider = new InjectionProvider<>(InjectConstructor.class);

InjectConstructor instance = provider.get(context);
assertSame(dependency, instance.dependency);
}
}

此时运行测试,将不通过。

原因是,使用 get 方法获取组件时,会去容器中查找组件的依赖,并赋值到组件中。但是目前去容器中查找的方法,并没有携带 Qualifier 标记的信息,只包含了类型的信息。

image-20240820093625332

image-20240820093829851

实现,即查找依赖时需要从容器中获取带有 qualifier

1
context.get(ComponentRef.of(type, qualifier))

提取一个新方法,在其中获取参数的注解,并传递给 toDependency 方法。

image-20240820100400074

因为前面实现了一个获取方法参数的注解的功能,这里提取一个方法:

1
2
3
4
private static Annotation getQualifier(Parameter parameter) {
return Arrays.stream(parameter.getAnnotations()).filter(a -> a.annotationType().isAnnotationPresent(Qualifier.class))
.findFirst().orElse(null);
}

运行测试,通过。

方法注入被 Qualifier 标记的依赖

通过构造函数注入的方式,注入被 Qualifier 标记的组件,构造以下测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@BeforeEach
public void setUp() {
Mockito.reset(context);
Mockito.when(context.get(eq(ComponentRef.of(Dependency.class, new NamedLiteral("ChosenOne"))))).thenReturn(Optional.of(dependency));
}

// inject with qualifier
@Test
public void should_inject_dependency_with_qualifier_via_method() {
InjectionProvider<InjectMethod> provider = new InjectionProvider<>(InjectMethod.class);

InjectMethod instance = provider.get(context);
assertSame(dependency, instance.dependency);
}

static class InjectMethod {
Dependency dependency;

@Inject
void install(@Named("ChosenOne") Dependency dependency) {
this.dependency = dependency;
}
}

运行测试,通过。因为方法注入和构造函数注入的内部实现是一样的,所以不需要修改生产代码。

字段注入被 Qualifier 标记的依赖

构造测试

1
2
3
4
5
6
7
8
9
10
11
12
13
@BeforeEach
public void setUp() {
Mockito.reset(context);
Mockito.when(context.get(eq(ComponentRef.of(Dependency.class, new NamedLiteral("ChosenOne"))))).thenReturn(Optional.of(dependency));
}

@Test
public void should_inject_dependency_with_qualifier_via_field() {
InjectionProvider<InjectField> provider = new InjectionProvider<>(InjectField.class);

InjectField instance = provider.get(context);
assertSame(dependency, instance.dependency);
}

同理需要修改 toDependency 的参数:

Snipaste_2024-08-20_10-39-36

因为前面实现过获取 Field 中的注解的功能,这里提取一个方法。

1
2
3
4
private static Annotation getQualifier(Field field) {
return Arrays.stream(field.getAnnotations()).filter(a -> a.annotationType().isAnnotationPresent(Qualifier.class))
.findFirst().orElse(null);
}

非法的 Qualifier 注入

非法注入的情况是指,同时使用两个 Qualifier 注解标注同一个依赖时的情况。

根据 JSR330 规范,同一个组件可以被多个 Qualifier 标记注册多个,但是依赖只能指定一个 Qualifier 注解。

构造器非法注入

构造测试:

1
2
3
4
5
6
7
8
9
10
11
12
// TODO throw illegal component if illegal qualifier given to injection point
static class MultiQualifierInjectConstructor {
@Inject
public MultiQualifierInjectConstructor(@Named("ChosenOne") @AnotherOne Dependency dependency) {
}
}
@Test
public void should_throw_exception_if_multi_qualifier_given_to_inject_constructor() {
// 需要在创建时检查依赖是否合法
assertThrows(IllegalComponentException.class,
() -> new InjectionProvider<>(MultiQualifierInjectConstructor.class));
}

运行测试,不通过,即不会抛出异常,我们需要的是抛出异常。

因为当前并没有在创建 InjectionProvider 时检查依赖是否合法,也没有去校验依赖被多个 Qualifier 标注的情况,目前只取其中一个 Qualifier 注解注册。

所以需要修改检查依赖的代码,并将检查依赖的代码加入到构造 Provider 时。

修改获取依赖参数上的 Qualifier 注解,判断注解的数量:

1
2
3
4
5
6
private static Annotation getQualifier(Parameter parameter) {
List<Annotation> qualifiers = Arrays.stream(parameter.getAnnotations())
.filter(a -> a.annotationType().isAnnotationPresent(Qualifier.class)).toList();
if (qualifiers.size() > 1) throw new IllegalComponentException();
return qualifiers.stream().findFirst().orElse(null);
}

并在构造函数中获取依赖(获取依赖时,会调用 getQualifier 方法,即会检查依赖上的 Qualifier 是否不合法)

image-20240820111434672

运行测试,通过。

方法非法注入

构造测试:

1
2
3
4
5
6
7
8
9
10
11
12
static class MultiQualifierInjectMethod {
Dependency dependency;
@Inject
public void install(@Named("ChosenOne") @AnotherOne Dependency dependency) {
}
}
@Test
public void should_throw_exception_if_multi_qualifier_given_to_inject_method() {
// 需要在创建时检查依赖是否合法
assertThrows(IllegalComponentException.class,
() -> new InjectionProvider<>(MultiQualifierInjectMethod.class));
}

运行测试,直接通过。因为方法注入的内部实现和构造器注入的内部实现一致,不需要修改生产代码。

字段非法注入

构造测试:

1
2
3
4
5
6
7
8
9
10
static class MultiQualifierInjectField {
@Inject
@Named("ChosenOne") @AnotherOne Dependency dependency;
}
@Test
public void should_throw_exception_if_multi_qualifier_given_to_inject_field() {
// 需要在创建时检查依赖是否合法
assertThrows(IllegalComponentException.class,
() -> new InjectionProvider<>(MultiQualifierInjectField.class));
}

同理,也需要修改 getQualifier 方法:

1
2
3
4
5
6
private static Annotation getQualifier(Field field) {
List<Annotation> qualifiers = Arrays.stream(field.getAnnotations())
.filter(a -> a.annotationType().isAnnotationPresent(Qualifier.class)).toList();
if (qualifiers.size() > 1) throw new IllegalComponentException();
return qualifiers.stream().findFirst().orElse(null);
}

运行测试,通过。

重构

合并两个 getQualifier

我们这里目前有两个内部实现完全一样的方法,分别是:getQualifier(Field field) 、getQualifier(Parameter parameter)

因为 Field 和 Parameter 实现了共同的 AnnotatedElement 接口,可以将这两个方法合并为一个。

1
2
3
4
5
6
private static Annotation getQualifier(AnnotatedElement parameter) {
List<Annotation> qualifiers = Arrays.stream(parameter.getAnnotations())
.filter(a -> a.annotationType().isAnnotationPresent(Qualifier.class)).toList();
if (qualifiers.size() > 1) throw new IllegalComponentException();
return qualifiers.stream().findFirst().orElse(null);
}

简化 toDependency 代码,减少调用层级

image-20240820141337390

image-20240820141638666

dependency 被获取了两次,属于重复

image-20240820142512043

这是最难发现和消除的坏味道。这可能意味着我们对模型概念的封装不足。

模型封装

先创建一个封装类,将注入器和依赖组合在一起。

1
2
3
static record Injectable<Element extends AccessibleObject>(Element element, ComponentRef<?>[] required){

}

范型继承自 AccessibleObject,因为其子类正好包含当前需要的 Constructor、Field、Method,并使用 ComponentRef 数组表示依赖。

image-20240820143332218

封装构造器和依赖

使用 injectableConstructor 来代替原有的 injectConstructor,同样是先新增功能再替换:

1
2
private Constructor<T> injectConstructor;
private Injectable<Constructor<T>> injectableConstructor;

new InjectionProvider 时,同时为 injectableConstructor 赋值,需要先获取 constructor 和 构造器依赖的 dependency,获取这两数据的代码,在现有的代码里面已经存在了,直接复用就可以。

1
2
3
4
Constructor<T> constructor = getInjectConstructor(component);
ComponentRef<?>[] constructorDependencies = Arrays.stream(constructor.getParameters()).map(InjectionProvider::toComponentRef)
.toArray(ComponentRef<?>[]::new);
this.injectableConstructor = new Injectable<>(constructor, constructorDependencies);

接着,需要找到 injectConstructor 在哪里使用,并使用 injectableConstructor 替换掉 injectConstructor。

image-20240820145941249

并且以上 getDependencies 方法中的部分还需要替换为如下,避免重复获取依赖,可以直接从 injectableConstructor 获取:

image-20240820150159534

同样的,newInstance 也不需要重新获取依赖,可以直接从 injectable 中获取。

在 Injectable 中增加一个方法,直接从 context 容器中查找依赖,并返回为 newInstance 所需的数组。

1
2
3
4
5
static record Injectable<Element extends AccessibleObject>(Element element, ComponentRef<?>[] required){
Object[] toDependencies(Context context) {
return Arrays.stream(required).map(context::get).map(Optional::get).toArray();
}
}

image-20240820151251760

修改后,可以移除 injectConstructor。

再将 injectableConstructor 重名回 injectConstructor。

封装方法注入器和依赖

同样的,使用 injectableMethods 替换掉 injectMethods:

1
2
private List<Injectable<Method>> injectableMethods;
private List<Method> injectMethods;

因为构造器和普通方法都是属于方法,并且因为 Constructor 和 Method 都有一个名为 Executable 的父类,所以将构造每一个 Executable 的 Injectable 的代码提取为一个方法,用于同时创建 Constructor 和 Method 的 Injectable 封装类:

1
2
3
4
5
private static <Element extends Executable> Injectable<Element> getInjectable(Element method) {
ComponentRef<?>[] dependencies = Arrays.stream(method.getParameters()).map(InjectionProvider::toComponentRef)
.toArray(ComponentRef<?>[]::new);
return new Injectable<>(method, dependencies);
}

image-20240820153558513

那么 injectableMethods 的赋值语句就如下所示:

1
this.injectableMethods = getInjectMethods(component).stream().map(InjectionProvider::getInjectable).toList();

接着找到,injectMethods 在哪里被使用

之前的代码有一处有问题的地方,这里应该先使用 injectMethods 替换掉,不然就在 new InjectionProvider 时,重复调用了两次 getInjectMethods 方法

image-20240820154445155

修为为:

image-20240820154815243

image-20240820154941586

修改为:

image-20240820155126273

image-20240820160107174

修改为:

Snipaste_2024-08-20_16-01-58

移除掉 injectMethods 字段

再将 injectableMethods 重命名回 injectMethods

简单重构

先将当前的 getInjectable 方法移动到 Injectable 中,并重命名为 of,这就是一个工厂方法:

image-20240820161115163

封装字段注入器和依赖

使用 injectableFields 替换掉 injectFields

1
2
3
private List<Injectable<Field>> injectableFields;

private List<Field> injectFields;

在 Injectable 中新建一个工厂方法:

image-20240820161528106

那么 injectableFields 的赋值语句为:

1
this.injectableFields = getInjectFields(component).stream().map(Injectable::of).toList();

接着查找 injectFields 在哪里被使用,并修改为 injectableFields

image-20240820161933112

image-20240820162458834

image-20240820162837117

更好的实现是直接从 Injectable 中获取依赖,避免计算:

Snipaste_2024-08-20_16-32-49

移除 injectFields 字段,

再将 injectableFields 重命名为 injectFields

至此,dependencies 字段已无用,也可以删除,并移除掉一些不再使用的方法。

那么,InjectionProvider 的字段和构造函数就变为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Injectable<Constructor<T>> injectConstructor;
private List<Injectable<Method>> injectMethods;
private List<Injectable<Field>> injectFields;

public InjectionProvider(Class<T> component) {
if (Modifier.isAbstract(component.getModifiers())) throw new IllegalComponentException();

Constructor<T> constructor = getInjectConstructor(component);
this.injectConstructor = Injectable.of(constructor);
this.injectMethods = getInjectMethods(component).stream().map(Injectable::of).toList();
this.injectFields = getInjectFields(component).stream().map(Injectable::of).toList();

if (injectFields.stream().map(Injectable::element).anyMatch(f -> Modifier.isFinal(f.getModifiers())))
throw new IllegalComponentException();
if (injectMethods.stream().map(Injectable::element).anyMatch(m -> m.getTypeParameters().length != 0))
throw new IllegalComponentException();

}

整理代码

观察发现,这几个方法只会在 Injectable 中被调用,可以将这几个方法移动到 Injectable 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static ComponentRef<?> toComponentRef(Field field) {
Annotation qualifier = getQualifier(field);
return ComponentRef.of(field.getGenericType(), qualifier);
}

private static ComponentRef<?> toComponentRef(Parameter parameter) {
Annotation qualifier = getQualifier(parameter);
return ComponentRef.of(parameter.getParameterizedType(), qualifier);
}

private static Annotation getQualifier(AnnotatedElement parameter) {
List<Annotation> qualifiers = Arrays.stream(parameter.getAnnotations())
.filter(a -> a.annotationType().isAnnotationPresent(Qualifier.class)).toList();
if (qualifiers.size() > 1) throw new IllegalComponentException();
return qualifiers.stream().findFirst().orElse(null);
}

可以使用 Move Member 的重构方式移动:

image-20240820171152367

image-20240820171834398

将这些表达式都提取为方法:

image-20240820172735344

将getDependencies 方法:

1
2
3
4
5
6
7
8
@Override
public List<ComponentRef<?>> getDependencies() {
return Stream.concat(
Stream.concat(Arrays.stream(injectConstructor.required()),
injectFields.stream().map(Injectable::required).flatMap(Arrays::stream)),
injectMethods.stream().map(Injectable::required).flatMap(Arrays::stream))
.toList();
}

简化为,先拼接为 Injectable 的 Stream,再 Map

1
2
3
4
5
@Override
public List<ComponentRef<?>> getDependencies() {
return Stream.concat(Stream.concat(Stream.of(injectConstructor), injectFields.stream()), injectMethods.stream())
.flatMap(i -> Arrays.stream(i.required)).toList();
}

测试文档化重组

Provider 和 Qualifier 的测试

在 ContextText.TypeBinding.WithQualifier 中增加 Provider 和 Qualifier 相关联的两个测试,用于检查

  • 获取被 Qualifier 的组件的 Provider
  • 获取无对应 Qualifier 标记的组件的Provider是,Provider 应为空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void should_retrieve_bind_type_as_provider() {
TestComponent component = new TestComponent() {
};
config.bind(TestComponent.class, component, new NamedLiteral("ChosenOne"), new AnotherOneLiteral());
Context context = config.getContext();
Optional<Provider<TestComponent>> provider =
context.get(new ComponentRef<Provider<TestComponent>>(new AnotherOneLiteral()) {});

assertTrue(provider.isPresent());
}

@Test
public void should_retrieve_empty_if_no_matched_qualifier() {
TestComponent component = new TestComponent() {
};
config.bind(TestComponent.class, component);
Context context = config.getContext();
Optional<Provider<TestComponent>> provider =
context.get(new ComponentRef<Provider<TestComponent>>(new NamedLiteral("ChosenOne")) {});

assertTrue(provider.isEmpty());
}

需要在 ComponentRef 中增加构造方法:

1
2
3
4
public ComponentRef(Annotation qualifier) {
Type type = ((ParameterizedType) (getClass().getGenericSuperclass())).getActualTypeArguments()[0];
init(type, qualifier);
}

Qualifier 依赖检查参数化

将当前 ContextText.DependencyCheck.WithQualifier 中两个测试修改为参数化的测试:

未找到被 Qualifier 标记的依赖时,抛出依赖不存在的异常的的参数化测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// dependency missing if qualifier not match
@ParameterizedTest
@MethodSource
public void should_throw_exception_if_dependency_with_qualifier_not_found(Class<? extends TestComponent> componentType) {

config.bind(Dependency.class, new Dependency() {
});
config.bind(TestComponent.class, componentType, new NamedLiteral("ChosenOne"));

DependencyNotFoundException exception =
assertThrows(DependencyNotFoundException.class, () -> config.getContext());

assertEquals(new Component(TestComponent.class, new NamedLiteral("ChosenOne")), exception.getComponent());
assertEquals(new Component(Dependency.class, new AnotherOneLiteral()), exception.getDependency());

}
public static Stream<Arguments> should_throw_exception_if_dependency_with_qualifier_not_found() {
return Stream.of(
Arguments.of(Named.of("Constructor Injection with Qualifier", DependencyCheck.WithQualifier.InjectConstructor.class)),
Arguments.of(Named.of("Field Injection with Qualifier", DependencyCheck.WithQualifier.InjectField.class)),
Arguments.of(Named.of("Method Injection with Qualifier", DependencyCheck.WithQualifier.InjectMethod.class)),
Arguments.of(Named.of("Provider Constructor Injection with Qualifier", DependencyCheck.WithQualifier.InjectConstructorProvider.class)),
Arguments.of(Named.of("Provider Field Injection with Qualifier", DependencyCheck.WithQualifier.InjectFieldProvider.class)),
Arguments.of(Named.of("Provider Method Injection with Qualifier", DependencyCheck.WithQualifier.InjectMethodProvider.class))
);
}

static class InjectConstructor implements TestComponent {
@Inject
public InjectConstructor(@AnotherOne Dependency dependency) {
}
}

static class InjectField implements TestComponent {
@Inject
@AnotherOne Dependency dependency;
}
static class InjectMethod implements TestComponent {
Dependency dependency;

@Inject
public void install(@AnotherOne Dependency dependency) {
this.dependency = dependency;
}
}

static class InjectConstructorProvider implements TestComponent {
@Inject
public InjectConstructorProvider(@AnotherOne Provider<Dependency> dependency) {
}
}

static class InjectFieldProvider implements TestComponent {
@Inject
@AnotherOne
Provider<Dependency> dependency;
}

static class InjectMethodProvider implements TestComponent {
Dependency dependency;

@Inject
public void install(@AnotherOne Provider<Dependency> dependency) {
this.dependency = dependency.get();
}
}

包含 Qualifier 标注的依赖的循环依赖检查的参数化测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// check cyclic dependencies with qualifier
// A -> @AnotherOne A -> @Named A
@ParameterizedTest(name = "{1} -> @AnotherOne({0}) -> @Named(\"ChosenOne\") not cyclic dependencies")
@MethodSource
public void should_not_throw_exception_if_component_with_same_type_tagged_with_different_qualifier(Class<? extends Dependency> anotherDependencyType,
Class<? extends Dependency> notCyclicDependencyType) {
Dependency instance = new Dependency() {
};
config.bind(Dependency.class, instance, new NamedLiteral("ChosenOne"));
config.bind(Dependency.class, anotherDependencyType, new AnotherOneLiteral());
config.bind(Dependency.class, notCyclicDependencyType);

assertDoesNotThrow(() -> config.getContext());
}

public static Stream<Arguments> should_not_throw_exception_if_component_with_same_type_tagged_with_different_qualifier() {
List<Arguments> arguments = new ArrayList<>();
for (Named anotherDependency : List.of(Named.of("Constructor Injection", AnotherOneDependencyConstructor.class),
Named.of("Field Injection", DependencyCheck.WithQualifier.AnotherOneDependencyField.class),
Named.of("Method Injection", DependencyCheck.WithQualifier.AnotherOneDependencyMethod.class))) {
for (Named notCyclicDependency : List.of(Named.of("Constructor Injection", NotCyclicDependencyConstructor.class),
Named.of("Field Injection", DependencyCheck.WithQualifier.NotCyclicDependencyField.class),
Named.of("Method Injection", DependencyCheck.WithQualifier.NotCyclicDependencyMethod.class))) {
arguments.add(Arguments.of(anotherDependency, notCyclicDependency));
}
}
return arguments.stream();
}

static class AnotherOneDependencyConstructor implements Dependency {
@Inject
public AnotherOneDependencyConstructor(@jakarta.inject.Named("ChosenOne") Dependency dependency) {
}
}
static class AnotherOneDependencyField implements Dependency {
@Inject
@jakarta.inject.Named("ChosenOne") Dependency dependency;

}
static class AnotherOneDependencyMethod implements Dependency {
@Inject
public void install(@jakarta.inject.Named("ChosenOne") Dependency dependency) {
}
}

static class NotCyclicDependencyConstructor implements Dependency {
@Inject
public NotCyclicDependencyConstructor(@AnotherOne Dependency dependency) {
}
}
static class NotCyclicDependencyField implements Dependency {
@Inject
@AnotherOne Dependency dependency;
}
static class NotCyclicDependencyMethod implements Dependency {
@Inject
public void install(@AnotherOne Dependency dependency) {
}
}

Singleton-生命周期管理

Singleton 生命周期

  • 注册组件时,可额外指定是否为 Singleton
  • 注册组件时,可从类对象上提取 Singleton 标注
  • 对于包含 Singleton 标注的组件,在容器范围内提供唯一实例
  • 容器组件默认不是 Single 生命周期

基于当前的架构现状,可以将任务转换为如下 todo list

1
2
3
4
5
6
// TODO default scope should not be singleton
// TODO bind component as singleton scoped
// TODO bind component with qualifiers as singleton scoped
// TODO get scope from component class
// TODO get scope from component with qualifiers
// TODO bind component with customize scope annotation

将任务放置在 ContextText.TypeBinding.WithScope 中:

image-20240820192553665

默认非单例

默认非单例模式,目前默认就是非单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
static class NotSingletonComponent {
}
@Test
public void should_not_be_singleton_scope_by_default() {
config.bind(NotSingletonComponent.class, NotSingletonComponent.class);
Context context = config.getContext();

NotSingletonComponent component1 = context.get(ComponentRef.of(NotSingletonComponent.class)).get();
NotSingletonComponent component2 = context.get(ComponentRef.of(NotSingletonComponent.class)).get();

assertNotSame(component1, component2);
}

被 Qualifier 标注的依赖,默认也是非单例的,在 WithScope 中再创建一个 WithQualifier 分组,测试带有 Qualifier 标注的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Nested
public class WithQualifier {
@Test
public void should_not_be_singleton_scope_by_default() {
config.bind(NotSingletonComponent.class, NotSingletonComponent.class, new AnotherOneLiteral());
Context context = config.getContext();

NotSingletonComponent component1 = context.get(ComponentRef.of(NotSingletonComponent.class, new AnotherOneLiteral())).get();
NotSingletonComponent component2 = context.get(ComponentRef.of(NotSingletonComponent.class, new AnotherOneLiteral())).get();

assertNotSame(component1, component2);
}
}

绑定组件为单例模式

构建测试

新建一个 SingletonLiteral,用于作为 bind 的参数,来指定将当前组件注册为单例模式:

1
2
3
4
5
6
record SingletonLiteral() implements jakarta.inject.Singleton {
@Override
public Class<? extends Annotation> annotationType() {
return jakarta.inject.Singleton.class;
}
}

构造测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
// TODO bind component as singleton scoped
static class SingletonComponent {
}
@Test
public void should_bind_component_as_singleton_scope() {
config.bind(SingletonComponent.class, SingletonComponent.class, new SingletonLiteral());
Context context = config.getContext();

SingletonComponent component1 = context.get(ComponentRef.of(SingletonComponent.class)).get();
SingletonComponent component2 = context.get(ComponentRef.of(SingletonComponent.class)).get();

assertSame(component1, component2);
}

运行测试,会抛出一个 IllegalComponentException,抛出的位置是:

image-20240820194638100

异常的原因是目前只支持 Qualifier 注解。

修改,让其同时支持 Qualifier 和 Scope 两种注解:

image-20240820195127187

运行测试,现在还是异常:

1
java.util.NoSuchElementException: No value present

不符合我们预期的 assertSame 的情况。我们预期要么相等要么不相等,不应该是不存在的情况。

原因依然在 bind 方法处,这里将 Scope 注解也当成了 Qualifier :

Snipaste_2024-08-20_19-55-36

修改,过滤掉非 Qualifier 的注解,并注意当 qualifiers 为空时也需要注册:

image-20240820200136437

运行测试,异常,现在是我们期望的 Same 或 NotSame 异常:

1
2
Expected :world.nobug.tdd.di.ContextTest$TypeBinding$WithScope$SingletonComponent@10b892d5
Actual :world.nobug.tdd.di.ContextTest$TypeBinding$WithScope$SingletonComponent@3d3f761a

说明目前还是非单例的。

实现

使用一个 Proxy 或者 Decorator Pattern,包装 InjectionProvider 修改其创建行为,根据条件创建。

新建一个 ComponentProvider 的子类,其中缓存一个实例,每次 get 实例时先从缓存中取,缓存缺失时才新建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static class SingletonProvider<T> implements ComponentProvider<T> {
T instance;
ComponentProvider<T> provider;

SingletonProvider(ComponentProvider<T> provider) {
this.provider = provider;
}

@Override
public T get(Context context) {
if (instance == null) instance = provider.get(context);
return instance;
}
}

image-20240820201833198

判断,当 Scope 存在时,使用 SingletonProvider 代替 InjectionProvider。

运行测试,通过。

同样的,构造测试,检查被 Scope 和 Qualifier 注解同时标记的依赖是否支持单例:

1
2
3
4
5
6
7
8
9
10
11
// bind component with qualifiers as singleton scoped
@Test
public void should_bind_component_as_singleton_scope() {
config.bind(SingletonComponent.class, SingletonComponent.class, new SingletonLiteral(), new AnotherOneLiteral());
Context context = config.getContext();

SingletonComponent component1 = context.get(ComponentRef.of(SingletonComponent.class, new AnotherOneLiteral())).get();
SingletonComponent component2 = context.get(ComponentRef.of(SingletonComponent.class, new AnotherOneLiteral())).get();

assertSame(component1, component2);
}

运行测试,通过。

支持 @Singleton

如果一个类被 @Singleton 标记,那么在绑定时可以不指定 Scope,对于这样的类默认为单例模式。

这个是 JSR330 规范中的一个用例,查看 Scope 注解的注释:

Snipaste_2024-08-20_20-37-35

1
2
3
4
@Singleton
static class SingletonComponentAnnotated {

}

构造测试

1
2
3
4
5
6
7
8
9
10
11
12
// TODO get scope from component class
@Test
public void should_retrieve_scope_annotation_from_component() {
// 未指定 scope 时,默认将标记了 Singleton 的类作为单例
config.bind(SingletonComponentAnnotated.class, SingletonComponentAnnotated.class);
Context context = config.getContext();

SingletonComponentAnnotated component1 = context.get(ComponentRef.of(SingletonComponentAnnotated.class)).get();
SingletonComponentAnnotated component2 = context.get(ComponentRef.of(SingletonComponentAnnotated.class)).get();

assertSame(component1, component2);
}

运行测试,异常:

1
2
Expected :world.nobug.tdd.di.ContextTest$TypeBinding$WithScope$SingletonComponentAnnotated@3a4b0e5d
Actual :world.nobug.tdd.di.ContextTest$TypeBinding$WithScope$SingletonComponentAnnotated@10b892d5

实现

目前使用的 bind 方法是:

image-20240820204448777

修改为:

image-20240820204544810

因为,重载的 bind 方法中现在已经支持 Annotation... annotations 参数为空的情况了。

同样的,构造组件同时被 Qualifier 标注时的测试:

1
2
3
4
5
6
7
8
9
10
11
12
// TODO get scope from component with qualifiers
@Test
public void should_retrieve_scope_annotation_from_component() {
// 未指定 scope 时,默认将标记了 Singleton 的类作为单例
config.bind(SingletonComponentAnnotated.class, SingletonComponentAnnotated.class, new AnotherOneLiteral());
Context context = config.getContext();

SingletonComponentAnnotated component1 = context.get(ComponentRef.of(SingletonComponentAnnotated.class, new AnotherOneLiteral())).get();
SingletonComponentAnnotated component2 = context.get(ComponentRef.of(SingletonComponentAnnotated.class, new AnotherOneLiteral())).get();

assertSame(component1, component2);
}

运行测试,异常:

1
2
Expected :world.nobug.tdd.di.ContextTest$TypeBinding$WithScope$SingletonComponentAnnotated@2ddc8ecb
Actual :world.nobug.tdd.di.ContextTest$TypeBinding$WithScope$SingletonComponentAnnotated@229d10bd

原因是目前并没有取组件上标记的 @Singleton , 只取了 bind 方法参数中的 Scope 注解:

image-20240820205249652

增加从组件上获取 Scope 注解的实现,当无法从参数中获取到 Scope 时,尝试将 scope 赋值为从组件上获取到的 Scope 注解:

Snipaste_2024-08-20_20-59-15

运行测试,通过。

小 bug

image-20240820210355336

这里不应该从 type 取注解,而是应该从 implementation 上取。

依赖检查

依赖不存在

1
2
3
4
5
6
7
8
9
10
11
12
13
// dependencies not exist
@ParameterizedTest
@MethodSource
public void should_throw_exception_if_dependency_not_found(Class<? extends TestComponent> componentType) {
config.bind(TestComponent.class, componentType);

DependencyNotFoundException exception = assertThrows(DependencyNotFoundException.class, () -> {
config.getContext();
});

assertEquals(Dependency.class, exception.getDependency().type());
assertEquals(TestComponent.class, exception.getComponent().type());
}

增加一个被 @Singleton 标记的测试用例:

image-20240821093308594

1
2
3
4
5
@Singleton
static class MissingDependencyScope implements TestComponent {
@Inject
Dependency dependency;
}

运行测试:

1
org.opentest4j.AssertionFailedError: Expected world.nobug.tdd.di.DependencyNotFoundException to be thrown, but nothing was thrown.

我们预期应该抛出依赖不存在的异常,但是这里并没有抛出。说明在 config.getContext() 中 checkDependencies 时并没有获取到该组件的正确依赖。因为当前组件被构建成 SingletonProvider 并且当前并没有实现 getDependencies 方法,实现该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static class SingletonProvider<T> implements ComponentProvider<T> {
T instance;
ComponentProvider<T> provider;

SingletonProvider(ComponentProvider<T> provider) {
this.provider = provider;
}

@Override
public T get(Context context) {
if (instance == null) {
instance = provider.get(context);
}
return instance;
}

@Override
public List<ComponentRef<?>> getDependencies() {
return provider.getDependencies();
}
}

运行测试,通过。

再增加一个 Provider包装的依赖的测试:

1
Arguments.of(Named.of("Provider Scope", DependencyCheck.MissingDependencyProviderScope.class))
1
2
3
4
5
@Singleton
static class MissingDependencyProviderScope implements TestComponent {
@Inject
Provider<Dependency> dependency;
}

运行测试,通过。

循环依赖

对于 Scope 的循环依赖是一个可选项。

这种情况有一个很典型的优化:当两个组件都是构造函数相互依赖时,并且其中有一个是 Singleton 的,那么这个循环依赖是不成立的,因为并不是每次都需要构造新对象。

自定义 Scope 注解

自定义一个 Scope 注解 Pooled,来表示指定数量的多例:

1
2
3
4
5
6
7
8
9
10
11
@Scope
@Documented
@Retention(RUNTIME)
@interface Pooled {}

record PooledLiteral() implements Pooled {
@Override
public Class<? extends Annotation> annotationType() {
return Pooled.class;
}
}

同样的,需要定义一个 Provider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class PooledProvider<T> implements ContextConfig.ComponentProvider<T> {
static int MAX = 2;

private int current;
private List<T> instancePool = new ArrayList<>();
ContextConfig.ComponentProvider<T> provider;

PooledProvider(ContextConfig.ComponentProvider<T> provider) {
this.provider = provider;
}

@Override
public T get(Context context) {
if (instancePool.size() < MAX) instancePool.add(provider.get(context));
return instancePool.get(current++ % MAX);
}

@Override
public List<ComponentRef<?>> getDependencies() {
return provider.getDependencies();
}
}

构造测试

1
2
3
4
5
6
7
8
9
10
11
12
13
// TODO bind component with customize scope annotation
static class PooledComponent {

}
@Test
public void should_bind_component_with_customize_scope_annotation() {
config.bind(PooledComponent.class, PooledComponent.class, new PooledLiteral());
Context context = config.getContext();

List<PooledComponent> instances = IntStream.range(0, 5)
.mapToObj(i -> context.get(ComponentRef.of(PooledComponent.class)).get()).toList();
assertEquals(PooledProvider.MAX, new HashSet<>(instances).size());
}

运行测试,将失败:

1
2
Expected :2
Actual :1

因为我们当前将所有 Scope 的注解都设置为单例。

实现

我们预期的方式是,为 config 配置指定的 Scope 应该如何创建对应的 Provider:

image-20240821103932822

创建该方法:

1
2
public <ScopeType extends Annotation> void scope(Class<ScopeType> scopeType, Function<ComponentProvider<?>, ComponentProvider<?>> provider) {
}

新建一个字段来保存 scope 信息:

1
private Map<Class<?>, Function<ComponentProvider<?>, ComponentProvider<?>>> scopes = new HashMap<>();

那么 scope 方法的实现为:

1
2
3
public <ScopeType extends Annotation> void scope(Class<ScopeType> scopeType, Function<ComponentProvider<?>, ComponentProvider<?>> provider) {
scopes.put(scopeType, provider);
}

然后还需要新建一个默认构造函数,并在其中初始化默认的 Scope 的 Provider 方法:

1
2
3
public ContextConfig() {
scopes.put(Singleton.class, SingletonProvider::new);
}

修改 bind 方法中获取 scope 的 provider 的实现:

1
if (scope.isPresent()) provider = scopes.get(scope.get().annotationType()).apply(provider);

运行测试,通过。

重构

重构之前,先将 ComponentProvider 和 SingletonProvider 移动到最外层。

为下面的参数定义一个函数式接口,简化代码:

Snipaste_2024-08-21_11-10-20

Snipaste_2024-08-21_11-13-29

1
2
3
interface ScopeProvider {
ComponentProvider<?> create(ComponentProvider<?> provider);
}

对应的修改后:

image-20240821111855275

image-20240821111943949

重构简化 bind 方法

目前,这个 bind 方法中的实现比较复杂:

image-20240821114336444

分析,这个代码中主要是对不同类型的 annotations 参数进行处理,目前这个参数中包含的的类型有:

  • Scope
  • Qualifier
  • 其他

我们要做的就是将 annotations 根据 Scope、Qualifier 和 其他来进行分类。

这里我们可以将 “其他” 这个类别用一个自定义的注解表示:

1
2
3
private @interface Illegal {

}

并定义一个分类函数:

1
2
3
4
5
6
7
private Class<? extends Annotation> typeOf(Annotation annotation) {
Class<? extends Annotation> type = annotation.annotationType();
return Stream.of(Qualifier.class, Scope.class)
.filter(type::isAnnotationPresent)
.findFirst()
.orElse(Illegal.class);
}

那么分组函数就是:

1
2
Map<Class<?>, List<Annotation>> annotationGroups =
Arrays.stream(annotations).collect(Collectors.groupingBy(this::typeOf, Collectors.toList()));

然后通过使用分组的数据来简化后面的代码逻辑。

首先,是对 Illegal 情况的判断:

image-20240821134009469

简化为:

1
if (annotationGroups.containsKey(Illegal.class)) throw new IllegalComponentException();

Scope Sad Path

增加几个关于 Scope 的 sad path

1
2
3
// TODO multi scope provided
// TODO multi scope annotated
// TODO undefined scope

注册时为组件设置多个 scope,构造测试:

1
2
3
4
5
6
7
8
// TODO multi scope provided
@Test
public void should_throw_exception_if_multi_scope_provided() {
config.scope(Pooled.class, PooledProvider::new);
config.scope(Singleton.class, SingletonProvider::new);
assertThrows(IllegalComponentException.class,
() -> config.bind(PooledComponent.class, PooledComponent.class, new PooledLiteral(), new SingletonLiteral()));
}

运行测试,不通过。说明并没有判断设置多个 Scope 的情况。

增加判断:

Snipaste_2024-08-21_14-20-48

多个 Scope 注解标注同一个组件时

1
2
3
4
5
6
7
8
9
10
11
// multi scope annotated
@Singleton @Pooled
static class MultiScopeAnnotatedComponent {

}
@Test
public void should_throw_exception_if_multi_scope_annotated() {
config.scope(Pooled.class, PooledProvider::new);
assertThrows(IllegalComponentException.class,
() -> config.bind(MultiScopeAnnotatedComponent.class, MultiScopeAnnotatedComponent.class));
}

运行测试,通过。

注册时,设置未定义的 Scope:

1
2
3
4
5
6
// TODO undefined scope
@Test
public void should_throw_exception_if_undefined_scope() {
assertThrows(IllegalComponentException.class,
() -> config.bind(PooledComponent.class, PooledComponent.class, new PooledLiteral()));
}

在这个测试中并没有指定在容器中配置 Pooled 注解如何定义 Provider,也就是没有执行 config.scope(Pooled.class, PooledProvider::new);

运行测试,异常:

1
org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> expected: <world.nobug.tdd.di.IllegalComponentException> but was: <java.lang.NullPointerException>

有一个空指针异常,是因为无法获取 Pooled 注解对应的 Provider,获取到的为 null。

修改实现:

image-20240821144348995

运行测试,通过。

测试覆盖率

Run …. with Coverage 检查代码的测试覆盖率:

image-20240821145440311

image-20240821150535770

虽然我们没有做到 100% 的代码覆盖了,但是我们做到了 100% 的功能覆盖,对于有些行因为语言特性的需求或者其他原因的代码,也没有必要写测试覆盖。

走的更远

可以引入 Jakarta 的 tck 包,来测试当前容器对 JSR330 规范的兼容度,并完善对 JSR330 规范的兼容。