· TDD · 14 min read
TDD 实现 Spring DI 容器 (七) - 生命周期管理
本文探讨了对组件生命周期的支持,解决了 Singleton 的并发安全问题。同时为容器引入了完整的自定义 Scope 作用域扩展接口,圆满达成了兼容 JSR-330 规范的目标。
目录 (Table of Contents)
目录 (Table of Contents)
Singleton-生命周期管理
Singleton 生命周期
- 注册组件时,可额外指定是否为 Singleton
- 注册组件时,可从类对象上提取 Singleton 标注
- 对于包含 Singleton 标注的组件,在容器范围内提供唯一实例
- 容器组件默认不是 Single 生命周期
基于当前的架构现状,可以将任务转换为如下 todo list
1 // TODO default scope should not be singleton
2 // TODO bind component as singleton scoped
3 // TODO bind component with qualifiers as singleton scoped
4 // TODO get scope from component class
5 // TODO get scope from component with qualifiers
6 // TODO bind component with customize scope annotation将任务放置在 ContextText.TypeBinding.WithScope 中:

默认非单例
默认非单例模式,目前默认就是非单例模式:
1 static class NotSingletonComponent {
2 }
3 @Test
4 public void should_not_be_singleton_scope_by_default() {
5 config.bind(NotSingletonComponent.class, NotSingletonComponent.class);
6 Context context = config.getContext();
7
8 NotSingletonComponent component1 = context.get(ComponentRef.of(NotSingletonComponent.class)).get();
9 NotSingletonComponent component2 = context.get(ComponentRef.of(NotSingletonComponent.class)).get();
10
11 assertNotSame(component1, component2);
12 }被 Qualifier 标注的依赖,默认也是非单例的,在 WithScope 中再创建一个 WithQualifier 分组,测试带有 Qualifier 标注的依赖:
1 @Nested
2 public class WithQualifier {
3 @Test
4 public void should_not_be_singleton_scope_by_default() {
5 config.bind(NotSingletonComponent.class, NotSingletonComponent.class, new AnotherOneLiteral());
6 Context context = config.getContext();
7
8 NotSingletonComponent component1 = context.get(ComponentRef.of(NotSingletonComponent.class, new AnotherOneLiteral())).get();
9 NotSingletonComponent component2 = context.get(ComponentRef.of(NotSingletonComponent.class, new AnotherOneLiteral())).get();
10
11 assertNotSame(component1, component2);
12 }
13 }绑定组件为单例模式
构建测试
新建一个 SingletonLiteral,用于作为 bind 的参数,来指定将当前组件注册为单例模式:
1 record SingletonLiteral() implements jakarta.inject.Singleton {
2 @Override
3 public Class<? extends Annotation> annotationType() {
4 return jakarta.inject.Singleton.class;
5 }
6 }构造测试:
1 // TODO bind component as singleton scoped
2 static class SingletonComponent {
3 }
4 @Test
5 public void should_bind_component_as_singleton_scope() {
6 config.bind(SingletonComponent.class, SingletonComponent.class, new SingletonLiteral());
7 Context context = config.getContext();
8
9 SingletonComponent component1 = context.get(ComponentRef.of(SingletonComponent.class)).get();
10 SingletonComponent component2 = context.get(ComponentRef.of(SingletonComponent.class)).get();
11
12 assertSame(component1, component2);
13 }运行测试,会抛出一个 IllegalComponentException,抛出的位置是:

异常的原因是目前只支持 Qualifier 注解。
修改,让其同时支持 Qualifier 和 Scope 两种注解:

运行测试,现在还是异常:
1 java.util.NoSuchElementException: No value present不符合我们预期的 assertSame 的情况。我们预期要么相等要么不相等,不应该是不存在的情况。
原因依然在 bind 方法处,这里将 Scope 注解也当成了 Qualifier :

修改,过滤掉非 Qualifier 的注解,并注意当 qualifiers 为空时也需要注册:

运行测试,异常,现在是我们期望的 Same 或 NotSame 异常:
1 Expected :world.nobug.tdd.di.ContextTest$TypeBinding$WithScope$SingletonComponent@10b892d5
2 Actual :world.nobug.tdd.di.ContextTest$TypeBinding$WithScope$SingletonComponent@3d3f761a说明目前还是非单例的。
实现
使用一个 Proxy 或者 Decorator Pattern,包装 InjectionProvider 修改其创建行为,根据条件创建。
新建一个 ComponentProvider 的子类,其中缓存一个实例,每次 get 实例时先从缓存中取,缓存缺失时才新建:
1 static class SingletonProvider<T> implements ComponentProvider<T> {
2 T instance;
3 ComponentProvider<T> provider;
4
5 SingletonProvider(ComponentProvider<T> provider) {
6 this.provider = provider;
7 }
8
9 @Override
10 public T get(Context context) {
11 if (instance == null) instance = provider.get(context);
12 return instance;
13 }
14 }
判断,当 Scope 存在时,使用 SingletonProvider 代替 InjectionProvider。
运行测试,通过。
同样的,构造测试,检查被 Scope 和 Qualifier 注解同时标记的依赖是否支持单例:
1 // bind component with qualifiers as singleton scoped
2 @Test
3 public void should_bind_component_as_singleton_scope() {
4 config.bind(SingletonComponent.class, SingletonComponent.class, new SingletonLiteral(), new AnotherOneLiteral());
5 Context context = config.getContext();
6
7 SingletonComponent component1 = context.get(ComponentRef.of(SingletonComponent.class, new AnotherOneLiteral())).get();
8 SingletonComponent component2 = context.get(ComponentRef.of(SingletonComponent.class, new AnotherOneLiteral())).get();
9
10 assertSame(component1, component2);
11 }运行测试,通过。
支持 @Singleton
如果一个类被 @Singleton 标记,那么在绑定时可以不指定 Scope,对于这样的类默认为单例模式。
这个是 JSR330 规范中的一个用例,查看 Scope 注解的注释:
1 @Singleton
2 static class SingletonComponentAnnotated {
3
4 }构造测试
1 // TODO get scope from component class
2 @Test
3 public void should_retrieve_scope_annotation_from_component() {
4 // 未指定 scope 时,默认将标记了 Singleton 的类作为单例
5 config.bind(SingletonComponentAnnotated.class, SingletonComponentAnnotated.class);
6 Context context = config.getContext();
7
8 SingletonComponentAnnotated component1 = context.get(ComponentRef.of(SingletonComponentAnnotated.class)).get();
9 SingletonComponentAnnotated component2 = context.get(ComponentRef.of(SingletonComponentAnnotated.class)).get();
10
11 assertSame(component1, component2);
12 }运行测试,异常:
1 Expected :world.nobug.tdd.di.ContextTest$TypeBinding$WithScope$SingletonComponentAnnotated@3a4b0e5d
2 Actual :world.nobug.tdd.di.ContextTest$TypeBinding$WithScope$SingletonComponentAnnotated@10b892d5实现
目前使用的 bind 方法是:

修改为:

因为,重载的 bind 方法中现在已经支持 Annotation... annotations 参数为空的情况了。
同样的,构造组件同时被 Qualifier 标注时的测试:
1 // TODO get scope from component with qualifiers
2 @Test
3 public void should_retrieve_scope_annotation_from_component() {
4 // 未指定 scope 时,默认将标记了 Singleton 的类作为单例
5 config.bind(SingletonComponentAnnotated.class, SingletonComponentAnnotated.class, new AnotherOneLiteral());
6 Context context = config.getContext();
7
8 SingletonComponentAnnotated component1 = context.get(ComponentRef.of(SingletonComponentAnnotated.class, new AnotherOneLiteral())).get();
9 SingletonComponentAnnotated component2 = context.get(ComponentRef.of(SingletonComponentAnnotated.class, new AnotherOneLiteral())).get();
10
11 assertSame(component1, component2);
12 }运行测试,异常:
1 Expected :world.nobug.tdd.di.ContextTest$TypeBinding$WithScope$SingletonComponentAnnotated@2ddc8ecb
2 Actual :world.nobug.tdd.di.ContextTest$TypeBinding$WithScope$SingletonComponentAnnotated@229d10bd原因是目前并没有取组件上标记的 @Singleton , 只取了 bind 方法参数中的 Scope 注解:

增加从组件上获取 Scope 注解的实现,当无法从参数中获取到 Scope 时,尝试将 scope 赋值为从组件上获取到的 Scope 注解:

运行测试,通过。
小 bug

这里不应该从 type 取注解,而是应该从 implementation 上取。
依赖检查
依赖不存在
1 // dependencies not exist
2 @ParameterizedTest
3 @MethodSource
4 public void should_throw_exception_if_dependency_not_found(Class<? extends TestComponent> componentType) {
5 config.bind(TestComponent.class, componentType);
6
7 DependencyNotFoundException exception = assertThrows(DependencyNotFoundException.class, () -> {
8 config.getContext();
9 });
10
11 assertEquals(Dependency.class, exception.getDependency().type());
12 assertEquals(TestComponent.class, exception.getComponent().type());
13 }增加一个被 @Singleton 标记的测试用例:

1 @Singleton
2 static class MissingDependencyScope implements TestComponent {
3 @Inject
4 Dependency dependency;
5 }运行测试:
1 org.opentest4j.AssertionFailedError: Expected world.nobug.tdd.di.DependencyNotFoundException to be thrown, but nothing was thrown.我们预期应该抛出依赖不存在的异常,但是这里并没有抛出。说明在 config.getContext() 中 checkDependencies 时并没有获取到该组件的正确依赖。因为当前组件被构建成 SingletonProvider 并且当前并没有实现 getDependencies 方法,实现该方法:
1 static class SingletonProvider<T> implements ComponentProvider<T> {
2 T instance;
3 ComponentProvider<T> provider;
4
5 SingletonProvider(ComponentProvider<T> provider) {
6 this.provider = provider;
7 }
8
9 @Override
10 public T get(Context context) {
11 if (instance == null) {
12 instance = provider.get(context);
13 }
14 return instance;
15 }
16
17 @Override
18 public List<ComponentRef<?>> getDependencies() {
19 return provider.getDependencies();
20 }
21 }运行测试,通过。
再增加一个 Provider包装的依赖的测试:
1 Arguments.of(Named.of("Provider Scope", DependencyCheck.MissingDependencyProviderScope.class))1 @Singleton
2 static class MissingDependencyProviderScope implements TestComponent {
3 @Inject
4 Provider<Dependency> dependency;
5 }运行测试,通过。
循环依赖
对于 Scope 的循环依赖是一个可选项。
这种情况有一个很典型的优化:当两个组件都是构造函数相互依赖时,并且其中有一个是 Singleton 的,那么这个循环依赖是不成立的,因为并不是每次都需要构造新对象。
自定义 Scope 注解
自定义一个 Scope 注解 Pooled,来表示指定数量的多例:
1 @Scope
2 @Documented
3 @Retention(RUNTIME)
4 @interface Pooled {}
5
6 record PooledLiteral() implements Pooled {
7 @Override
8 public Class<? extends Annotation> annotationType() {
9 return Pooled.class;
10 }
11 }同样的,需要定义一个 Provider:
1 class PooledProvider<T> implements ContextConfig.ComponentProvider<T> {
2 static int MAX = 2;
3
4 private int current;
5 private List<T> instancePool = new ArrayList<>();
6 ContextConfig.ComponentProvider<T> provider;
7
8 PooledProvider(ContextConfig.ComponentProvider<T> provider) {
9 this.provider = provider;
10 }
11
12 @Override
13 public T get(Context context) {
14 if (instancePool.size() < MAX) instancePool.add(provider.get(context));
15 return instancePool.get(current++ % MAX);
16 }
17
18 @Override
19 public List<ComponentRef<?>> getDependencies() {
20 return provider.getDependencies();
21 }
22 }构造测试
1 // TODO bind component with customize scope annotation
2 static class PooledComponent {
3
4 }
5 @Test
6 public void should_bind_component_with_customize_scope_annotation() {
7 config.bind(PooledComponent.class, PooledComponent.class, new PooledLiteral());
8 Context context = config.getContext();
9
10 List<PooledComponent> instances = IntStream.range(0, 5)
11 .mapToObj(i -> context.get(ComponentRef.of(PooledComponent.class)).get()).toList();
12 assertEquals(PooledProvider.MAX, new HashSet<>(instances).size());
13 }运行测试,将失败:
1 Expected :2
2 Actual :1因为我们当前将所有 Scope 的注解都设置为单例。
实现
我们预期的方式是,为 config 配置指定的 Scope 应该如何创建对应的 Provider:

创建该方法:
1 public <ScopeType extends Annotation> void scope(Class<ScopeType> scopeType, Function<ComponentProvider<?>, ComponentProvider<?>> provider) {
2 }新建一个字段来保存 scope 信息:
1 private Map<Class<?>, Function<ComponentProvider<?>, ComponentProvider<?>>> scopes = new HashMap<>();那么 scope 方法的实现为:
1 public <ScopeType extends Annotation> void scope(Class<ScopeType> scopeType, Function<ComponentProvider<?>, ComponentProvider<?>> provider) {
2 scopes.put(scopeType, provider);
3 }然后还需要新建一个默认构造函数,并在其中初始化默认的 Scope 的 Provider 方法:
1 public ContextConfig() {
2 scopes.put(Singleton.class, SingletonProvider::new);
3 }修改 bind 方法中获取 scope 的 provider 的实现:
1 if (scope.isPresent()) provider = scopes.get(scope.get().annotationType()).apply(provider);运行测试,通过。
重构
重构之前,先将 ComponentProvider 和 SingletonProvider 移动到最外层。
为下面的参数定义一个函数式接口,简化代码:


1 interface ScopeProvider {
2 ComponentProvider<?> create(ComponentProvider<?> provider);
3 }对应的修改后:


重构简化 bind 方法
目前,这个 bind 方法中的实现比较复杂:

分析,这个代码中主要是对不同类型的 annotations 参数进行处理,目前这个参数中包含的的类型有:
- Scope
- Qualifier
- 其他
我们要做的就是将 annotations 根据 Scope、Qualifier 和 其他来进行分类。
这里我们可以将 “其他” 这个类别用一个自定义的注解表示:
1 private @interface Illegal {
2
3 }并定义一个分类函数:
1 private Class<? extends Annotation> typeOf(Annotation annotation) {
2 Class<? extends Annotation> type = annotation.annotationType();
3 return Stream.of(Qualifier.class, Scope.class)
4 .filter(type::isAnnotationPresent)
5 .findFirst()
6 .orElse(Illegal.class);
7 }那么分组函数就是:
1 Map<Class<?>, List<Annotation>> annotationGroups =
2 Arrays.stream(annotations).collect(Collectors.groupingBy(this::typeOf, Collectors.toList()));然后通过使用分组的数据来简化后面的代码逻辑。
首先,是对 Illegal 情况的判断:

简化为:
1 if (annotationGroups.containsKey(Illegal.class)) throw new IllegalComponentException();Scope Sad Path
增加几个关于 Scope 的 sad path
1 // TODO multi scope provided
2 // TODO multi scope annotated
3 // TODO undefined scope注册时为组件设置多个 scope,构造测试:
1 // TODO multi scope provided
2 @Test
3 public void should_throw_exception_if_multi_scope_provided() {
4 config.scope(Pooled.class, PooledProvider::new);
5 config.scope(Singleton.class, SingletonProvider::new);
6 assertThrows(IllegalComponentException.class,
7 () -> config.bind(PooledComponent.class, PooledComponent.class, new PooledLiteral(), new SingletonLiteral()));
8 }运行测试,不通过。说明并没有判断设置多个 Scope 的情况。
增加判断:

多个 Scope 注解标注同一个组件时
1 // multi scope annotated
2 @Singleton @Pooled
3 static class MultiScopeAnnotatedComponent {
4
5 }
6 @Test
7 public void should_throw_exception_if_multi_scope_annotated() {
8 config.scope(Pooled.class, PooledProvider::new);
9 assertThrows(IllegalComponentException.class,
10 () -> config.bind(MultiScopeAnnotatedComponent.class, MultiScopeAnnotatedComponent.class));
11 }运行测试,通过。
注册时,设置未定义的 Scope:
1 // TODO undefined scope
2 @Test
3 public void should_throw_exception_if_undefined_scope() {
4 assertThrows(IllegalComponentException.class,
5 () -> config.bind(PooledComponent.class, PooledComponent.class, new PooledLiteral()));
6 }在这个测试中并没有指定在容器中配置 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。
修改实现:

运行测试,通过。
测试覆盖率
Run …. with Coverage 检查代码的测试覆盖率:


虽然我们没有做到 100% 的代码覆盖了,但是我们做到了 100% 的功能覆盖,对于有些行因为语言特性的需求或者其他原因的代码,也没有必要写测试覆盖。
走的更远
可以引入 Jakarta 的 tck 包,来测试当前容器对 JSR330 规范的兼容度,并完善对 JSR330 规范的兼容。
NoBug