臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

IoC 是把对象交给 Spring 管理,DI 是 Spring 把对象需要的依赖传进来;构造方法注入,就是通过构造参数完成 DI。

二、🧭 核心概念

概念大白话解释
IoC 容器Spring 管理对象的地方
Bean被 Spring 容器管理的对象
DISpring 把 Bean 交给需要它的对象
构造方法注入通过构造参数接收依赖
IoC 管对象 → 对象成为 Bean → DI 负责组装 Bean

三、⚙️ 构造方法注入

@RestController
public class ChatController {
 
    private final ChatClient chatClient;
 
    // Spring 创建 ChatController 时,会查找 ChatClient 类型的 Bean 并传进来。
    public ChatController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }
}

Spring 首先按照构造参数类型寻找 Bean:

候选数量结果
1 个直接注入
0 个启动失败,提示缺少 Bean
多个使用 @Qualifier、@Primary 等进一步选择
仍无法确定启动失败,不会随便选一个
// @Primary:多个同类型 Bean 中的默认选择。
@Bean
@Primary
ChatClient defaultChatClient(ChatClient.Builder builder) {
    return builder.build();
}
 
// @Qualifier:当前注入点明确指定某个 Bean。
public ChatController(
        @Qualifier("businessChatClient") ChatClient chatClient) {
    this.chatClient = chatClient;
}

为什么推荐构造方法注入

依赖清晰,可以使用 final 保证初始化后不被替换,也方便在单元测试中直接传入假对象。只有一个构造方法时,Spring 会自动使用它,因此通常不用写 @Autowired。

四、🔍 容易混淆的地方

4.1 build() 创建的对象不一定是 Bean

public ToolCallingController(ChatClient.Builder builder) {
    // 普通 Java 方法调用:创建新对象,但没有把它注册进 Spring 容器。
    this.chatClient = builder.build();
}

如果对象由 @Bean 方法返回,才会被注册为 Bean:

@Bean
ChatClient chatClient(ChatClient.Builder builder) {
    return builder.build();
}

4.2 构造方法循环依赖

如果 A 的构造方法需要 B,而 B 的构造方法又需要 A,双方都无法先创建,Spring 会抛出异常。

正确思路通常是重新拆分职责,而不是依赖循环引用技巧。

五、🛠️ 按图实现一个简易 IoC 容器

整体流程

读取扫描路径 → 扫描 .class → 判断组件注解 → 反射创建对象 → 存入 Map → 注入依赖。

下面先按图片的顺序实现字段注入。这种方式可以先创建全部对象,再统一给字段赋值。构造方法注入的区别见 5.7。

5.1 第一步:配置包扫描路径

这里的“配置”指的是包扫描范围配置,也就是告诉容器从哪个 Java 包开始找类。

src/main/resources/application.properties:

# IoC 容器从这个包开始扫描
scan.package=com.example.app

读取配置:

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
 
public final class ConfigLoader {
 
    private ConfigLoader() {
    }
 
    public static String getScanPackage() {
        Properties properties = new Properties();
 
        // getResourceAsStream:从 src/main/resources 中读取文件。
        try (InputStream inputStream = ConfigLoader.class
                .getClassLoader()
                .getResourceAsStream("application.properties")) {
 
            if (inputStream == null) {
                throw new IllegalStateException("找不到 application.properties");
            }
 
            properties.load(inputStream);
            String scanPackage = properties.getProperty("scan.package");
 
            if (scanPackage == null || scanPackage.isBlank()) {
                throw new IllegalStateException("没有配置 scan.package");
            }
 
            return scanPackage;
        } catch (IOException e) {
            throw new IllegalStateException("读取配置文件失败", e);
        }
    }
}

最终得到包名 com.example.app。下一步会把它转换为路径 com/example/app。

Spring Boot 通常不用自己写这个配置:@SpringBootApplication 已包含组件扫描,默认从启动类所在包向下扫描。这里自己配置是为了展示手写容器的原理。

5.2 第二步:递归扫描 .class 文件

Java 源文件编译后会变成 .class 文件。扫描的目标,是把包路径下的 .class 加载成 Class<?> 对象。

import java.io.File;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Stream;
 
public final class ClassScanner {
 
    private ClassScanner() {
    }
 
    public static Set<Class<?>> scan(String basePackage) {
        Set<Class<?>> classes = new HashSet<>();
 
        // 将 com.example.app 转换成 com/example/app。
        String packagePath = basePackage.replace('.', '/');
        ClassLoader classLoader = Thread.currentThread()
                .getContextClassLoader();
        URL resource = classLoader.getResource(packagePath);
 
        if (resource == null) {
            throw new IllegalStateException("找不到扫描目录:" + packagePath);
        }
 
        // 最小示例只支持 IDE 直接运行时的普通文件目录。
        if (!"file".equals(resource.getProtocol())) {
            throw new IllegalStateException("当前示例暂不支持扫描 Jar:" + resource);
        }
 
        try {
            Path rootPath = Paths.get(resource.toURI());
 
            // Files.walk:递归遍历当前目录及其全部子目录。
            try (Stream<Path> paths = Files.walk(rootPath)) {
                paths.filter(Files::isRegularFile)
                        .filter(path -> path.toString().endsWith(".class"))
                        // 暂时排除内部类,例如 UserService$1.class。
                        .filter(path -> !path.getFileName().toString().contains("$"))
                        .map(path -> toClass(basePackage, rootPath, path))
                        .forEach(classes::add);
            }
 
            return classes;
        } catch (Exception e) {
            throw new IllegalStateException("扫描 Class 失败", e);
        }
    }
 
    private static Class<?> toClass(
            String basePackage,
            Path rootPath,
            Path classFile) {
 
        // 例如得到 service/UserService.class。
        String relativePath = rootPath.relativize(classFile).toString();
 
        // 去掉 .class,再把路径分隔符换成点。
        String relativeClassName = relativePath
                .substring(0, relativePath.length() - 6)
                .replace(File.separatorChar, '.');
        String className = basePackage + "." + relativeClassName;
 
        try {
            // Class.forName:根据完整类名加载 Class 对象。
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException("无法加载类:" + className, e);
        }
    }
}

例如扫描完成后,集合中保存的是:

UserController.class
UserService.class
UserRepository.class

它们只是描述类结构的 Class 对象,还不是业务对象。

简化边界

真实 Spring 扫描的是 classpath 资源,还需要处理 Jar、过滤规则和 BeanDefinition;上面的代码只用于理解基本原理。

5.3 第三步:定义并识别组件注解

扫描出来的类很多,只有带组件标记的类才交给容器管理。注解必须保留到运行期,反射才能读取。

MyComponent.java:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface MyComponent {
}

MyService.java:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@MyComponent
public @interface MyService {
}

@MyController 和 @MyRepository 的写法相同:它们也在注解类上标记 @MyComponent。

private boolean isComponent(Class<?> clazz) {
    // 类直接使用了 @MyComponent。
    if (clazz.isAnnotationPresent(MyComponent.class)) {
        return true;
    }
 
    // 检查 @MyService 等注解本身是否带有 @MyComponent。
    for (Annotation annotation : clazz.getAnnotations()) {
        if (annotation.annotationType()
                .isAnnotationPresent(MyComponent.class)) {
            return true;
        }
    }
 
    return false;
}

判断过程:

UserService 上有 @MyService
→ @MyService 上有 @MyComponent
→ UserService 应由容器管理

5.4 第四步:反射创建对象并存入 Map

容器遍历 Class 集合,筛选组件类,通过无参构造方法创建对象,再放入 Map。

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
 
public class SimpleIocContainer {
 
    // Key 是类,Value 是创建出来的对象。
    private final Map<Class<?>, Object> beans = new ConcurrentHashMap<>();
 
    public void registerBeans(Set<Class<?>> classes) {
        for (Class<?> clazz : classes) {
            // 接口和抽象类不能直接创建实例。
            if (clazz.isInterface()
                    || Modifier.isAbstract(clazz.getModifiers())) {
                continue;
            }
 
            if (!isComponent(clazz)) {
                continue;
            }
 
            try {
                // 当前图片式实现要求类具有无参构造方法。
                Constructor<?> constructor = clazz.getDeclaredConstructor();
                constructor.setAccessible(true);
 
                // newInstance:相当于执行 new UserService()。
                Object bean = constructor.newInstance();
                beans.put(clazz, bean);
            } catch (ReflectiveOperationException e) {
                throw new IllegalStateException(
                        "创建 Bean 失败:" + clazz.getName(), e);
            }
        }
    }
}

Map 中的数据类似:

UserRepository.class → UserRepository 对象
UserService.class    → UserService 对象
UserController.class → UserController 对象

使用 Class 作为 Key,后面就可以按照依赖类型找对象。ConcurrentHashMap 是线程安全 Map;如果容器只在单线程启动阶段初始化,普通 HashMap 也能工作。

5.5 第五步:给字段注入依赖

先定义注入标记 MyAutowired.java:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyAutowired {
}

业务类:

@MyService
public class UserService {
 
    @MyAutowired
    private UserRepository userRepository;
 
    public String getUserName() {
        return userRepository.findName();
    }
}

容器遍历所有 Bean 的字段,找到 @MyAutowired,再从 Map 中获取对应类型的对象并赋值:

public void injectDependencies() {
    for (Object bean : beans.values()) {
        for (Field field : bean.getClass().getDeclaredFields()) {
            if (!field.isAnnotationPresent(MyAutowired.class)) {
                continue;
            }
 
            Object dependency = findBeanByType(field.getType());
 
            try {
                // 允许给 private 字段赋值。
                field.setAccessible(true);
 
                // 相当于 userService.userRepository = userRepository。
                field.set(bean, dependency);
            } catch (IllegalAccessException e) {
                throw new IllegalStateException("依赖注入失败:" + field, e);
            }
        }
    }
}
 
private Object findBeanByType(Class<?> requiredType) {
    List<Object> matches = beans.entrySet()
            .stream()
            // 当前 Bean 类型可以赋值给字段类型,就说明类型匹配。
            .filter(entry -> requiredType.isAssignableFrom(entry.getKey()))
            .map(Map.Entry::getValue)
            .toList();
 
    if (matches.isEmpty()) {
        throw new IllegalStateException("找不到依赖:" + requiredType.getName());
    }
 
    if (matches.size() > 1) {
        throw new IllegalStateException("存在多个同类型 Bean:" + requiredType.getName());
    }
 
    return matches.get(0);
}

实际过程是:

读取 UserService.userRepository 字段
→ 字段类型是 UserRepository
→ 从 Map 找到 UserRepository 对象
→ field.set(userService, userRepository)
→ 注入完成

5.6 把图片中的步骤串起来

public class Application {
 
    public static void main(String[] args) {
        // 1. 从配置中获取包扫描路径。
        String scanPackage = ConfigLoader.getScanPackage();
 
        // 2. 递归扫描并获得 Class 集合。
        Set<Class<?>> classes = ClassScanner.scan(scanPackage);
 
        SimpleIocContainer container = new SimpleIocContainer();
 
        // 3. 判断组件注解、反射创建对象、存入 Map。
        container.registerBeans(classes);
 
        // 4. 遍历 Bean,为 @MyAutowired 字段注入依赖。
        container.injectDependencies();
 
        // 5. 从容器取出对象并使用。
        UserController controller = container.getBean(UserController.class);
        controller.printUser();
    }
}

getBean():

public <T> T getBean(Class<T> type) {
    Object bean = findBeanByType(type);
    return type.cast(bean);
}

完整调用链:

读取 com.example.app
→ 扫描出 Class
→ 识别组件注解
→ 反射创建对象
→ 存入 Map
→ 给字段注入依赖
→ 从容器获取 Bean

5.7 换成构造方法注入有什么不同

图片里的顺序更适合字段注入。构造方法注入不能先执行无参构造再给字段赋值,而是必须在创建对象前准备好构造参数。

public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
}

容器必须递归创建依赖:

准备创建 UserService
→ 发现构造参数需要 UserRepository
→ 先获取或创建 UserRepository
→ 执行 new UserService(userRepository)

核心代码:

private Object createBean(Class<?> beanClass) {
    try {
        Constructor<?> constructor = beanClass.getDeclaredConstructors()[0];
        Class<?>[] parameterTypes = constructor.getParameterTypes();
 
        // 每个构造参数都递归地从容器获取或创建。
        Object[] dependencies = Arrays.stream(parameterTypes)
                .map(this::getOrCreateBean)
                .toArray();
 
        constructor.setAccessible(true);
        Object bean = constructor.newInstance(dependencies);
        beans.put(beanClass, bean);
        return bean;
    } catch (ReflectiveOperationException e) {
        throw new IllegalStateException(
                "创建 Bean 失败:" + beanClass.getName(), e);
    }
}

此时还要使用一个 Set<Class<?>> 记录“正在创建”的类。如果创建 A 时需要 B,创建 B 时又需要 A,就说明出现了构造器循环依赖,应立即报错,避免无限递归。

与真正 Spring 的边界

这套代码只解释核心链路。真正的 Spring 还会注册 BeanDefinition,并处理 BeanPostProcessor、作用域、生命周期、代理、条件装配等机制。

六、🎤 面试一般怎么问

6.1 30 秒标准回答

IoC 是控制反转,对象不再由业务代码自己创建,而是交给 Spring 容器管理。DI 是依赖注入,容器在创建 Bean 时把它依赖的其他 Bean 注入进来。常见方式有构造方法、Setter 和字段注入,我一般优先使用构造方法注入,因为依赖更明确,可以配合 final,也更方便测试。只有一个构造方法时,可以省略 @Autowired。

6.2 高频追问与短答

6.2.1 IoC 和 DI 有什么区别

IoC 是设计思想:对象的创建权交给容器。DI 是实现 IoC 的主要方式:容器把依赖传给对象。

6.2.2 为什么不推荐字段注入

依赖被隐藏在字段里,不能使用 final,脱离 Spring 容器时也不方便测试。构造方法注入能让依赖在创建对象时一次性准备完整。

6.2.3 多个同类型 Bean 怎么注入

可以用 @Primary 指定默认 Bean,也可以在注入点用 @Qualifier 精确指定。仍无法唯一确定时,Spring 会报错。

6.2.4 @Bean 和 @Component 有什么区别

两者都能把对象交给 Spring 管理。@Component 标在类上,由组件扫描发现;@Bean 标在配置类的方法上,适合手动创建对象或注册第三方类。

6.2.5 Bean 默认是单例吗

是。Spring 默认每个容器只创建一个该 Bean 实例,但可以通过作用域配置改成 prototype、request 等。

6.2.6 如何实现一个简单的 IoC 容器

首先通过配置获取包扫描路径,递归扫描目录下的 .class 文件;然后通过反射读取类上的注解,筛选出需要交给 IoC 容器管理的类。

接着使用反射创建对象,并按照 Bean 名称或类型存入 Map 中。最后遍历这些 Bean,检查它们依赖的对象,再从容器中找到对应 Bean,通过构造方法或字段完成依赖注入。

如果使用构造方法注入,还需要递归创建依赖,并检测循环依赖。

扫描包 → 筛选类 → 反射创建 → Map 保存 → 依赖注入

回答边界

这是简化版 IoC 容器的实现思路。真正的 Spring 容器还包含 BeanDefinition、BeanPostProcessor、作用域、生命周期、代理等机制。

6.2.7 构造方法注入会遇到什么问题

最典型的是构造器循环依赖。A 必须先拿到 B,B 又必须先拿到 A,Spring 无法完成创建;通常说明类的职责需要重新拆分。

七、📌 快速记忆

  • IoC:对象交给 Spring 管。
  • DI:Spring 把依赖传进来。
  • 构造方法注入:Spring 根据构造参数寻找 Bean。
  • 单构造方法通常不需要 @Autowired。
  • 多个同类型 Bean:用 @Primary 或 @Qualifier。
  • new、build() 创建的普通对象,不会自动成为 Bean。
  • 手写容器:定范围 → 扫 Class → 看注解 → 建对象 → 存 Map → 注依赖。

面试回答路线

是什么 → IoC 和 DI 的关系 → 为什么推荐构造方法注入 → 多 Bean 怎么选 → 循环依赖边界。

相关笔记:Spring Bean 生命周期、2.如何实现一个 IoC 容器。

八、📚 官方资料