依赖注入的核心是“谁决定对象怎么来”,即类不自己new依赖、不硬编码创建逻辑;真正落地需IoC容器接管依赖获取路径,接口抽象与构造器注入是关键。

依赖注入不是“把对象塞进去”,而是“谁来决定对象怎么来”
很多人一看到 setXXX() 或 @Autowired 就以为是 DI,其实那只是表现形式。DI 的核心判断标准只有一个:**类内部不自己 new 依赖对象,也不硬编码依赖的创建逻辑**。只要 new SomeService() 出现在业务类里,哪怕后面加了 setter,也不算真正意义上的 DI。
常见错误现象:
– 单元测试时只能用真实数据库连接,因为 new JdbcDao() 写死在 service 里
– 换个缓存实现要改三四个类,因为每个类都自己 new RedisCache()
– @PostConstruct 里手动调 init() 初始化依赖,绕过了容器管理
- 真正起作用的是“控制反转”(IoC)容器——它负责实例化、装配、生命周期管理
- DI 是 IoC 的一种实现方式,重点在“依赖由外部注入”,而非“怎么注”
- 手动 new + setter 赋值 ≠ DI;只有容器接管了依赖的获取路径,才算落地
Spring 中 @Autowired 的注入时机和失效场景
@Autowired 看似简单,但实际生效前提是:该类必须是 Spring 容器管理的 Bean。否则即使写了注解,字段永远为 null。
典型失效场景:
– 在 new XxxService() 创建的对象上调用方法(绕过 Spring 容器)
– 使用 static 字段或方法尝试注入(Spring 不处理静态成员)
– 类上漏了 @Component / @Service,或没被 @ComponentScan 扫描到
– 构造器注入时参数类型有多个候选 Bean,又没加 @Qualifier
立即学习“Java免费学习笔记(深入)”;
- 推荐优先用构造器注入:
public UserService(@Qualifier("mysql") UserDao userDao),避免空指针且便于单元测试 - 字段注入(
@Autowired在 private 字段上)虽然简洁,但隐藏了依赖关系,且无法在构造阶段校验 - 接口注入(如继承
ApplicationContextAware)已基本淘汰,耦合容器 API,不推荐
手写简易 DI 容器的关键逻辑在哪
理解 Spring 的 DI 不需要读源码,但得知道它绕不开三个动作:**扫描 → 实例化 → 绑定**。自己写个玩具容器,重点不在功能多全,而在看清这三步如何解耦。
一个最简可行版本要处理:
– 类路径扫描:用 ClassPathScanningCandidateComponentProvider 找带 @Component 的类
– 反射实例化:调 clazz.getDeclaredConstructor().newInstance(),注意私有构造器要 setAccessible(true)
– 依赖绑定:遍历字段找 @Autowired,根据类型从已注册 Bean 中匹配,再用 Field.set() 注入
- 类型匹配失败时,Spring 会 fallback 到名称匹配(字段名 = Bean 名),但手动实现时容易忽略这点
- 循环依赖能工作,靠的是“提前暴露半成品 Bean”,即三级缓存中的
singletonFactories,不是靠字段注入顺序 - 不要试图在初始化方法(如
@PostConstruct)里访问尚未注入的字段——此时注入还没开始
为什么接口+实现分离对 DI 至关重要
DI 能否顺利落地,80% 取决于你有没有把“依赖契约”抽成接口。如果 service 直接依赖 MyBatisUserDao 这个具体类,那无论用什么容器,替换实现都得改代码。
使用场景对比:
– 依赖 UserDao 接口:可自由切换 JdbcUserDao、MyBatisUserDao、MockUserDao
– 依赖 MyBatisUserDao 类:换 ORM 框架就得重写 service,单元测试只能连真实 DB
- 接口命名别带技术栈痕迹,比如别叫
MyBatisUserDao,而应是UserDao;实现类才体现技术细节 - Spring 默认按类型注入,所以同一接口多个实现时,必须用
@Qualifier("xxx")明确指定 - 测试时用
@MockBean替换真实 Bean,本质就是利用了接口抽象——只要实现同一接口,容器就认
真正难的从来不是写 @Autowired,而是设计出能被容器自然组装的类结构:没有 new,没有静态工具调用,所有协作对象都来自接口声明,且生命周期不由自己掌控。










