手写 @Resource 简易容器与真实 Spring 对照
本页通过一个只依赖 JDK 8 的微型容器,帮助理解“定义注册、singleton 创建、注入元数据扫描、名称解析、类型回退、反射写入和异常失败”主线。
它是教学模型,不是 Spring 源码复刻。真实 Spring 使用 CommonAnnotationBeanPostProcessor、DependencyDescriptor、DefaultListableBeanFactory、作用域、生命周期、代理和大量缓存,不能把本页的几十行实现当成 Spring 内部实现。
学习目标
- 自己定义运行期注解并读取字段、Setter 元数据;
- 解释为什么必须先有 BeanDefinition,不能只保存对象 Map;
- 实现按显式名称、默认属性名和唯一类型查找;
- 明确显式名称找不到时为什么不能静默换实现;
- 实现类型校验、重复定义、无候选和多候选异常;
- 解释实例化、属性填充和 singleton 缓存的顺序;
- 理解反射注入 private 字段的封装和模块边界;
- 故意制造歧义和缺失依赖,并根据异常定位;
- 将简易实现的每一步映射到真实 Spring;
- 说清简易容器没有实现哪些生产能力。
一、为什么要手写,但不能迷信手写
直接使用 Spring:
@Resource(name = "smsChannel")
private MessageChannel channel;初学者只看到字段最终有值,看不到中间过程。手写最小模型能暴露:
- 注解本身只是元数据;
- 必须有人扫描注解;
- 必须定义候选查找规则;
- 必须先创建或取得依赖;
- 必须检查类型是否可赋值;
- 必须反射字段或调用 Setter;
- 失败必须携带 Bean 名和注入点。
但手写容器通常没有:
- BeanPostProcessor 链;
- 构造器自动选择;
@Autowired、Qualifier、Primary 和泛型解析;- singleton 三级缓存和早期代理;
- FactoryBean;
- scope;
- AOP;
- 生命周期和销毁;
- 父子容器;
- Profile/Condition;
- 并发创建协调;
- merged BeanDefinition;
- 依赖关系和销毁顺序;
- 完整异常体系。
所以学习目标是理解主线和设计约束,不是重新实现 Spring。
二、先纠正 @Resource 查找规则
本页教学容器采用以下明确规则:
显式写name
↓
只按该名称查找
↓
不存在直接失败
↓
存在后检查类型未显式写 name:
推导字段名或Setter属性名
↓
默认名称存在则使用并检查类型
↓
默认名称不存在
↓
按类型查找候选
↓
唯一候选才注入
↓
零个或多个都失败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 为什么显式名称不能自动换成别的实现
如果代码明确写:
@Resource(name = "bankPaymentGateway")却因为名称缺失自动注入唯一的 mockPaymentGateway,系统可能把生产支付切到模拟实现。显式名称是配置契约,找不到应在启动阶段失败。
2.2 真实 Spring 的准确边界
真实 Spring 中 @Resource 常由 CommonAnnotationBeanPostProcessor 处理。未显式 name 时会从字段名或 Setter 属性名推导默认名称;在其配置允许默认类型回退、且默认名称不存在时,才可能进入类型解析。显式 name、JNDI 配置、版本和处理器设置会改变路径,因此不能无条件背“先名称,找不到一定按类型”。
完整真实规则见@Autowired与@Resource注入全过程。
三、微型容器的总体架构
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
如果只有:
Map<String, Object> beans;容器只知道“已经有什么对象”,不知道:
- 对象还没创建时应使用哪个 class;
- 是否 singleton;
- 是否 lazy;
- 构造参数是什么;
- 初始化和销毁方法是什么;
- 创建失败后能否重试;
- 类型查询时有哪些候选。
本页最小定义只保存名称和 class:
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 第三方依赖:
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);
}
}
}期望输出:
success=SMS+EMAIL+AUDIT
ambiguous=passed
explicitMissing=passed六、成功 Demo 逐步分析
6.1 显式名称
@MyResource(name = "smsChannel")
private MessageChannel primaryChannel;容器只查 smsChannel,取得 SmsChannel 后验证它实现 MessageChannel。即使 EmailChannel 也是同类型,也不会参与选择。
6.2 默认字段名称
@MyResource
private MessageChannel emailChannel;未显式 name,默认名称是字段名 emailChannel。定义中存在该名称,所以直接按名称使用;不会因为还有 SmsChannel 而发生歧义。
6.3 Setter 属性名与唯一类型回退
@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,但其状态、销毁和异常处理更完整。
八、反射注入全过程
字段注入:
扫描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:
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 Map | DefaultListableBeanFactory定义注册表 | 真实定义元数据丰富且可合并 |
| singletonObjects | DefaultSingletonBeanRegistry一级缓存 | 真实还有早期缓存、创建锁和销毁关系 |
| currentlyCreating | singletonsCurrentlyInCreation等状态 | 真实区分singleton/prototype并发与异常 |
| instantiate | createBeanInstance | 真实支持构造器、Supplier、工厂方法等 |
| injectResources | CommonAnnotationBeanPostProcessor | 真实是BPP并缓存InjectionMetadata |
| resolveResource | BeanFactory依赖解析 | 真实支持限定符、泛型、父容器等 |
| field.set/method.invoke | ResourceElement注入 | 真实还有访问器、异常包装和AOT边界 |
| refresh遍历 | preInstantiateSingletons | 真实先完成BFPP/BPP和多种基础设施 |
| clear on failure | refresh失败销毁和缓存清理 | 真实执行完整destroy和状态回滚 |
十一、为什么真实 Spring 使用 BeanPostProcessor
如果把 Resource 扫描硬编码在 BeanFactory 的 createBean 中:
- 容器核心与每种注解耦合;
- 新增注解必须修改核心源码;
- 无法独立排序和替换;
- 测试和扩展困难。
Spring 通过 CommonAnnotationBeanPostProcessor 接入 Bean 生命周期:
- 合并定义阶段查找并缓存 Resource 元数据;
- 属性填充阶段执行注入;
- 同一处理器体系还可处理部分生命周期注解;
- BeanFactory 只提供候选和依赖解析能力;
- 新处理器可以用扩展点加入,不修改核心工厂。
这是开闭原则在框架中的实际应用。
十二、真实 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
- 确认对象是不是 Spring 创建;
- 检查是否手工 new、反序列化或第三方回调创建;
- 检查字段是否 static;
- 检查当前代码是否在构造器里过早读取字段;
- 检查 CommonAnnotationBeanPostProcessor 是否注册;
- 检查测试上下文是否加载该 Bean;
- 检查 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。
十六、扩展练习
按顺序练习,每增加功能先写失败测试:
- 增加
getBean(Class<T>),多候选必须失败; - 增加 lazy 定义,理解 refresh 与 getBean;
- 增加 prototype,禁止进入 singletonObjects;
- 增加 init/destroy 回调;
- 缓存字段和方法 InjectionMetadata;
- 增加构造器注入,但不要使用“参数最多”简化规则;
- 增加依赖关系图和销毁顺序;
- 增加受控 early reference,再观察循环依赖风险;
- 增加 BeanPostProcessor 接口,把 Resource 扫描移出核心;
- 对照 Spring 源码删除教学容器,回到真实框架。
十七、面试标准回答
@Resource 是 JVM 自动注入吗
不是。注解只是运行期元数据。Spring 中通常由 CommonAnnotationBeanPostProcessor 扫描字段或方法、缓存注入元数据,在属性填充阶段根据名称和类型规则从BeanFactory解析依赖,再反射字段或调用方法完成注入。
显式 name 找不到会自动按类型吗
不能无条件这么说。显式名称通常代表明确资源契约,找不到应失败;未显式name时会推导字段或属性名,在Spring处理器允许默认类型回退且默认名称不存在时,才可能按类型解析。具体还受版本和处理器配置影响。
为什么按类型有多个实现不能选第一个
遍历顺序不是业务语义,注册顺序改变会导致注入实现变化。应使用显式name、Qualifier、唯一Primary或集合策略,并让无法唯一选择时启动失败。
手写容器与真实 Spring 最大区别是什么
手写容器只演示定义、创建、查找和反射注入主线;真实Spring还有后置处理器、复杂候选算法、构造器解析、作用域、三级缓存、早期代理、FactoryBean、生命周期、父子容器、并发协调、异常回滚和销毁等完整基础设施。
十八、关联知识与验收
掌握验收:
- 能解释注解为什么不会自己执行;
- 能说清显式name、默认名称和类型回退;
- 能运行成功、歧义和显式名称缺失三个测试;
- 能解释BeanDefinition与singleton实例Map的区别;
- 能说明为什么多候选不能随机选第一个;
- 能画出字段和Setter反射注入流程;
- 能解释为什么教学容器不支持循环依赖;
- 能把每个教学方法映射到真实Spring组件;
- 能说明javax到jakarta迁移边界;
- 能根据异常链完成生产注入排查。
