Skip to content

手写 @Resource 简易容器与真实 Spring 对照

本页通过一个只依赖 JDK 8 的微型容器,帮助理解“定义注册、singleton 创建、注入元数据扫描、名称解析、类型回退、反射写入和异常失败”主线。

它是教学模型,不是 Spring 源码复刻。真实 Spring 使用 CommonAnnotationBeanPostProcessorDependencyDescriptorDefaultListableBeanFactory、作用域、生命周期、代理和大量缓存,不能把本页的几十行实现当成 Spring 内部实现。

学习目标

  • 自己定义运行期注解并读取字段、Setter 元数据;
  • 解释为什么必须先有 BeanDefinition,不能只保存对象 Map;
  • 实现按显式名称、默认属性名和唯一类型查找;
  • 明确显式名称找不到时为什么不能静默换实现;
  • 实现类型校验、重复定义、无候选和多候选异常;
  • 解释实例化、属性填充和 singleton 缓存的顺序;
  • 理解反射注入 private 字段的封装和模块边界;
  • 故意制造歧义和缺失依赖,并根据异常定位;
  • 将简易实现的每一步映射到真实 Spring;
  • 说清简易容器没有实现哪些生产能力。

一、为什么要手写,但不能迷信手写

直接使用 Spring:

java
@Resource(name = "smsChannel")
private MessageChannel channel;

初学者只看到字段最终有值,看不到中间过程。手写最小模型能暴露:

  1. 注解本身只是元数据;
  2. 必须有人扫描注解;
  3. 必须定义候选查找规则;
  4. 必须先创建或取得依赖;
  5. 必须检查类型是否可赋值;
  6. 必须反射字段或调用 Setter;
  7. 失败必须携带 Bean 名和注入点。

但手写容器通常没有:

  • BeanPostProcessor 链;
  • 构造器自动选择;
  • @Autowired、Qualifier、Primary 和泛型解析;
  • singleton 三级缓存和早期代理;
  • FactoryBean;
  • scope;
  • AOP;
  • 生命周期和销毁;
  • 父子容器;
  • Profile/Condition;
  • 并发创建协调;
  • merged BeanDefinition;
  • 依赖关系和销毁顺序;
  • 完整异常体系。

所以学习目标是理解主线和设计约束,不是重新实现 Spring。

二、先纠正 @Resource 查找规则

本页教学容器采用以下明确规则:

text
显式写name

只按该名称查找

不存在直接失败

存在后检查类型

未显式写 name:

text
推导字段名或Setter属性名

默认名称存在则使用并检查类型

默认名称不存在

按类型查找候选

唯一候选才注入

零个或多个都失败
mermaid
flowchart TD
    A["发现MyResource注入点"] --> B{"是否显式name?"}
    B -- "是" --> C["按显式名称查找"]
    C --> D{"名称存在?"}
    D -- "否" --> E["抛MissingBeanException"]
    D -- "是" --> F["检查实际类型"]
    B -- "否" --> G["推导字段或Setter属性名"]
    G --> H{"默认名称存在?"}
    H -- "是" --> F
    H -- "否" --> I["按声明类型收集候选"]
    I --> J{"候选数量"}
    J -- "0" --> E
    J -- "1" --> F
    J -- "多个" --> K["抛AmbiguousBeanException"]
    F --> L{"可以赋值?"}
    L -- "否" --> M["抛BeanTypeMismatchException"]
    L -- "是" --> N["字段反射或Setter调用"]

2.1 为什么显式名称不能自动换成别的实现

如果代码明确写:

java
@Resource(name = "bankPaymentGateway")

却因为名称缺失自动注入唯一的 mockPaymentGateway,系统可能把生产支付切到模拟实现。显式名称是配置契约,找不到应在启动阶段失败。

2.2 真实 Spring 的准确边界

真实 Spring 中 @Resource 常由 CommonAnnotationBeanPostProcessor 处理。未显式 name 时会从字段名或 Setter 属性名推导默认名称;在其配置允许默认类型回退、且默认名称不存在时,才可能进入类型解析。显式 name、JNDI 配置、版本和处理器设置会改变路径,因此不能无条件背“先名称,找不到一定按类型”。

完整真实规则见@Autowired@Resource注入全过程

三、微型容器的总体架构

mermaid
flowchart TD
    A["registerBeanDefinition"] --> B["definitions保存创建元数据"]
    B --> C["refresh遍历非懒定义"]
    C --> D["getBean"]
    D --> E["检查singletonObjects"]
    E --> F{"已创建?"}
    F -- "是" --> G["返回singleton"]
    F -- "否" --> H["标记currentlyCreating"]
    H --> I["调用无参构造器实例化"]
    I --> J["扫描MyResource字段和方法"]
    J --> K["递归getBean解析依赖"]
    K --> L["反射字段或调用方法"]
    L --> M["完整对象放入singletonObjects"]
    M --> N["清除创建标记"]

微型容器只支持 singleton 和无参构造器,故意不实现循环依赖。创建中的 Bean 再次被请求时直接抛出 CircularDependencyException,让学习者先理解问题,再到独立页面学习真实 Spring 的早期引用。

四、为什么需要 BeanDefinition

如果只有:

java
Map<String, Object> beans;

容器只知道“已经有什么对象”,不知道:

  • 对象还没创建时应使用哪个 class;
  • 是否 singleton;
  • 是否 lazy;
  • 构造参数是什么;
  • 初始化和销毁方法是什么;
  • 创建失败后能否重试;
  • 类型查询时有哪些候选。

本页最小定义只保存名称和 class:

java
static final class BeanDefinition {
    private final String name;
    private final Class<?> beanClass;

    BeanDefinition(String name, Class<?> beanClass) {
        this.name = name;
        this.beanClass = beanClass;
    }
}

真实 Spring 的 BeanDefinition 远比这丰富,但“定义与实例分离”是容器扩展能力的起点。

五、完整 JDK 8 可运行 Demo

保存为 MiniResourceDemo.java,不需要 Maven 第三方依赖:

java
import java.beans.Introspector;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class MiniResourceDemo {
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.METHOD})
    public @interface MyResource {
        String name() default "";
    }

    static final class BeanDefinition {
        private final String name;
        private final Class<?> beanClass;

        BeanDefinition(String name, Class<?> beanClass) {
            this.name = name;
            this.beanClass = beanClass;
        }
    }

    static class MiniApplicationContext {
        private final Map<String, BeanDefinition> definitions =
                new LinkedHashMap<String, BeanDefinition>();
        private final Map<String, Object> singletonObjects =
                new LinkedHashMap<String, Object>();
        private final Set<String> currentlyCreating =
                new LinkedHashSet<String>();
        private boolean refreshed;

        public void registerBeanDefinition(
                String name, Class<?> beanClass) {
            requireText(name, "beanName");
            if (beanClass == null) {
                throw new IllegalArgumentException("beanClass不能为空");
            }
            if (refreshed) {
                throw new IllegalStateException("refresh后禁止注册新定义");
            }
            if (definitions.containsKey(name)) {
                throw new DuplicateBeanDefinitionException(name);
            }
            definitions.put(name, new BeanDefinition(name, beanClass));
        }

        public void refresh() {
            if (refreshed) {
                throw new IllegalStateException("容器不能重复refresh");
            }
            refreshed = true;
            try {
                for (String beanName : definitions.keySet()) {
                    getBean(beanName);
                }
            } catch (RuntimeException exception) {
                singletonObjects.clear();
                currentlyCreating.clear();
                throw exception;
            }
        }

        public synchronized Object getBean(String name) {
            requireText(name, "beanName");
            Object singleton = singletonObjects.get(name);
            if (singleton != null) {
                return singleton;
            }
            BeanDefinition definition = definitions.get(name);
            if (definition == null) {
                throw new MissingBeanException(
                        "不存在名称为'" + name + "'的BeanDefinition");
            }
            if (!currentlyCreating.add(name)) {
                throw new CircularDependencyException(
                        "检测到循环创建:" + currentlyCreating + " -> " + name);
            }
            try {
                Object bean = instantiate(definition);
                injectResources(name, bean);
                singletonObjects.put(name, bean);
                return bean;
            } catch (RuntimeException exception) {
                singletonObjects.remove(name);
                throw exception;
            } finally {
                currentlyCreating.remove(name);
            }
        }

        public <T> T getBean(String name, Class<T> requiredType) {
            Object bean = getBean(name);
            if (!requiredType.isInstance(bean)) {
                throw new BeanTypeMismatchException(
                        "Bean '" + name + "'实际类型为"
                                + bean.getClass().getName()
                                + ",不能赋值给" + requiredType.getName());
            }
            return requiredType.cast(bean);
        }

        private Object instantiate(BeanDefinition definition) {
            try {
                Constructor<?> constructor =
                        definition.beanClass.getDeclaredConstructor();
                boolean accessible = constructor.isAccessible();
                try {
                    if (!accessible) constructor.setAccessible(true);
                    return constructor.newInstance();
                } finally {
                    if (!accessible) constructor.setAccessible(false);
                }
            } catch (Exception exception) {
                throw new BeanCreationException(
                        "创建Bean '" + definition.name + "'失败,需要无参构造器",
                        exception);
            }
        }

        private void injectResources(String beanName, Object bean) {
            List<Class<?>> hierarchy = classHierarchy(bean.getClass());
            for (Class<?> type : hierarchy) {
                for (Field field : type.getDeclaredFields()) {
                    MyResource resource = field.getAnnotation(MyResource.class);
                    if (resource != null) {
                        injectField(beanName, bean, field, resource);
                    }
                }
                for (Method method : type.getDeclaredMethods()) {
                    MyResource resource = method.getAnnotation(MyResource.class);
                    if (resource != null) {
                        injectMethod(beanName, bean, method, resource);
                    }
                }
            }
        }

        private void injectField(String beanName, Object bean,
                                 Field field, MyResource resource) {
            if (Modifier.isStatic(field.getModifiers())
                    || Modifier.isFinal(field.getModifiers())) {
                throw new InvalidInjectionPointException(
                        "Bean '" + beanName + "'字段" + field.getName()
                                + "不能是static或final");
            }
            Object dependency = resolveResource(
                    resource.name(), field.getName(), field.getType(),
                    beanName + "." + field.getName());
            boolean accessible = field.isAccessible();
            try {
                if (!accessible) field.setAccessible(true);
                field.set(bean, dependency);
            } catch (Exception exception) {
                throw new BeanCreationException(
                        "注入字段失败:" + beanName + "." + field.getName(),
                        exception);
            } finally {
                if (!accessible) field.setAccessible(false);
            }
        }

        private void injectMethod(String beanName, Object bean,
                                  Method method, MyResource resource) {
            if (Modifier.isStatic(method.getModifiers())
                    || method.getParameterTypes().length != 1) {
                throw new InvalidInjectionPointException(
                        "MyResource方法必须是非static单参数方法:"
                                + beanName + "." + method.getName());
            }
            String propertyName = propertyName(method);
            Class<?> dependencyType = method.getParameterTypes()[0];
            Object dependency = resolveResource(
                    resource.name(), propertyName, dependencyType,
                    beanName + "." + method.getName() + "(...)" );
            boolean accessible = method.isAccessible();
            try {
                if (!accessible) method.setAccessible(true);
                method.invoke(bean, dependency);
            } catch (Exception exception) {
                throw new BeanCreationException(
                        "调用注入方法失败:" + beanName + "." + method.getName(),
                        exception);
            } finally {
                if (!accessible) method.setAccessible(false);
            }
        }

        private Object resolveResource(String explicitName,
                                       String defaultName,
                                       Class<?> requiredType,
                                       String injectionPoint) {
            if (explicitName != null && !explicitName.trim().isEmpty()) {
                return resolveByName(
                        explicitName.trim(), requiredType, injectionPoint);
            }
            if (definitions.containsKey(defaultName)) {
                return resolveByName(defaultName, requiredType, injectionPoint);
            }
            List<String> candidates = new ArrayList<String>();
            for (BeanDefinition definition : definitions.values()) {
                if (requiredType.isAssignableFrom(definition.beanClass)) {
                    candidates.add(definition.name);
                }
            }
            if (candidates.isEmpty()) {
                throw new MissingBeanException(
                        "注入点" + injectionPoint + "找不到类型"
                                + requiredType.getName());
            }
            if (candidates.size() > 1) {
                throw new AmbiguousBeanException(
                        "注入点" + injectionPoint + "按类型找到多个候选:"
                                + candidates);
            }
            return resolveByName(candidates.get(0), requiredType, injectionPoint);
        }

        private Object resolveByName(String dependencyName,
                                     Class<?> requiredType,
                                     String injectionPoint) {
            if (!definitions.containsKey(dependencyName)) {
                throw new MissingBeanException(
                        "注入点" + injectionPoint + "显式/默认名称'"
                                + dependencyName + "'不存在");
            }
            Object dependency = getBean(dependencyName);
            if (!requiredType.isInstance(dependency)) {
                throw new BeanTypeMismatchException(
                        "注入点" + injectionPoint + "需要"
                                + requiredType.getName() + ",但Bean '"
                                + dependencyName + "'实际是"
                                + dependency.getClass().getName());
            }
            return dependency;
        }

        private static String propertyName(Method method) {
            String name = method.getName();
            if (name.startsWith("set") && name.length() > 3) {
                return Introspector.decapitalize(name.substring(3));
            }
            return name;
        }

        private static List<Class<?>> classHierarchy(Class<?> beanClass) {
            List<Class<?>> result = new ArrayList<Class<?>>();
            Class<?> current = beanClass;
            while (current != null && current != Object.class) {
                result.add(0, current);
                current = current.getSuperclass();
            }
            return result;
        }

        private static void requireText(String value, String argument) {
            if (value == null || value.trim().isEmpty()) {
                throw new IllegalArgumentException(argument + "不能为空");
            }
        }
    }

    interface MessageChannel {
        String send();
    }

    static class EmailChannel implements MessageChannel {
        public String send() { return "EMAIL"; }
    }

    static class SmsChannel implements MessageChannel {
        public String send() { return "SMS"; }
    }

    static class AuditLog {
        String record() { return "AUDIT"; }
    }

    static class NoticeService {
        @MyResource(name = "smsChannel")
        private MessageChannel primaryChannel;

        @MyResource
        private MessageChannel emailChannel;

        private AuditLog auditLog;

        @MyResource
        void setAuditLog(AuditLog auditLog) {
            this.auditLog = auditLog;
        }

        String execute() {
            return primaryChannel.send() + "+"
                    + emailChannel.send() + "+" + auditLog.record();
        }
    }

    static class AmbiguousConsumer {
        @MyResource
        private MessageChannel channel;
    }

    static class MissingNamedConsumer {
        @MyResource(name = "missingChannel")
        private MessageChannel channel;
    }

    static class ContainerException extends RuntimeException {
        ContainerException(String message) { super(message); }
        ContainerException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    static class DuplicateBeanDefinitionException extends ContainerException {
        DuplicateBeanDefinitionException(String name) {
            super("重复BeanDefinition:" + name);
        }
    }

    static class MissingBeanException extends ContainerException {
        MissingBeanException(String message) { super(message); }
    }

    static class AmbiguousBeanException extends ContainerException {
        AmbiguousBeanException(String message) { super(message); }
    }

    static class BeanTypeMismatchException extends ContainerException {
        BeanTypeMismatchException(String message) { super(message); }
    }

    static class InvalidInjectionPointException extends ContainerException {
        InvalidInjectionPointException(String message) { super(message); }
    }

    static class CircularDependencyException extends ContainerException {
        CircularDependencyException(String message) { super(message); }
    }

    static class BeanCreationException extends ContainerException {
        BeanCreationException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    public static void main(String[] args) {
        successfulInjection();
        ambiguousTypeMustFail();
        explicitMissingNameMustFail();
    }

    private static void successfulInjection() {
        MiniApplicationContext context = baseContext();
        context.registerBeanDefinition("securityAudit", AuditLog.class);
        context.registerBeanDefinition("noticeService", NoticeService.class);
        context.refresh();
        String result = context.getBean(
                "noticeService", NoticeService.class).execute();
        assertEquals("SMS+EMAIL+AUDIT", result);
        System.out.println("success=" + result);
    }

    private static void ambiguousTypeMustFail() {
        MiniApplicationContext context = baseContext();
        context.registerBeanDefinition(
                "ambiguousConsumer", AmbiguousConsumer.class);
        try {
            context.refresh();
            throw new AssertionError("预期AmbiguousBeanException");
        } catch (AmbiguousBeanException expected) {
            System.out.println("ambiguous=passed");
        }
    }

    private static void explicitMissingNameMustFail() {
        MiniApplicationContext context = baseContext();
        context.registerBeanDefinition(
                "missingNamedConsumer", MissingNamedConsumer.class);
        try {
            context.refresh();
            throw new AssertionError("预期MissingBeanException");
        } catch (MissingBeanException expected) {
            System.out.println("explicitMissing=passed");
        }
    }

    private static MiniApplicationContext baseContext() {
        MiniApplicationContext context = new MiniApplicationContext();
        context.registerBeanDefinition("emailChannel", EmailChannel.class);
        context.registerBeanDefinition("smsChannel", SmsChannel.class);
        return context;
    }

    private static void assertEquals(String expected, String actual) {
        if (!expected.equals(actual)) {
            throw new AssertionError(
                    "expected=" + expected + ", actual=" + actual);
        }
    }
}

期望输出:

text
success=SMS+EMAIL+AUDIT
ambiguous=passed
explicitMissing=passed

六、成功 Demo 逐步分析

6.1 显式名称

java
@MyResource(name = "smsChannel")
private MessageChannel primaryChannel;

容器只查 smsChannel,取得 SmsChannel 后验证它实现 MessageChannel。即使 EmailChannel 也是同类型,也不会参与选择。

6.2 默认字段名称

java
@MyResource
private MessageChannel emailChannel;

未显式 name,默认名称是字段名 emailChannel。定义中存在该名称,所以直接按名称使用;不会因为还有 SmsChannel 而发生歧义。

6.3 Setter 属性名与唯一类型回退

java
@MyResource
void setAuditLog(AuditLog auditLog) { ... }

JavaBeans 属性名由 setAuditLog 推导为 auditLog。容器没有这个名称,但只有 securityAudit 的 class 可以赋值给 AuditLog,所以唯一类型回退成功。

这三个注入点故意覆盖三条不同路径,最终输出 SMS+EMAIL+AUDIT

七、失败 Demo 为什么必须存在

7.1 多候选不能随机选

AmbiguousConsumer 的字段名是 channel,没有同名定义;按 MessageChannel 类型找到 emailChannel 和 smsChannel。容器抛出 AmbiguousBeanException,并把候选名称写入异常。

绝不能依赖 HashMap 遍历顺序“选第一个”,否则换 JDK、注册顺序或部署包后可能注入另一个实现。

7.2 显式名称缺失不能回退

MissingNamedConsumer 明确要求 missingChannel。即使容器里存在两个 MessageChannel,也必须报 MissingBeanException,不能换成“看起来能用”的实现。

7.3 refresh 失败为什么清 singleton

教学容器在 refresh 失败后清空已经创建的 singleton,避免调用者拿到只完成一部分依赖的容器。真实 Spring refresh 失败也会销毁已创建 singleton 和取消 refresh,但其状态、销毁和异常处理更完整。

八、反射注入全过程

字段注入:

text
扫描Class层次

读取DeclaredFields

查找运行期MyResource注解

确定依赖名称和类型

getBean递归创建依赖

检查requiredType.isInstance

临时setAccessible(true)

Field.set

恢复原accessible状态

Setter 注入则使用 Method.invoke。方法必须是非 static 且只有一个参数。本页允许非 public Setter,只为演示反射;商业代码仍应优先构造器注入强制依赖。

8.1 为什么扫描父类

getDeclaredFields() 只返回当前 class 声明字段,不自动包含父类。容器要沿 superclass 收集元数据。本页从父类到子类扫描,便于形成稳定顺序;真实 Spring 还会缓存 InjectionMetadata。

8.2 JDK 9+ 模块边界

JDK 8 中 setAccessible(true) 常能突破 Java 访问检查。JDK 9 模块系统之后,未开放 package 的深反射可能抛 InaccessibleObjectException。框架、模块声明和启动参数必须兼容,不能认为 private 反射永远成功。

九、创建状态与循环依赖

本页容器在实例化和注入全部完成后才放入 singletonObjects。如果 A 注入 B、B 又注入 A:

mermaid
flowchart TD
    A["创建A并标记currentlyCreating"] --> B["A解析B"]
    B --> C["创建B并标记currentlyCreating"]
    C --> D["B解析A"]
    D --> E["发现A正在创建"]
    E --> F["抛CircularDependencyException"]
    F --> G["清理创建标记和失败singleton"]

真实 Spring 对部分 singleton 字段/Setter 循环会在实例化后、注入前加入 early singleton factory,通过早期引用解决;涉及代理时还要保持 earlyBeanReference 和最终代理一致。本页故意不实现,因为随意把半成品提前放入 Map 会产生原始对象泄漏和代理不一致。

完整原理见循环依赖与三级缓存全过程

十、教学容器与真实 Spring 逐项映射

教学实现真实Spring常见组件关键差异
definitions MapDefaultListableBeanFactory定义注册表真实定义元数据丰富且可合并
singletonObjectsDefaultSingletonBeanRegistry一级缓存真实还有早期缓存、创建锁和销毁关系
currentlyCreatingsingletonsCurrentlyInCreation等状态真实区分singleton/prototype并发与异常
instantiatecreateBeanInstance真实支持构造器、Supplier、工厂方法等
injectResourcesCommonAnnotationBeanPostProcessor真实是BPP并缓存InjectionMetadata
resolveResourceBeanFactory依赖解析真实支持限定符、泛型、父容器等
field.set/method.invokeResourceElement注入真实还有访问器、异常包装和AOT边界
refresh遍历preInstantiateSingletons真实先完成BFPP/BPP和多种基础设施
clear on failurerefresh失败销毁和缓存清理真实执行完整destroy和状态回滚

十一、为什么真实 Spring 使用 BeanPostProcessor

如果把 Resource 扫描硬编码在 BeanFactory 的 createBean 中:

  • 容器核心与每种注解耦合;
  • 新增注解必须修改核心源码;
  • 无法独立排序和替换;
  • 测试和扩展困难。

Spring 通过 CommonAnnotationBeanPostProcessor 接入 Bean 生命周期:

  1. 合并定义阶段查找并缓存 Resource 元数据;
  2. 属性填充阶段执行注入;
  3. 同一处理器体系还可处理部分生命周期注解;
  4. BeanFactory 只提供候选和依赖解析能力;
  5. 新处理器可以用扩展点加入,不修改核心工厂。

这是开闭原则在框架中的实际应用。

十二、真实 Spring 为什么不能“先创建全部再注入”

原页面的实现先创建全部实例,再统一扫描注入,看起来简单,却有问题:

  • 构造器参数无法注入;
  • 创建失败时半成品已在 Map;
  • lazy Bean 被全部创建;
  • prototype 语义无法实现;
  • FactoryBean 无法区分工厂和产品;
  • 初始化回调发生时依赖状态不清晰;
  • AOP 早期代理无法协调;
  • dependsOn 和销毁顺序无法记录。

真实 Spring 按 getBean 递归解析依赖,每个 Bean 走完整 create、populate、initialize 流程,并用 scope 和缓存协调。

十三、设计这个容器时为什么这样取舍

使用 LinkedHashMap

为了让定义遍历和异常候选列表稳定,便于测试和阅读。但候选选择不能依赖顺序;多候选仍然失败。

使用无参构造器

把课程焦点放在 Resource 字段和方法注入。构造器解析是另一套复杂算法,应到 IoC 和源码页学习,不能写一个“参数最多”规则冒充 Spring。

不实现自动循环依赖

错误提前缓存原始对象比直接失败更危险。先失败,再学习真实三级缓存。

refresh 后禁止注册

避免定义和 singleton 缓存并发变化。真实 Spring 有更复杂的定义重置和缓存失效,但普通业务也不应在运行高峰随意修改容器结构。

失败后抛异常

容器初始化失败必须让调用方知道,不能 printStackTrace 后继续返回一个不完整上下文。

十四、商业项目里应该怎样注入

  • 强制业务依赖优先构造器注入;
  • 多实现使用语义明确的 Qualifier,而不是依赖字段名碰巧匹配;
  • Resource 显式 name 用稳定 Bean 名,不接受用户输入;
  • 不注入 static 字段;
  • 不用 Resource 直接注入任意用户可选择的策略 Bean;
  • 策略路由使用业务码白名单;
  • 避免在构造器中调用尚未完成字段注入的对象;
  • 升级 Spring 6 时整体迁移 javax 到 jakarta;
  • 生产启动失败保留完整 cause 和候选名称,但不打印敏感配置。

十五、生产注入问题排查

15.1 字段仍然为 null

  1. 确认对象是不是 Spring 创建;
  2. 检查是否手工 new、反序列化或第三方回调创建;
  3. 检查字段是否 static;
  4. 检查当前代码是否在构造器里过早读取字段;
  5. 检查 CommonAnnotationBeanPostProcessor 是否注册;
  6. 检查测试上下文是否加载该 Bean;
  7. 检查 javax/jakarta 包是否与 Spring 版本匹配。

15.2 注入错实现

记录注入点、显式/默认名称、候选 Bean 名、运行时 class、代理类型、Profile、Condition和父子容器。不要只根据接口有几个实现猜测。

15.3 本地成功、生产失败

比较:

  • Spring/JDK版本;
  • javax/jakarta依赖;
  • Profile;
  • 条件装配报告;
  • Bean覆盖策略;
  • 组件扫描;
  • 父子上下文;
  • 编译参数和混淆;
  • JDK模块开放配置。

15.4 启动报最外层 UnsatisfiedDependencyException

沿 cause 链找到最深的 Missing、NoUnique、TypeMismatch 或 BeanCreationException。最外层 Bean 只是依赖链入口,真正错误可能在数据源、配置绑定或更深层 Bean。

十六、扩展练习

按顺序练习,每增加功能先写失败测试:

  1. 增加 getBean(Class<T>),多候选必须失败;
  2. 增加 lazy 定义,理解 refresh 与 getBean;
  3. 增加 prototype,禁止进入 singletonObjects;
  4. 增加 init/destroy 回调;
  5. 缓存字段和方法 InjectionMetadata;
  6. 增加构造器注入,但不要使用“参数最多”简化规则;
  7. 增加依赖关系图和销毁顺序;
  8. 增加受控 early reference,再观察循环依赖风险;
  9. 增加 BeanPostProcessor 接口,把 Resource 扫描移出核心;
  10. 对照 Spring 源码删除教学容器,回到真实框架。

十七、面试标准回答

@Resource 是 JVM 自动注入吗

不是。注解只是运行期元数据。Spring 中通常由 CommonAnnotationBeanPostProcessor 扫描字段或方法、缓存注入元数据,在属性填充阶段根据名称和类型规则从BeanFactory解析依赖,再反射字段或调用方法完成注入。

显式 name 找不到会自动按类型吗

不能无条件这么说。显式名称通常代表明确资源契约,找不到应失败;未显式name时会推导字段或属性名,在Spring处理器允许默认类型回退且默认名称不存在时,才可能按类型解析。具体还受版本和处理器配置影响。

为什么按类型有多个实现不能选第一个

遍历顺序不是业务语义,注册顺序改变会导致注入实现变化。应使用显式name、Qualifier、唯一Primary或集合策略,并让无法唯一选择时启动失败。

手写容器与真实 Spring 最大区别是什么

手写容器只演示定义、创建、查找和反射注入主线;真实Spring还有后置处理器、复杂候选算法、构造器解析、作用域、三级缓存、早期代理、FactoryBean、生命周期、父子容器、并发协调、异常回滚和销毁等完整基础设施。

十八、关联知识与验收

掌握验收:

  • 能解释注解为什么不会自己执行;
  • 能说清显式name、默认名称和类型回退;
  • 能运行成功、歧义和显式名称缺失三个测试;
  • 能解释BeanDefinition与singleton实例Map的区别;
  • 能说明为什么多候选不能随机选第一个;
  • 能画出字段和Setter反射注入流程;
  • 能解释为什么教学容器不支持循环依赖;
  • 能把每个教学方法映射到真实Spring组件;
  • 能说明javax到jakarta迁移边界;
  • 能根据异常链完成生产注入排查。