如何用 Java 反射和注解实现一个简易依赖注入容器?
讲解如何用 Java 反射和注解实现简易依赖注入容器,通过 @Component 与 @Inject 标记、包扫描、构造器和字段注入及单例管理,完成不到 150 行的迷你 DI 容器。
照着做下来,你能得到一个不到 150 行的迷你 DI 容器:它能自动扫描指定包下的类,按构造器注入和字段注入装配依赖,并把对象管理成单例。
第一步:定义两个注解
这一步做的是"给容器贴标签",做完后你就能用 @Component 标记可被管理的类、用 @Inject 标记需要注入的构造器或字段。
新建包 com.example.di,写两个注解文件:
@Retention(RetentionPolicy.RUNTIME) // 必须是 RUNTIME,否则反射读不到
@Target(ElementType.TYPE)
public @interface Component {}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD})
public @interface Inject {}
注意:
RetentionPolicy如果写成CLASS(默认值),编译后注解会被丢弃,运行时isAnnotationPresent永远返回 false,这是最常见的第一个坑。
第二步:写包扫描
这一步要拿到指定包下所有 Class 对象,做完后容器才知道有哪些候选组件。
private static Set<Class<?>> scan(String basePackage) throws Exception {
Set<Class<?>> result = new LinkedHashSet<>();
String path = basePackage.replace('.', '/');
Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(path);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
if (!"file".equals(url.getProtocol())) continue; // jar 场景暂不处理
collect(new File(url.toURI()), basePackage, result);
}
return result;
}
private static void collect(File dir, String pkg, Set<Class<?>> out) throws Exception {
File[] files = dir.listFiles();
if (files == null) return;
for (File f : files) {
if (f.isDirectory()) {
collect(f, pkg + "." + f.getName(), out);
} else if (f.getName().endsWith(".class")) {
String cn = pkg + "." + f.getName().substring(0, f.getName().length() - 6);
out.add(Class.forName(cn, false, Thread.currentThread().getContextClassLoader()));
}
}
}
注意:打成 jar 包后
url.getProtocol()是"jar",上面的代码会直接跳过,导致容器一个 Bean 都扫不到。生产项目请改用 ClassGraph 这类库,或对JarURLConnection单独处理。
第三步:搭容器骨架并注册组件
这一步把扫描结果里的 @Component 类登记进容器,得到一张"类型 → 实现类"的注册表。
public class MiniContainer {
private final Map<Class<?>, Class<?>> bindings = new HashMap<>();
private final Map<Class<?>, Object> singletons = new HashMap<>();
private final Set<Class<?>> creating = new HashSet<>();
public MiniContainer(String basePackage) {
try {
for (Class<?> c : scan(basePackage)) {
if (c.isAnnotationPresent(Component.class) && !c.isInterface()) {
bindings.put(c, c);
}
}
} catch (Exception e) {
throw new RuntimeException("扫描失败", e);
}
}
第四步:实现构造器注入
这一步让容器能顺着构造器参数递归地把依赖建出来,做完后 getBean(UserService.class) 就能自动拿到装配好的对象。
public <T> T getBean(Class<T> type) {
Class<?> impl = bindings.getOrDefault(type, type);
Object cached = singletons.get(impl);
if (cached == null) cached = create(impl);
return type.cast(cached);
}
private Object create(Class<?> impl) {
if (singletons.containsKey(impl)) return singletons.get(impl);
if (!creating.add(impl)) {
throw new IllegalStateException("出现循环依赖: " + impl.getName());
}
try {
Constructor<?> ctor = pickConstructor(impl);
Class<?>[] paramTypes = ctor.getParameterTypes();
Object[] args = new Object[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++) {
args[i] = getBean(paramTypes[i]);
}
Object instance = ctor.newInstance(args);
singletons.put(impl, instance); // 先放缓存,再填字段
injectFields(instance);
return instance;
} catch (ReflectiveOperationException e) {
throw new RuntimeException("实例化失败: " + impl.getName(), e);
} finally {
creating.remove(impl);
}
}
private Constructor<?> pickConstructor(Class<?> impl) {
Constructor<?>[] ctors = impl.getDeclaredConstructors();
for (Constructor<?> c : ctors) {
if (c.isAnnotationPresent(Inject.class)) { c.setAccessible(true); return c; }
}
for (Constructor<?> c : ctors) {
if (c.getParameterCount() == 0) { c.setAccessible(true); return c; }
}
throw new IllegalStateException("找不到 @Inject 构造器,也没有无参构造器: " + impl.getName());
}
注意:
singletons.put一定要放在injectFields之前。否则 A、B 互相字段注入时会无限递归;提前入缓存正是 Spring 三级缓存要解决的同一个问题。
第五步:实现字段注入
这一步处理标了 @Inject 的成员变量,做完后私有字段也能被自动赋值。
private void injectFields(Object instance) throws IllegalAccessException {
Class<?> c = instance.getClass();
while (c != null && c != Object.class) {
for (Field f : c.getDeclaredFields()) {
if (f.isAnnotationPresent(Inject.class)) {
f.setAccessible(true);
f.set(instance, getBean(f.getType()));
}
}
c = c.getSuperclass(); // getDeclaredFields 不含父类字段
}
}
}
第六步:跑起来验证
这一步用一个三层调用验证容器是否工作。
原文链接:https://www.gj0.com/thread-1828.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。