写在前面
这是手搓轮子的第一场硬仗。Spring 的核心就是 IOC,面试官最爱问的也是 IOC,比如"说一下 Bean 的生命周期"、"循环依赖怎么解决的"。这些问题我背过很多遍,但背完就忘,因为没有手感。这次决定从零手搓一个 IOC 容器,搞懂它的底层到底在干什么。
本篇是 java-handmade-wheels 项目下的第二个子模块 handmade-ioc,它依赖上一篇的 handmade-common。项目结构如下:
java-handmade-wheels/ ← 父项目(统一管理版本和公共配置)
├── pom.xml ← 父 pom,packaging 为 pom
├── handmade-common/ ← 第一篇已完成:工具箱
│ ├── pom.xml
│ └── src/main/java/com/flittly/handmade/common/
│ ├── exception/HandmadeException.java
│ ├── scanner/ClassScanner.java ← IOC 要用它扫包
│ ├── utils/ReflectionUtils.java ← IOC 要用它反射创建对象
│ ├── utils/StringUtils.java
│ └── utils/AnnotationUtils.java
│
└── handmade-ioc/ ← 本篇:手搓 IOC 容器
├── pom.xml ← 依赖 handmade-common
└── src/main/java/com/flittly/ioc/
├── annotation/ ← @Component、@Autowired
└── context/ ← ApplicationContext 容器
handmade-ioc 的 pom.xml 需要引入 handmade-common 依赖,这样就能直接使用 ClassScanner.scan() 扫包、ReflectionUtils.newInstance() 创建对象,不用自己写反射了。
一、IOC 是什么?为什么需要它?
1.1 IOC 是干什么的
IOC(Inversion of Control,控制反转)是一种设计思想:对象的创建和组装不再由你自己控制,而是交给容器来管理。
没有 IOC 的世界,你得自己 new 对象、自己组装依赖:
UserDao dao = new UserDaoImpl();
UserService service = new UserServiceImpl();
service.setUserDao(dao); // 手动接线
有 IOC 之后,你只需要在类上标个注解,容器帮你搞定一切:
@Component
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao; // 容器自动注入,你不用管
}
1.2 为什么需要它
手动 new 对象有什么问题?
- 耦合度高:
new UserDaoImpl()把代码和具体实现绑死了,换实现类要改代码 - 重复劳动:每个用到 UserService 的地方都要手动创建和组装
- 生命周期难管:单例还是多例?什么时候创建?什么时候销毁?
IOC 就是来解决这些问题的。你只声明"我需要什么"(@Autowired),容器负责"找到它、创建它、注入它"。
1.3 IOC 的本质,一句话
IOC 容器就是一个 Map,key 是类型,value 是对象实例,往里填的过程用反射自动完成。
你以前肯定写过:
Map<String, User> map = new HashMap<>();
map.put("alice", new User());
User u = map.get("alice");
IOC 就是把 key 从 String 换成 Class,put 的过程用反射代替手动 new。就这么简单。
1.4 三个核心概念
| 概念 | 是什么 | 通俗解释 |
|---|---|---|
| Bean | 被 @Component 标注的类实例化出来的对象 | 容器管理的对象 |
| BeanDefinition | Bean 的"图纸",记录类型、名称、作用域 | 对象的配方 |
| 容器 | 存 Bean 的地方,本质是 Map<Class<?>, Object> | 对象仓库 |
我们初版简化处理,不搞 BeanDefinition,直接实例化存 Map。
二、我想要的最终效果
先写一段"假想"的代码,从结果倒推需要实现什么:
// 启动容器,告诉它扫描哪个包
ApplicationContext ctx = new ApplicationContext("com.flittly.ioc.demo");
// 从容器拿 Bean
UserService userService = ctx.getBean(UserService.class);
userService.sayHello();
而 UserService 的定义是这样的:
@Component
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
public void sayHello() {
System.out.println("Hello, " + userDao.findName());
}
}
2.1 ApplicationContext 就是”容器“
注意:ApplicationContext 不是 Java 自带的类,也不是 Spring 的类(虽然 Spring 确实有同名类),而是我们要自己写的类。上面那段"梦想用法"是目标--我们希望最终能这样用,但现在这个类还不存在,需要我们从零实现它。
前面说"容器就是一个 Map",但 Map 只是存数据用的,谁来扫包、谁来创建对象、谁来注入依赖?这些逻辑得有个类来承载,我们给这个类起名叫 ApplicationContext。
它的内部持有一个 Map<Class<?>, Object> 用来存 Bean,同时负责扫包、实例化、注入等全部工作。这里的 Class<?> 就是 UserService.class 这种东西--Class 是 Java 官方用来存放类信息(元数据)的类,.class 语法拿到一个类的 Class 对象,里面记录了这个类有哪些字段、方法、注解等信息。容器用 Class 对象当 key,用实例当 value,getBean(UserService.class) 就是"给我这本说明书对应的实例"。
为什么 new ApplicationContext("com.flittly.ioc.demo") 就是"启动容器"?因为我们要把这些逻辑全写在它的构造函数里--你一 new 它,它就立刻开始扫包、创建对象、注入依赖,构造函数执行完,容器就准备好了,可以直接 getBean 取东西了。传入的参数 com.flittly.ioc.demo 是告诉容器去哪个包下找 Bean。
2.2 倒推:要实现的能力清单
盯着这段代码想:要让它在没有 Spring 的情况下跑起来,需要做什么?
- 定义
@Component和@Autowired注解 - 写一个
ApplicationContext类,构造时扫包 - 找到带
@Component的类,反射实例化,存入 Map - 遍历所有 Bean 的字段,找到
@Autowired的,从 Map 里找依赖并注入 getBean从 Map 里取
三、分阶段实现
实现的过程一共四个阶段,每个阶段都能跑、能验证。这是手搓的核心心法:先跑通再优化。
3.1 阶段一:能存能取(最小可用版)
这个阶段只做一件事:扫包 -> 找 @Component -> new 出来 -> 存进 Map -> getBean 能取出来。不管依赖注入,不管循环依赖。
3.1.1 定义注解
package com.flittly.ioc.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记一个类为 Bean,容器会扫描并实例化它
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value() default ""; // Bean 名称,可选
}
package com.flittly.ioc.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记字段需要容器自动注入
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
注解上面标了两个元注解(注解上的注解),解释一下:
@Target -- 这个注解能标在哪里
ElementType.TYPE:能标在类上(@Component标在类上)ElementType.FIELD:能标在字段上(@Autowired标在字段上)- 还有
METHOD(方法上)、PARAMETER(参数上)等,后续会用到
@Retention -- 这个注解能活多久
注解不是一直存在的,它在什么时候"消失"取决于 @Retention 怎么配。Java 有三个保留级别:
源码阶段 编译阶段 运行阶段
(.java文件) (.class文件) (JVM运行时)
| | |
v v v
SOURCE ---- 存在 ---- 消失 ---- 消失 ← 编译后就没了
CLASS ---- 存在 ---- 存在 ---- 消失 ← .class里有,但运行时读不到
RUNTIME ---- 存在 ---- 存在 ---- 存在 ← 运行时还在,反射能读到
SOURCE:注解只在源码里存在,编译成.class后就被丢弃了。比如@Override--它的作用只是告诉编译器检查一下是不是真的覆写了父类方法,编译完就没用了CLASS(默认):注解会写进.class文件,但 JVM 加载类时不会把它读进内存,运行时用反射读不到RUNTIME:注解会写进.class文件,而且 JVM 加载类时也会把它读进内存,程序运行时能用反射读到
我们的 @Component 必须用 RUNTIME,因为 IOC 容器是在程序运行时用反射检查类上有没有 @Component:
// 程序运行时,IOC 容器执行这段代码
if (clazz.isAnnotationPresent(Component.class)) {
// 如果 @Retention 不是 RUNTIME,这里永远返回 false
// 因为注解在运行时已经不存在了,容器什么都扫不到
Object instance = ReflectionUtils.newInstance(clazz);
}
如果写成 SOURCE,编译后 @Component 就没了,IOC 容器运行时看不到任何注解。如果写成 CLASS(不写 @Retention 默认就是 CLASS),.class 文件里有注解,但 JVM 没把它读进内存,反射同样读不到。想让反射在运行时读到注解,必须配 @Retention(RUNTIME)。
3.1.2 写 ApplicationContext(最小版)
这个版本的 ApplicationContext 只做两件事:扫包实例化、按类型取 Bean。没有依赖注入,没有接口匹配。
package com.flittly.ioc.context;
import com.flittly.handmade.common.scanner.ClassScanner;
import com.flittly.handmade.common.utils.ReflectionUtils;
import com.flittly.ioc.annotation.Component;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* IOC 容器(阶段一:最小可用版)
* 只支持:扫包实例化 + 按类型精确匹配取 Bean
*/
public class ApplicationContext {
/**
* Bean 容器:类型 -> 实例
* 比如 {DemoComponent.class -> DemoComponent实例}
* 这就是前面说的"容器本质是一个 Map"
*/
private final Map<Class<?>, Object> beanMap = new ConcurrentHashMap<>();
/**
* 构造容器,传入要扫描的包名
* new 的瞬间就完成扫包和实例化,构造函数执行完容器就准备好了
*/
public ApplicationContext(String basePackage) {
try {
registerBeans(basePackage);
} catch (Exception e) {
throw new RuntimeException("IOC 容器初始化失败", e);
}
}
/**
* 扫描包,找到所有 @Component 标注的类,实例化并放入容器
*/
private void registerBeans(String basePackage) throws Exception {
// 用 common 里的 ClassScanner 扫描包,拿到这个包下所有的 Class 对象
List<Class<?>> classes = ClassScanner.scan(basePackage);
for (Class<?> clazz : classes) {
// 检查这个类上有没有标 @Component
if (clazz.isAnnotationPresent(Component.class)) {
// 跳过接口和抽象类,因为它们不能 new
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) {
continue;
}
// 反射创建实例:相当于 new XxxService()
Object instance = ReflectionUtils.newInstance(clazz);
// 存进容器:key 是类,value 是实例
beanMap.put(clazz, instance);
}
}
}
/**
* 从容器获取 Bean(阶段一:直接精确匹配)
* 传入的类型必须和 Map 里的 key 完全一致才能找到
*/
@SuppressWarnings("unchecked")
public <T> T getBean(Class<T> type) {
return (T) beanMap.get(type);
}
}
注意这个版本的 getBean 很简单,直接 beanMap.get(type) 精确匹配。如果传 UserService.class 但 Map 里的 key 是 UserServiceImpl.class,会返回 null。这个问题在阶段三解决。
@SuppressWarnings("unchecked") 是压住编译器警告的。因为 beanMap.get(type) 返回 Object,强转成 T 时编译器无法确认转换是否安全,会亮黄线警告。加这个注解就是告诉编译器"我知道有风险,别警告了"。
3.1.3 写测试
先写一个最简单的 Bean,放在 src/main/java/com/flittly/ioc/demo/ 下:
package com.flittly.ioc.demo;
import com.flittly.ioc.annotation.Component;
@Component
public class DemoComponent {
public void hello() {
System.out.println("I am a bean!");
}
}
测试类放在 src/test/java/com/flittly/ioc/ 下:
package com.flittly.ioc;
import com.flittly.ioc.context.ApplicationContext;
import com.flittly.ioc.demo.DemoComponent;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ApplicationContextTest {
@Test
void testGetBean() {
// 启动容器,扫描 com.flittly.ioc.demo 包
ApplicationContext ctx = new ApplicationContext("com.flittly.ioc.demo");
// 从容器取出 DemoComponent 的实例
DemoComponent bean = ctx.getBean(DemoComponent.class);
// 确认拿到了,不是 null
assertNotNull(bean);
// 调用方法,应该打印 "I am a bean!"
bean.hello();
}
}
跑一下,打印出 "I am a bean!",阶段一就过了。虽然还没有依赖注入,但你已经有了一个能用的容器。
3.2 阶段二:依赖注入
阶段一的容器能存能取,但 Bean 之间的依赖还没自动注入。如果 UserService 里有 @Autowired private UserDao userDao,容器不会帮你塞值,userDao 是 null。
这个阶段升级 ApplicationContext:加上 injectDependencies 方法,让容器自动注入依赖。
3.2.1 写业务类
这些类都放在 src/main/java/com/flittly/ioc/demo/ 下。
注意:这个阶段 UserDao 是具体类,不是接口。因为阶段二的 getBeanByType 只支持精确类型匹配,字段类型和 Bean 类型必须完全一致才能找到。接口类型的注入在阶段三解决。
package com.flittly.ioc.demo;
import com.flittly.ioc.annotation.Component;
/**
* UserDao(阶段二暂用具体类,阶段三会改成接口)
*/
@Component
public class UserDao {
public String findName() {
return "Alice";
}
}
package com.flittly.ioc.demo;
import com.flittly.ioc.annotation.Autowired;
import com.flittly.ioc.annotation.Component;
/**
* UserService 依赖 UserDao
* userDao 字段标了 @Autowired,容器会自动注入
*/
@Component
public class UserService {
@Autowired
private UserDao userDao;
public void sayHello() {
// 如果注入成功,userDao 不是 null,能正常调用
// 如果注入失败,userDao 是 null,这里会 NullPointerException
System.out.println("Hello, " + userDao.findName());
}
}
3.2.2 升级 ApplicationContext
在阶段一的基础上新增两个方法:injectDependencies(遍历 Bean 注入依赖)和 getBeanByType(按类型找 Bean)。构造函数里也要加上调用。
新增的代码:
/**
* 构造函数升级:加一步注入依赖
*/
public ApplicationContext(String basePackage) {
try {
registerBeans(basePackage);
injectDependencies(); // ← 新增:第二步,注入依赖
} catch (Exception e) {
throw new RuntimeException("IOC 容器初始化失败", e);
}
}
/**
* 新增方法:遍历所有 Bean,对 @Autowired 字段进行注入
*/
private void injectDependencies() {
// 遍历容器里每一个已创建的 Bean
for (Map.Entry<Class<?>, Object> entry : beanMap.entrySet()) {
Object bean = entry.getValue(); // Bean 实例
Class<?> clazz = entry.getKey(); // Bean 的类型
// 获取这个 Bean 的所有字段(包括 private)
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 检查字段上有没有标 @Autowired
if (field.isAnnotationPresent(Autowired.class)) {
// 有 @Autowired!按字段类型从容器里找对应的 Bean
Object dependency = getBeanByType(field.getType());
if (dependency != null) {
// 用反射把找到的 Bean 塞进这个字段
ReflectionUtils.setField(field, bean, dependency);
}
}
}
}
}
/**
* 新增方法:按类型从容器里找 Bean
* 阶段二:只支持精确匹配(beanMap.get(type))
* 阶段三会升级为支持接口类型查找
*/
private Object getBeanByType(Class<?> type) {
return beanMap.get(type);
}
需要额外 import:
import com.flittly.ioc.annotation.Autowired;
import java.lang.reflect.Field;
为什么必须先全部实例化,再统一注入?
关键在于 injectDependencies 的执行顺序:先实例化所有 Bean,再统一注入。不能边实例化边注入,因为 A 依赖 B 时,B 可能还没被实例化。所以构造函数里先调 registerBeans(全部 new 完),再调 injectDependencies(统一注入)。
坑:private 字段要先 setAccessible(true)
有个坑:注入 private 字段时,一定要 setAccessible(true),否则会报 IllegalAccessException。这个在 ReflectionUtils.setField 里已经处理了。
3.2.3 写测试验证
在 ApplicationContextTest 里加一个测试方法:
@Test
void testDependencyInjection() {
// 启动容器,扫描 demo 包
ApplicationContext ctx = new ApplicationContext("com.flittly.ioc.demo");
// 从容器拿 UserService
UserService service = ctx.getBean(UserService.class);
// 确认拿到了
assertNotNull(service);
// 调用 sayHello,内部会用到注入的 userDao
// 如果注入成功,打印 "Hello, Alice"
// 如果注入失败(userDao 是 null),这里会 NullPointerException
service.sayHello();
}
跑一下,打印出 "Hello, Alice" 就说明依赖注入成功了。
3.3 阶段三:接口类型注入
阶段二的注入能跑了,但有个限制:字段类型必须和 Bean 类型完全一致。实际开发中,字段类型通常是接口,而 Bean 类型是实现类,精确匹配找不到。
这个阶段升级 getBeanByType,让它能通过接口类型找到实现类。
3.3.1 把 UserDao 改成接口
package com.flittly.ioc.demo;
/**
* UserDao 接口
*/
public interface UserDao {
String findName();
}
package com.flittly.ioc.demo;
import com.flittly.ioc.annotation.Component;
/**
* UserDao 实现类,标了 @Component,容器会创建它的实例
*
* 说明:在实际项目中用 MyBatis 时,Dao 层通常只写接口不写实现类,
* MyBatis 会用动态代理自动生成实现。但这里我们还没有手搓 ORM(那是第五篇的事),
* 没有动态代理,接口就没有实现类,IOC 容器扫不到任何东西,注入的就是 null。
* 所以这里临时手写一个 UserDaoImpl 作为占位,等第五篇手搓完 ORM 就不需要了。
*/
@Component
public class UserDaoImpl implements UserDao {
@Override
public String findName() {
return "Alice";
}
}
UserService 里的字段类型不变,还是 @Autowired private UserDao userDao。但现在 UserDao 是接口,beanMap 里存的 key 是 UserDaoImpl.class(实现类),直接 beanMap.get(UserDao.class) 返回 null。
3.3.2 升级 getBeanByType
在精确匹配的基础上,加一步遍历找子类或实现类:
/**
* 按类型从容器里找 Bean(阶段三升级版)
* 先精确匹配,没找到就遍历找子类或实现类
*/
private Object getBeanByType(Class<?> type) {
// 第一步:精确匹配(阶段二的逻辑)
Object bean = beanMap.get(type);
if (bean != null) {
return bean;
}
// 第二步:遍历容器,找子类或实现类(阶段三新增)
// type.isAssignableFrom(entry.getKey()) 的意思是:
// "type 是不是 entry.getKey() 的父类或接口"
// 比如 type=UserDao.class, entry.getKey()=UserDaoImpl.class -> true
for (Map.Entry<Class<?>, Object> entry : beanMap.entrySet()) {
if (type.isAssignableFrom(entry.getKey())) {
return entry.getValue();
}
}
return null;
}
3.3.3 升级 getBean
阶段一的 getBean 直接用 beanMap.get(type),只支持精确匹配。现在要改成走 getBeanByType,这样外部调用 getBean(UserService.class) 也能通过接口找到实现类:
/**
* 从容器获取 Bean(阶段三升级版)
* 改为走 getBeanByType,支持接口类型查找
*/
@SuppressWarnings("unchecked")
public <T> T getBean(Class<T> type) {
return (T) getBeanByType(type); // 改这里:从 beanMap.get 改成 getBeanByType
}
3.3.4 isAssignableFrom 是什么
A.isAssignableFrom(B) 是 Class 类的方法,意思是:"A 能不能接受 B 类型的值?" 等价于 "B 能不能赋值给 A?"
UserService service = new UserServiceImpl(); // 能赋值
// 所以
UserService.class.isAssignableFrom(UserServiceImpl.class) // true
Object o = "abc"; // Object 是 String 的父类
String s = o; // 编译报错!父类引用不能直接赋给子类
// 所以
String.class.isAssignableFrom(Object.class) // false
简单记:A.isAssignableFrom(B) 为 true,就说明 B 是 A 的子类或实现类(或 B 就是 A 本身)。
3.3.5 多实现问题
如果 UserDao 有两个实现类 UserDaoImpl 和 UserDaoMysqlImpl,上面的遍历会取到最后一个。Spring 用 @Qualifier 解决这个问题,指定具体用哪个。我们初版可以先不管。
3.3.6 测试验证
测试代码不需要改,和阶段二一样:
@Test
void testDependencyInjection() {
ApplicationContext ctx = new ApplicationContext("com.flittly.ioc.demo");
UserService service = ctx.getBean(UserService.class);
assertNotNull(service);
service.sayHello(); // 打印 "Hello, Alice"
}
虽然 UserDao 从具体类变成了接口,但测试依然能过--因为 getBeanByType 升级后能通过 isAssignableFrom 找到 UserDaoImpl。
3.4 阶段四:循环依赖(三级缓存)
这是面试最高频的问题,也是手搓 IOC 最难的部分。
3.4.1 什么是循环依赖
循环依赖就是两个 Bean 互相依赖:A 需要 B,B 又需要 A。
举个例子,假设有两个 Bean:
@Component
public class UserServiceImpl implements UserService {
@Autowired
private OrderService orderService; // A 依赖 B
}
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private UserService userService; // B 依赖 A
}
这里的 A 就是 UserServiceImpl 的实例,B 就是 OrderServiceImpl 的实例。"创建 A" 指的是用反射实例化(new UserServiceImpl()),"发现 A 需要 B" 指的是注入依赖时扫到 @Autowired 字段,发现需要一个 OrderService 类型的 Bean。
容器处理时的死循环过程:
问题出在第 5 步:A 还在创建过程中(第 1 步 new 了但第 2 步注入还没完成),容器又想重新创建 A,就无限递归了。
3.4.2 Spring 的解法:三级缓存
核心思路是:提前暴露半成品。A 刚 new 出来(还没注入依赖),就先把 A 的引用暴露出去。这样 B 创建时需要 A,就能拿到 A 的半成品引用。
三个 Map 的含义:
| 缓存 | 类型 | 存什么 |
|---|---|---|
| 一级缓存 singletonObjects | Map<String, Object> | 完整的 Bean(实例化+注入都完成了) |
| 二级缓存 earlySingletonObjects | Map<String, Object> | 半成品 Bean(实例化了但没注入完) |
| 三级缓存 singletonFactories | Map<String, ObjectFactory> | Bean 的工厂(用来生成半成品引用) |
注意,"三级"不是说它最优先,编号是按成熟度排的:一级存的是最成熟的成品,三级存的是最早期的工厂。存和取的顺序是反的:
存的时候:三级 -> 二级 -> 一级
new 出来后 -> 存三级缓存(工厂)
↓ 被别人循环依赖时
移到二级缓存(半成品),从三级删除
↓ 注入完成
存入一级缓存(成品),从二级删除
取的时候:一级 -> 二级 -> 三级
先查一级缓存(有没有成品?)
↓ 没有
再查二级缓存(有没有半成品?)
↓ 没有
再查三级缓存(有没有工厂?),调工厂拿到半成品,移到二级
执行流程:
为什么要三级而不是两级?因为如果 A 被 AOP 代理了,三级缓存里的工厂可以决定返回原始对象还是代理对象。但我们初版没有 AOP,用两级就够了。
3.4.3 一个故事帮你理解三级缓存
上面讲的有点抽象,打个比方你就明白了。
餐馆与养猪场的故事
从前,有两个人想合伙做生意,一个叫餐馆老板(Bean B),一个叫养猪场老板(Bean A)。
有一天,餐馆老板准备开业了(开始创建 Bean B)。但他发现一个致命问题:没有猪肉,餐馆根本没法运转(B 依赖 A)。于是他赶紧去找养猪场老板。
养猪场老板一拍大腿:"没问题!但我得先建养猪场、买猪仔、养大了才能供应你猪肉。"(开始创建 Bean A)
可是,建养猪场需要一笔贷款,银行的要求是:必须有餐馆签的排他性采购协议,证明养猪场未来不愁销路,银行才肯放贷。养猪场老板去找餐馆老板签协议,餐馆老板说:"我可以签协议,但你得先把养猪场的地址写给我,不然我以后去哪拉猪?"
养猪场需要餐馆的协议才能贷款,餐馆需要养猪场的地址才肯签协议。两个人面面相觑,陷入了死循环。(这就是循环依赖)
就在这僵局之下,养猪场老板脑子一转:"兄弟,我现在虽然养猪场还没建,但我可以给你一个未来的地址(比如'幸福路 88 号',我计划把养猪场建在这)。你先把协议签了,等你的餐馆装修好了,我的养猪场也刚好建在那个地址上了。"(写上预期地址的纸条,就是 Java 里的对象引用,也就是内存地址)
餐馆老板一听:"行,我先收着这个地址。"他把这张写着未来地址的纸条收进自己的"供应商名录"里(这就是二级缓存,存半成品引用)。然后签约、盖章,把签好的采购协议给了养猪场老板。接着餐馆老板安心地去搞装修、招厨师、定菜单了(B 拿到了 A 的半成品引用,完成了自己的属性注入和初始化)。
养猪场老板拿着餐馆签好的协议,找银行贷到了款,在"幸福路 88 号"上建起了养猪场,买了猪仔,猪也养得白白胖胖(A 完成了属性注入和初始化,变成成品)。
等餐馆彻底装修完毕开业了(B 变成成品,进入一级缓存),老板拿着当初那张写着"幸福路 88 号"的纸条去拉猪,发现地址没变,但里面已经全是养好的大肥猪了(B 拿到的 A 的半成品引用,最终指向了完整的成品 A)。两人完美合作,生意兴隆!
那为什么 Spring 还要搞出三级缓存呢?
如果故事到这里就结束了,那确实只需要"供应商名录"(二级缓存)就够了。但现实往往比故事更复杂。
假设餐馆老板是个很讲究的人,他要求:"我不管你的猪是怎么养的,但我拿到的猪肉,必须是经过特殊排酸处理的顶级猪肉!"(这就相当于 Spring 里的 AOP 动态代理,需要对原始对象进行包装增强)。
如果只用二级缓存,养猪场老板只能把刚建好的、还没处理的普通养猪场地址给餐馆。等餐馆开业需要猪肉时,发现猪肉没经过排酸处理,这就出大问题了!
所以,Spring 引入了三级缓存(对象工厂 ObjectFactory)。养猪场老板给餐馆的不再是单纯的地址,而是一张"提货券"。这张券上写着:"当你需要猪肉时,凭此券来找我,我不仅给你猪肉,还会当场帮你做好排酸处理!"
这样一来,不管餐馆老板什么时候来拉猪,拿到的都是经过完美包装的顶级猪肉。这就是三级缓存解决 AOP 代理问题的核心原理!
故事和技术的对应关系
| 故事里的概念 | 技术里的概念 |
|---|---|
| 餐馆老板 | Bean B(先完成创建的一方) |
| 养猪场老板 | Bean A(后完成创建的一方) |
| 写着未来地址的纸条 | Java 对象引用(内存地址) |
| 供应商名录 | 二级缓存 earlySingletonObjects(存半成品) |
| 装修好的餐馆 / 建好的养猪场 | 一级缓存 singletonObjects(存成品) |
| 采购协议 | A 需要注入的 B 的引用(A 依赖 B) |
| 提货券(能做排酸处理) | 三级缓存 singletonFactories(ObjectFactory,能做 AOP 代理) |
| 排酸处理 | AOP 动态代理(对原始对象包装增强) |
3.4.4 为什么构造器注入不能解决循环依赖
三级缓存的前提是"先 new 出来,再注入依赖"。但构造器注入是"new 的时候就需要依赖",没法先 new 再注入,所以没法提前暴露半成品。这也是 Spring 的行为,面试时说出来加分。
3.4.5 代码升级
循环依赖的代码改动比较大,核心升级点有四个。
改动一:数据结构从单个 Map 升级为六种数据容器
阶段三只有一个 beanMap,阶段四需要六个:
// 一级缓存:完整的 Bean(实例化+注入都完成了)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
// 二级缓存:半成品 Bean(实例化了但没注入完,循环依赖时对方从这里拿)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>();
// 三级缓存:Bean 工厂,提前暴露半成品引用,可决定是否做 AOP 代理
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>();
// 正在创建中的 Bean,用来检测循环依赖(如果发现已在创建中就说明是循环依赖了)
private final Set<String> singletonsCurrentlyInCreation =
Collections.newSetFromMap(new ConcurrentHashMap<>());
// Bean 名称 -> Class 映射,用来根据名称找到类(相当于 BeanDefinition)
private final Map<String, Class<?>> beanDefinitionMap = new ConcurrentHashMap<>();
// Class -> Bean 名称映射,按类型找 Bean 时先用这个快速定位
private final Map<Class<?>, String> typeToNameMap = new ConcurrentHashMap<>();
改动二:构造函数不再手动调两个方法,而是批量创建
public ApplicationContext(String basePackage) {
try {
// 第一步:扫描注册(只记名字和类型,不创建实例)
registerBeanDefinitions(basePackage);
// 第二步:逐个创建(内部会递归处理依赖和循环依赖)
for (String beanName : beanDefinitionMap.keySet()) {
getBean(beanName);
}
} catch (Exception e) {
throw new RuntimeException("IOC 容器初始化失败", e);
}
}
改动三:注册方法加上了类型映射
private void registerBeanDefinitions(String basePackage) throws Exception {
List<Class<?>> classes = ClassScanner.scan(basePackage);
for (Class<?> clazz : classes) {
if (!clazz.isAnnotationPresent(Component.class)) continue;
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) continue;
// 生成 Bean 名称:类名首字母小写
String beanName = clazz.getSimpleName();
beanName = Character.toLowerCase(beanName.charAt(0)) + beanName.substring(1);
// 存两份映射:名称->类(创建用),类->名称(查找用)
beanDefinitionMap.put(beanName, clazz);
typeToNameMap.put(clazz, beanName);
// 还要记录类实现的接口,这样按接口类型也能找到
for (Class<?> iface : clazz.getInterfaces()) {
typeToNameMap.putIfAbsent(iface, beanName);
}
}
}
改动四:核心方法 getBean(String) 实现三级缓存逻辑
这是整个 IOC 最核心的方法,也是循环依赖处理的关键。它替代了阶段三的 getBean(Class) + getBeanByType + registerBeans + injectDependencies 四个方法,把创建和查找合并成一个递归调用:
/**
* 按 Bean 名称获取实例(核心方法,包含三级缓存和循环依赖处理)
*
* 查找顺序:一级 -> 二级 -> 三级 -> 创建
* 存的时候按创建进度:三级 -> 二级 -> 一级
*/
@SuppressWarnings("unchecked")
private <T> T getBean(String beanName) {
// 【取】先查一级缓存:有没有成品?
Object bean = singletonObjects.get(beanName);
if (bean != null) return (T) bean;
// 【取】再查二级缓存:有没有半成品?(循环依赖时对方从这里拿)
bean = earlySingletonObjects.get(beanName);
if (bean != null) return (T) bean;
// 【取】再查三级缓存:有没有工厂?(第一次被循环依赖时在这里)
ObjectFactory<?> factory = singletonFactories.get(beanName);
if (factory != null) {
// 调工厂拿到半成品引用,放进二级缓存,再从三级删除
// 这样半成品只生成一次,之后其他人直接拿二级缓存,不会再调工厂
bean = factory.getObject();
earlySingletonObjects.put(beanName, bean);
singletonFactories.remove(beanName);
return (T) bean;
}
// 防死循环检查:走到这里说明一级二级三级都没有这个 Bean,它却还在创建中
// 这只可能发生在构造器注入的循环依赖上(new 就需要参数,来不及暴露半成品)
if (singletonsCurrentlyInCreation.contains(beanName)) {
throw new RuntimeException("循环依赖无法解决: " + beanName);
}
// 下面开始创建
Class<?> clazz = beanDefinitionMap.get(beanName);
if (clazz == null) throw new RuntimeException("找不到 Bean: " + beanName);
// 标记为"正在创建中",防止重复创建
singletonsCurrentlyInCreation.add(beanName);
// 【存】实例化 -> 放入三级缓存(工厂,提前暴露半成品引用)
Object instance = ReflectionUtils.newInstance(clazz);
singletonFactories.put(beanName, () -> instance); // lambda:返回半成品引用
// 依赖注入:如果依赖的 Bean 还没创建,getBean(depName) 会递归创建它
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Autowired.class)) {
String depName = typeToNameMap.get(field.getType());
// 递归调用 getBean!如果对方也依赖自己,就会在二/三级缓存拿到自己的半成品
Object dependency = getBean(depName);
ReflectionUtils.setField(field, instance, dependency);
}
}
// 【存】注入完成,升级为成品 -> 放入一级缓存,清理二三级缓存和创建标记
singletonObjects.put(beanName, instance);
earlySingletonObjects.remove(beanName);
singletonFactories.remove(beanName);
singletonsCurrentlyInCreation.remove(beanName);
return (T) instance;
}
循环依赖为什么能解决?关键在"放入三级缓存"和"递归调用"这两步。当 A 需要 B,getBean(B) 被递归调用,B 又需要 A,getBean(A) 再次被调用--但这次 A 在第一步(一级缓存)和第二步(二级缓存)都查不到,走到第三步(三级缓存)时发现里面有 A 的工厂,于是调工厂拿到 A 的半成品引用,并挪进二级缓存,返回给 B。B 注入完成进一级缓存;回到 A 的注入流程,A 注入 B 完成,A 也进一级缓存。全程没有重新创建 A,死循环被打破。
对外暴露的 getBean(Class) 方法也需要升级,改为按类型找名称再调用 getBean(name):
@SuppressWarnings("unchecked")
public <T> T getBean(Class<T> type) {
String beanName = typeToNameMap.get(type);
if (beanName == null) {
// 按类型没找到,遍历找子类/实现类
for (Map.Entry<Class<?>, String> entry : typeToNameMap.entrySet()) {
if (type.isAssignableFrom(entry.getKey())) {
beanName = entry.getValue();
break;
}
}
}
if (beanName == null) throw new RuntimeException("找不到类型为 " + type.getName() + " 的 Bean");
return getBean(beanName);
}
四、完整代码
4.1 注解定义
package com.flittly.ioc.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value() default "";
}
package com.flittly.ioc.annotation;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
4.2 ApplicationContext(含三级缓存,完整最终版)
package com.flittly.ioc.context;
import com.flittly.handmade.common.scanner.ClassScanner;
import com.flittly.handmade.common.utils.ReflectionUtils;
import com.flittly.ioc.annotation.Autowired;
import com.flittly.ioc.annotation.Component;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* IOC 容器(最终版)
* 支持:@Component 扫描、@Autowired 注入、接口类型查找、循环依赖(三级缓存)
*/
public class ApplicationContext {
// ==================== 三级缓存 ====================
// 一级缓存:完整的成品 Bean(实例化+注入都完成了,正常使用时从这里取)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
// 二级缓存:半成品 Bean(实例化了但还没注入完,循环依赖时对方从这里拿)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>();
// 三级缓存:Bean 工厂(刚 new 出来时先放这里,可以决定返回原始对象还是 AOP 代理对象)
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>();
// ==================== 辅助数据结构 ====================
// 正在创建中的 Bean 名称集合,用于检测循环依赖
private final Set<String> singletonsCurrentlyInCreation =
Collections.newSetFromMap(new ConcurrentHashMap<>());
// Bean 名称 -> Class 的映射(相当于简化版 BeanDefinition),根据名称找到类
private final Map<String, Class<?>> beanDefinitionMap = new ConcurrentHashMap<>();
// Class -> Bean 名称的映射,按类型找 Bean 时先从这快速定位
private final Map<Class<?>, String> typeToNameMap = new ConcurrentHashMap<>();
/**
* 启动容器:扫描包、注册 Bean 定义、逐一创建 Bean
*/
public ApplicationContext(String basePackage) {
try {
// 第一步:扫描注册,只记名字和类型的对应关系
registerBeanDefinitions(basePackage);
// 第二步:逐个创建,getBean(name) 内部会递归处理依赖和循环依赖
for (String beanName : beanDefinitionMap.keySet()) {
getBean(beanName);
}
} catch (Exception e) {
throw new RuntimeException("IOC 容器初始化失败", e);
}
}
/**
* 扫描包,记录所有 @Component 类的名称和类型映射
* 这一步只记录"有哪些 Bean",不创建实例
*/
private void registerBeanDefinitions(String basePackage) throws Exception {
List<Class<?>> classes = ClassScanner.scan(basePackage);
for (Class<?> clazz : classes) {
// 跳过没有 @Component 的类
if (!clazz.isAnnotationPresent(Component.class)) continue;
// 跳过接口和抽象类,它们不能实例化
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) continue;
// 生成 Bean 名称:默认用类名首字母小写(如 UserServiceImpl -> userServiceImpl)
// 如果 @Component 显式指定了名称(如 @Component("myService")),直接用指定的名称
Component component = clazz.getAnnotation(Component.class);
String beanName = component.value();
if (beanName.isEmpty()) {
beanName = clazz.getSimpleName();
beanName = Character.toLowerCase(beanName.charAt(0)) + beanName.substring(1);
}
// 存名称->类映射(创建 Bean 时用)
beanDefinitionMap.put(beanName, clazz);
// 存类->名称映射(按类型查找时用)
typeToNameMap.put(clazz, beanName);
// 还要记录类实现的接口,这样按接口类型也能找到 Bean
for (Class<?> iface : clazz.getInterfaces()) {
typeToNameMap.putIfAbsent(iface, beanName);
}
}
}
/**
* 按名称获取 Bean(核心方法:包含三级缓存的完整创建逻辑)
*
* 查找顺序:一级(成品) -> 二级(半成品) -> 三级(工厂) -> 创建
* 创建时的存入顺序:三级(工厂) -> 二级(半成品) -> 一级(成品)
*/
@SuppressWarnings("unchecked")
private <T> T getBean(String beanName) {
// 【取-第一步】查一级缓存:是不是已经有成品了?
Object bean = singletonObjects.get(beanName);
if (bean != null) return (T) bean;
// 【取-第二步】查二级缓存:有没有半成品?(循环依赖时对方放进去的)
bean = earlySingletonObjects.get(beanName);
if (bean != null) return (T) bean;
// 【取-第三步】查三级缓存:有没有工厂?(第一次被循环依赖时在这里)
ObjectFactory<?> factory = singletonFactories.get(beanName);
if (factory != null) {
// 调工厂拿到半成品引用,放进二级缓存,再从三级删除
// 这样半成品只生成一次,之后其他人直接拿二级缓存,不会再调工厂
bean = factory.getObject();
earlySingletonObjects.put(beanName, bean);
singletonFactories.remove(beanName);
return (T) bean;
}
// 【取-第四步】防死循环检查
// 走到这里说明一级二级三级都没有这个 Bean,它却还在创建中
// 这只可能发生在构造器注入的循环依赖上(new 就需要参数,来不及暴露半成品)
if (singletonsCurrentlyInCreation.contains(beanName)) {
throw new RuntimeException("循环依赖无法解决: " + beanName);
}
// 下面开始从头创建这个 Bean
Class<?> clazz = beanDefinitionMap.get(beanName);
if (clazz == null) throw new RuntimeException("找不到 Bean 定义: " + beanName);
// 标记为"正在创建中",这样如果递归回来发现已经在创建,就走二级或三级缓存
singletonsCurrentlyInCreation.add(beanName);
// 【创建阶段】反射创建半成品(对象有了,但 @Autowired 字段还是 null)
Object instance = ReflectionUtils.newInstance(clazz);
// 【存-第一步】放入三级缓存(工厂),提前暴露半成品引用
// 如果这个 Bean 正在创建时,别的 Bean 也依赖了它,对方就从三级缓存拿到这个半成品
singletonFactories.put(beanName, () -> instance);
// 【依赖注入阶段】
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(Autowired.class)) continue;
// 根据字段类型找到依赖的 Bean 名称
String depName = typeToNameMap.get(field.getType());
if (depName == null) {
throw new RuntimeException("找不到类型为 " + field.getType().getName()
+ " 的 Bean,无法注入到 " + beanName);
}
// 递归获取依赖 Bean(这里就是循环依赖的入口!)
// 如果 depName 也依赖当前正在创建的 beanName,就会走上面的二/三级缓存拿到半成品
Object dependency = getBean(depName);
// 把依赖塞进字段
ReflectionUtils.setField(field, instance, dependency);
}
// 【创建完成】升级为成品,从三级/二级移到一级
singletonObjects.put(beanName, instance);
earlySingletonObjects.remove(beanName);
singletonFactories.remove(beanName);
singletonsCurrentlyInCreation.remove(beanName);
return (T) instance;
}
/**
* 按类型获取 Bean(对外暴露的 API)
* 先按类型查名称,再调 getBean(name) 获取实例
*/
@SuppressWarnings("unchecked")
public <T> T getBean(Class<T> type) {
// 先精确匹配:类型->名称的直接映射
String beanName = typeToNameMap.get(type);
if (beanName == null) {
// 没找到:遍历找子类或实现类
for (Map.Entry<Class<?>, String> entry : typeToNameMap.entrySet()) {
if (type.isAssignableFrom(entry.getKey())) {
beanName = entry.getValue();
break;
}
}
}
if (beanName == null) {
throw new RuntimeException("找不到类型为 " + type.getName() + " 的 Bean");
}
return getBean(beanName);
}
}
/**
* Bean 工厂函数式接口(用于三级缓存)
* 只有一个方法 getObject(),配合 lambda 使用:() -> instance
*/
@FunctionalInterface
interface ObjectFactory<T> {
T getObject();
}
关于 @FunctionalInterface 这个注解:
它是 Java 的一个注解,意思是"函数式接口"--也就是只有一个抽象方法的接口。我们的 ObjectFactory 只有一个方法 getObject(),所以可以加这个注解。
它的作用是让编译器帮你检查:如果不小心加了第二个抽象方法,编译器会报错:
@FunctionalInterface
interface ObjectFactory<T> {
T getObject();
void doSomething(); // 编译报错!函数式接口只能有一个抽象方法
}
不加这个注解的话,加了第二个方法编译器也不会报错,但你就没法用 lambda 了。因为函数式接口只有一个方法,Java 知道你写的 lambda 对应的就是那个方法,不用猜:
// 不用 lambda(匿名内部类,啰嗦)
singletonFactories.put(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() {
return instance;
}
});
// 用 lambda(简洁,等价于上面)
singletonFactories.put(beanName, () -> instance);
() -> instance 的意思是:一个无参方法(()),返回 instance(-> instance)。Java 知道它对应的是 getObject() 方法,因为接口里只有这一个方法。
总结:@FunctionalInterface = 告诉编译器"这个接口只有一个方法,帮我盯着点,别让人加第二个"。加了之后就能用 lambda 简写,代码更简洁。
4.3 测试代码
package com.flittly.ioc.demo;
import com.flittly.ioc.annotation.Component;
public interface UserDao {
String findName();
}
@Component
class UserDaoImpl implements UserDao {
@Override
public String findName() {
return "Alice";
}
}
package com.flittly.ioc.demo;
import com.flittly.ioc.annotation.Autowired;
import com.flittly.ioc.annotation.Component;
public interface UserService {
void sayHello();
}
@Component
class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private OrderService orderService; // 循环依赖
@Override
public void sayHello() {
System.out.println("Hello, " + userDao.findName());
}
}
package com.flittly.ioc.demo;
import com.flittly.ioc.annotation.Autowired;
import com.flittly.ioc.annotation.Component;
public interface OrderService {
void order();
}
@Component
class OrderServiceImpl implements OrderService {
@Autowired
private UserService userService;
@Override
public void order() {
System.out.println("下单成功");
}
}
package com.flittly.ioc;
import com.flittly.ioc.context.ApplicationContext;
import com.flittly.ioc.demo.*;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ApplicationContextTest {
@Test
void testGetBean() {
ApplicationContext ctx = new ApplicationContext("com.flittly.ioc.demo");
UserService service = ctx.getBean(UserService.class);
assertNotNull(service);
service.sayHello();
}
@Test
void testCircularDependency() {
ApplicationContext ctx = new ApplicationContext("com.flittly.ioc.demo");
UserService userService = ctx.getBean(UserService.class);
OrderService orderService = ctx.getBean(OrderService.class);
assertNotNull(userService);
assertNotNull(orderService);
}
}
五、踩过的坑
5.1 ConcurrentModificationException
一开始我在遍历 beanMap 的同时往里放东西,结果并发修改异常。解决方法是用 new HashMap<>(beanMap) 复制一份再遍历,或者像最终版一样用递归创建。
5.2 注入了 null
某个 Bean 的依赖字段注入了 null。debug 了半天发现依赖的类没标 @Component,beanMap 里根本没有它。一定要确保所有需要被注入的类都标了 @Component。
5.3 循环依赖栈溢出
三级缓存没写对,A 创建 B,B 创建 A,无限递归。关键是在实例化之后、注入之前,要把半成品放到缓存里。这样 B 注入 A 时能从缓存拿到半成品,不会重新创建。
六、面试题对照
6.1 Spring Bean 的生命周期?
实例化(new) -> 属性填充(注入依赖) -> 初始化 -> 使用 -> 销毁。我写的 newInstance -> inject -> put singleton 就是这个流程。
6.2 循环依赖怎么解决?
三级缓存。一级存完整 Bean,二级存半成品,三级存工厂。A 实例化后先把半成品暴露到缓存,B 创建时需要 A 就从缓存拿。
6.3 为什么构造器注入不能解决循环依赖?
构造器注入时,实例化就需要参数,没法先 new 再注入,所以没法提前暴露半成品。
6.4 IOC 和 DI 的区别?
IOC 是思想(控制反转,对象创建权交给容器),DI 是实现手段(依赖注入,容器自动填充依赖)。
七、和工业级框架的差距
手搓完 IOC 之后,我觉得最大的收获不是代码本身,而是知道了 Spring 到底比我们多做了多少事。下面列一下主要的差距。
7.1 Bean 生命周期
我们的实现:new -> 注入 -> 放入 Map,就这三步。
Spring 的生命周期(简化版):
实例化 -> 属性填充 -> 各种 Aware 回调(BeanNameAware、BeanFactoryAware)
-> BeanPostProcessor.postProcessBeforeInitialization
-> @PostConstruct -> InitializingBean.afterPropertiesSet -> init-method
-> BeanPostProcessor.postProcessAfterInitialization(AOP 就在这里)
-> 使用
-> @PreDestroy -> DisposableBean.destroy -> destroy-method
Spring 有十几个回调点,每个都可以插自定义逻辑。我们的实现一个都没有。BeanPostProcessor 是 Spring 可扩展性的核心--AOP、自动配置、注解解析全靠它。
7.2 BeanDefinition
我们没有 BeanDefinition,直接 newInstance 创建对象。Spring 会先注册 BeanDefinition(记录类的元信息),再根据 BeanDefinition 来创建。这样可以在创建前做检查、修改、覆盖。
7.3 其他我们没做的功能
| 功能 | 说明 | Spring 怎么做 |
|---|---|---|
@Qualifier | 多实现时按名称指定 | 配合 @Autowired 使用 |
@Primary | 多实现时标记优先 | 标在类上,自动优先选它 |
@Scope("prototype") | 每次取都创建新对象 | 支持 singleton/prototype/request/session |
@Lazy | 第一次用才创建 | 延迟初始化 |
@Value | 注入配置值 | 从 properties/yml 读取 |
FactoryBean | 工厂模式创建 Bean | MyBatis 的 Mapper 就是 FactoryBean |
@Conditional | 条件化注册 | Spring Boot 自动配置的核心 |
| 事件机制 | ApplicationEvent | 发布订阅模式,Bean 之间解耦通信 |
| SpEL 表达式 | @Value("#{'a' + 'b'}") | 运行时表达式求值 |
| 环境隔离 | dev/prod 不同配置 | Environment + Profile |
| 类型转换 | String -> 任意类型 | ConversionService + PropertyEditor |
7.4 三级缓存的差距
我们的三级缓存是简化版。Spring 的三级缓存里存的是 ObjectFactory(一个工厂),调用时会触发 AOP 代理的创建。也就是说,Spring 的三级缓存不仅解决循环依赖,还保证了循环依赖场景下 AOP 代理的正确性。我们的实现没有 AOP 集成,所以这个细节没有处理。
7.5 如果想继续深入
- 看
AbstractApplicationContext.refresh()方法:这是 Spring 容器启动的入口,十几个步骤一目了然 - 研究
BeanPostProcessor:Spring 可扩展性的根基,AOP、自动配置都靠它 - 看
DefaultSingletonBeanRegistry.getSingleton():三级缓存的完整实现,比我们的复杂得多
八、总结
8.1 核心收获
IOC 容器的核心其实就两件事:反射创建对象 + Map 存取。循环依赖看着吓人,本质就是"提前暴露半成品"。手搓一遍之后,再背面试题就不是死记硬背了,而是"我写过的"。
8.2 完整代码结构
handmade-ioc/
└── src/
├── main/java/com/flittly/ioc/
│ ├── annotation/
│ │ ├── Component.java
│ │ └── Autowired.java
│ └── context/
│ └── ApplicationContext.java
└── test/java/com/flittly/ioc/
└── demo/
├── UserDao.java
├── UserDaoImpl.java
├── UserService.java
├── UserServiceImpl.java
├── OrderService.java
├── OrderServiceImpl.java
└── ApplicationContextTest.java
下一篇我们手搓 AOP。AOP 的代理对象最终也要被 IOC 容器管理,所以 IOC 是 AOP 的基础。