⭐ 推荐:社区规则条款 V1.0

如何用 Java 反射和注解实现一个简易依赖注入容器?

满月居于空
满月居于空 版主资深会员新人站长 👑年卡会员
发布于 2026-10-13 20:07 ·4 浏览 ·0 回复
内容摘要

讲解如何用 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 不含父类字段
        }
    }
}

第六步:跑起来验证

这一步用一个三层调用验证容器是否工作。

版权声明:本文来自 GJ站长论坛《如何用 Java 反射和注解实现一个简易依赖注入容器?》
原文链接:https://www.gj0.com/thread-1828.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~