【我的手搓轮子日记】(2)handmade-ioc 手搓 IOC 容器

0 阅读32分钟

写在前面

这是手搓轮子的第一场硬仗。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 对象有什么问题?

  1. 耦合度高new UserDaoImpl() 把代码和具体实现绑死了,换实现类要改代码
  2. 重复劳动:每个用到 UserService 的地方都要手动创建和组装
  3. 生命周期难管:单例还是多例?什么时候创建?什么时候销毁?

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 标注的类实例化出来的对象容器管理的对象
BeanDefinitionBean 的"图纸",记录类型、名称、作用域对象的配方
容器存 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 的情况下跑起来,需要做什么?

  1. 定义 @Component@Autowired 注解
  2. 写一个 ApplicationContext 类,构造时扫包
  3. 找到带 @Component 的类,反射实例化,存入 Map
  4. 遍历所有 Bean 的字段,找到 @Autowired 的,从 Map 里找依赖并注入
  5. 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 有两个实现类 UserDaoImplUserDaoMysqlImpl,上面的遍历会取到最后一个。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 的含义:

缓存类型存什么
一级缓存 singletonObjectsMap<String, Object>完整的 Bean(实例化+注入都完成了)
二级缓存 earlySingletonObjectsMap<String, Object>半成品 Bean(实例化了但没注入完)
三级缓存 singletonFactoriesMap<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工厂模式创建 BeanMyBatis 的 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 如果想继续深入

  1. AbstractApplicationContext.refresh() 方法:这是 Spring 容器启动的入口,十几个步骤一目了然
  2. 研究 BeanPostProcessor:Spring 可扩展性的根基,AOP、自动配置都靠它
  3. 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 的基础。