16 DI Container(4):如何实现对循环依赖的处理?
你好,我是徐昊,今天我们继续使用TDD的方式实现注入依赖容器。
回顾代码与任务列表
到目前为止,我们的代码是这样的:
package geektime.tdd.di;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static java.util.Arrays.stream;
public class Context {
private Map<Class<?>, Provider<?>> providers = new HashMap<>();
public <Type> void bind(Class<Type> type, Type instance) {
providers.put(type, (Provider<Type>) () -> instance);
}
public <Type, Implementation extends Type>
void bind(Class<Type> type, Class<Implementation> implementation) {
Constructor<Implementation> injectConstructor = getInjectConstructor(implementation);
providers.put(type, (Provider<Type>) () -> {
try {
Object[] dependencies = 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);
}
});
}
private <Type> Constructor<Type> getInjectConstructor(Class<Type> implementation) {
List<Constructor<?>> injectConstructors = stream(implementation.getConstructors())
.filter(c -> c.isAnnotationPresent(Inject.class)).collect(Collectors.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();
}
});
}
public <Type> Optional<Type> get(Class<Type> type) {
return Optional.ofNullable(providers.get(type)).map(provider -> (Type)provider.get());
}
}
任务列表状态为:
视频演示
让我们进入今天的部分:
思考题
为了我们更好的交流与互动,从这节课开始,思考题目除了固定的技术问题外,我还会设置一道较为轻松的题目,供你选择与回答。
- 对于依赖的检测,目前代码的实现是在实际创建组件对象时进行的。如果改为预先检查,我们要做哪些改变呢?
- 在今天这节课中,最让你有收获的地方是什么?为什么?
欢迎把你的想法分享在留言区,也欢迎把你的项目代码的链接分享出来。相信经过你的思考与实操,学习效果会更好!
- xiaobang 👍(2) 💬(1)
预先检查的实现思路,维护一张Map,其中Key为Component,value为依赖该Component的Component集合。bind时检查当前Component依赖的Component不在依赖当前Component的集合里,同时维护该Map
2022-04-24 - Jason 👍(1) 💬(1)
老师你好我有个问题: 实现检查是否循环依赖的代码,如果是在多线程的情况下是不是会出现bug。如果两个线程同时get一个component.class,其中一个不就会抛出异常? 这个问题的衍生就是: 在实际业务场景中,TDD如何自然的使用任务列表以及对应的testcase,推导出有关多线程的需求实现代码?
2022-06-04 - 临风 👍(1) 💬(0)
改为预先检查,我认为就要一开始就把实例创建好放入容器中去。这就导致你必须知道依赖的实例要怎么创建。 我想到的方式就是用spring的方式,1.通过@Component来指明实现类;2.通过xml或者@Configuration来指明如何初始化。 还想到一个方法就是bind必须按照顺序初始化,无依赖的先初始化,类似有向图,没有出度的节点先初始化。
2022-04-20 - 努力努力再努力 👍(0) 💬(0)
问题1:对于依赖的检测,目前代码的实现是在实际创建组件对象时进行的。如果改为预先检查,我们要做哪些改变呢? 如果改为在 bind 处检查,可以维护一个哈希表,key是componentType, value 是构造函数参数的集合。每绑定一个组件的时候,获取构造函数的参数到哈希表中查询,如果存在,判断对应的 value 中是否包含 component,包含则可以认为 存在循环依赖,抛出异常。 问题2:在今天这节课中,最让你有收获的地方是什么?为什么? 最有收获的是老师在编码过程中,将突发想到的问题,转化为测试用例记录在案。这让我想到一些平时在工作中,前一个月修复的Bug,基本都是在生产代码上直接进行修改的,有时候可能仅仅是加了一个 if 分支的判断,然而个把月后突然看到这个分支,完全想不起来当初为什么会这样加。如果有测试用例可以回溯,那么也方便后续的问题跟踪
2022-09-23 - 蝴蝶 👍(0) 💬(0)
问题 1:增加类似 builder 模式里面 build 的方法来检查依赖。
2022-08-13 - Geek_874b6f 👍(0) 💬(0)
Idea 支持将匿名类转化内部类的
2022-05-31 - keep_curiosity 👍(0) 💬(0)
使用Set存放循环依赖的组件不能保证组件依赖的顺序吧?如何构造有效的测试验证顺序呢? 我的跟练:https://github.com/codingthought/TDD-DI
2022-05-01 - 枫中的刀剑 👍(0) 💬(0)
解决循环依赖的思路确实妙啊,后来查看guice的源码里发现也是这样判断循环依赖的。
2022-04-27 - ACE丶8 👍(0) 💬(0)
收获:在实现循环依赖,自己的思路是将依赖信息,在构造的过程中不断传递,当构造的时候发现,构造信息中已经出现过该类,就抛出异常,最终没有解决。老师的思路是通过一个代理类,携带上构造信息,让循环依赖的检查变得更加简单,学习了。
2022-04-25 - aoe 👍(0) 💬(0)
最大收获:测试可以及时发现 Bug,将排查范围缩减到最小,虽然不停回放视频比对代码很吃力,但还是能看到希望。 原因:如果没有测试的代码,以下两个小细节导致的 Bug,找一天也不一定能发现! 跟着视频敲代码,卡住2次,都来自 public T get() 方法: 1、constructing = true; 的位置比老师的靠后(我天真的想法:靠前靠后关系大吗?赋值就行) 2、try 的作用域比老师的小(我天真的想法:要尽量将 try 的作用域减小,专注真正需要捕获异常的部分;提升性能) 可以使用 HashSet<Class<?>> classes = Sets.newHashSet(exception.getComponents()); 需要导入依赖:implementation 'com.google.guava:guava:31.1-jre'
2022-04-20