2
IoC 与 DI:对象不用自己 new
Inversion of Control & Dependency Injection
传统写法里,UserService 自己 new UserDao(),把对象的创建权攥在自己手里。问题是:UserService 和 UserDao 死死绑在一起,想换个 Mock 都换不了,想加日志也插不进去。IoC 的思路是——把"new 对象"这个动作从代码里踢出去,交给 Spring 容器统一管。
论IoC 到底反转了什么
没有 IoC 时:你在代码里主动 new UserDao(),你控制了对象的生命周期。
有了 IoC 后:你在类里写一个字段,Spring 在启动时自动把对象"递"给你。对象的创建、销毁、注入,全由容器管——这就是"控制反转"。
DI(依赖注入)是 IoC 的具体实现:容器把你需要的依赖"注入"到你身上。你不用去找它,它自己找上门。
好处:① 类和类之间解耦,想换实现就换实现;② 单例对象只造一次,省内存;③ 方便测试,测试时往字段里塞个 Mock 就行。
Bean:Spring 手里的对象都叫 Bean
被 Spring 容器管理的对象,统称 Bean。你写的 UserService 本身就是个普通 Java 类,一旦它被 Spring 收编,就成了 Bean。容器启动时会把所有 Bean 创建好、组装好,放进一个 Map 里等着被使用。
三种配置方式:XML、注解、Java 配置类
Spring 刚出来那十年,配置全写在 XML 里。后来注解流行,再后来 Java 配置类成了主流。新项目一律用 Java 配置类 + 注解,XML 只在维护老项目时才看一眼。
方式一:XML 配置(了解即可,老项目里还能见到)
<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans">
<!-- 把 UserDaoImpl 注册成一个 Bean,id 是 bean 的名字 -->
<bean id="userDao" class="com.example.dao.UserDaoImpl"/>
<!-- UserService 依赖 UserDao,用 property 注入 -->
<bean id="userService" class="com.example.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>
</beans>
方式二:注解配置(日常开发主力)
package com.example.dao;
import org.springframework.stereotype.Repository;
// @Repository 标记这是一个数据访问层的 Bean
@Repository
public class UserDaoImpl implements UserDao {
public String findNameById(long id) {
return "用户#" + id;
}
}
package com.example.service;
import org.springframework.stereotype.Service;
// @Service 标记这是业务层 Bean;@Component 是最通用的标记
// @Repository 给 DAO 层用,@Controller 给 Web 层用,本质上都一样
@Service
public class UserService {
// 字段注入:@Autowired 直接标在字段上,Spring 会自动赋值
// 注意:字段注入写法简单,但不推荐,原因见下方"推荐写法"
@Autowired
private UserDao userDao;
public String greet(long id) {
return "你好," + userDao.findNameById(id);
}
}
方式三:Java 配置类(最推荐,项目里到处都是)
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// @Configuration 表示这是一个配置类,相当于一个 XML 配置文件
// @ComponentScan 告诉 Spring 去哪个包下面扫描 @Component / @Service 这些注解
@Configuration
public class AppConfig {
// @Bean 把方法返回值注册成一个 Bean,方法名默认就是 Bean 的名字
@Bean
public UserDao userDao() {
return new UserDaoImpl();
}
@Bean
public UserService userService(UserDao userDao) {
// 方法参数会被 Spring 自动注入,不用你自己 new
return new UserServiceImpl(userDao);
}
}
启动容器:把配置类喂给 Spring
package com.example;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
// 用 Java 配置类启动 IoC 容器
var context = new AnnotationConfigApplicationContext(AppConfig.class);
// 从容器里把 UserService 这个 Bean 捞出来用
UserService service = context.getBean(UserService.class);
System.out.println(service.greet(42));
context.close(); // 优雅关闭,触发 Bean 的销毁回调
}
}
运行结果:
你好,用户#42
三种注入方式:构造器、Setter、字段
| 注入方式 | 写法 | 评价 |
| 构造器注入 | 在构造方法参数里声明依赖,Spring 自动传入。 | 推荐 依赖不可变、启动时立刻发现缺失、方便单元测试。Spring 官方首推。 |
| Setter 注入 | 写一个 setXxx 方法,Spring 调用它赋值。 | 可选 适合可选依赖、需要运行时替换的场景。 |
| 字段注入 | 字段上直接写 @Autowired。 | 不推荐 写法最短,但隐藏了依赖、new 出来的对象没法注入、不利于测试。 |
推荐写法:构造器注入(Spring 4.3+ 单构造器可省略 @Autowired)
@Service
public class UserService {
// 用 final 修饰,依赖一旦注入就不能改,安全
private final UserDao userDao;
private final LogService logService;
// 构造器注入:Spring 看到构造方法,自动把 UserDao、LogService 传进来
// 当类只有一个构造方法时,@Autowired 可以省略
public UserService(UserDao userDao, LogService logService) {
this.userDao = userDao;
this.logService = logService;
}
public String greet(long id) {
logService.log("greet called");
return "你好," + userDao.findNameById(id);
}
}
同一接口有多个实现:@Primary 与 @Qualifier
你有一个 NotificationSender 接口,下面有 EmailSender 和 SmsSender 两个实现,都被标成了 @Component。这时 UserService 构造器要 NotificationSender,Spring 懵了:两个候选,我到底注入哪个?这就是"无资格 Bean 异常"(NoUniqueBeanDefinitionException)。
| 解决方式 | 用法 |
@Primary | 标在首选实现上,多个候选时默认用它。"我就是默认,别问。" |
@Qualifier("名字") | 按名字精确指定要哪个实现。"我说要 EmailSender,就给我 EmailSender。" |
@Resource(name = "...") | JSR-250 注解,按字段名或显式名字注入。 |
@Primary:默认选我
@Component
@Primary // 多个候选时,优先选这个
public class EmailSender implements NotificationSender { ... }
@Component
public class SmsSender implements NotificationSender { ... }
@Qualifier:按名字点名
@Service
public class NotificationService {
private final NotificationSender sender;
// 明确指定要 smsSender 这个 Bean
public NotificationService(@Qualifier("smsSender") NotificationSender sender) {
this.sender = sender;
}
}
Bean 名字的默认规则
类上标了 @Component("xxx") 就叫 xxx;没写名字时,Spring 把类名首字母小写当 Bean 名——UserServiceImpl 的 Bean 名就是 userServiceImpl。但如果类名前两个字母都是大写,比如 XMLParser,Bean 名就保持原样 XMLParser。@Qualifier 写错名字,启动直接报 NoSuchBeanDefinitionException。
完整可运行案例:构造器注入 + 多实现 + @Qualifier
光看片段记不住,下面给一个能直接跑起来的最小工程:一个 MessageService 接口有两个实现,用 @Qualifier 点名要哪个;主业务类用构造器注入(官方推荐姿势)。从头到尾贴出来,你照着敲一遍就懂了。
第一步:定义接口和两个实现(多实现演示)
package com.example.ioc;
// 业务接口:发送消息
public interface MessageSender {
String send(String to, String content);
}
// 实现一:邮件发送
@Component("emailSender")
public class EmailSender implements MessageSender {
public String send(String to, String content) {
return "[邮件] 发给 " + to + ":" + content;
}
}
// 实现二:短信发送。两个实现同一个接口,Spring 注入时会犯难——得用 @Qualifier 点名
@Component("smsSender")
public class SmsSender implements MessageSender {
public String send(String to, String content) {
return "[短信] 发给 " + to + ":" + content;
}
}
第二步:业务类——构造器注入 + @Qualifier 点名要哪个实现
package com.example.ioc;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class NotificationService {
// 构造器注入:字段用 final,依赖不可变。
// 接口有两个实现,Spring 不知道该用哪个,所以用 @Qualifier 按 Bean 名点名。
private final MessageSender sender;
// 单构造器时 @Autowired 可以省略;这里写出来是为了看清楚注入点
@Autowired
public NotificationService(@Qualifier("smsSender") MessageSender sender) {
this.sender = sender; // 最终拿到的是 SmsSender
}
public String notify(String to, String msg) {
return sender.send(to, msg);
}
}
第三步:配置类——@Configuration + @ComponentScan
package com.example.ioc;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
// @Configuration = 一个 Java 版的 applicationContext.xml
// @ComponentScan 告诉 Spring:去 com.example.ioc 包下扫描 @Component/@Service,自动注册成 Bean
@Configuration
@ComponentScan("com.example.ioc")
public class AppConfig { }
第四步:启动容器,从容器里把 Bean 捞出来跑
package com.example.ioc;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
var ctx = new AnnotationConfigApplicationContext(AppConfig.class);
NotificationService svc = ctx.getBean(NotificationService.class);
System.out.println(svc.notify("张三", "您的账单已出"));
ctx.close();
}
}
[短信] 发给 张三:您的账单已出
# 把 @Qualifier("smsSender") 改成 @Qualifier("emailSender"),输出立刻变成:
[邮件] 发给 张三:您的账单已出
# 这就是"面向接口 + 依赖注入"的好处:业务代码一行没改,换个实现就换了行为。
记
本节要点
① Bean = 被 Spring 管理的对象。你写的普通 Java 类,加上 @Component/@Service/@Repository/@Controller 之一,就成了 Bean。
② 注入方式首选构造器注入:字段 final、依赖不可变、启动即失败(不会跑一半才 NPE)。
③ 配置类用 @Configuration + @Bean,扫描用 @ComponentScan。新项目别再写 XML。
④ 多实现冲突:@Primary 选默认,@Qualifier 点名要哪个。
本章面试题 · 第 2 章(IoC / DI)
1.(原理题)说说你理解的 IoC 容器到底做了什么?DI 和 IoC 是什么关系?
查看答案
答案:IoC 容器本质是一个大工厂 + 大 Map:启动时扫描/读取配置,把每个 Bean 的定义(BeanDefinition)存下来,再按依赖关系把对象 new 出来、组装好、放进单例池。DI 是 IoC 的落地手段——容器把依赖"注入"到你身上。解析:面试标准答法:IoC 是思想,DI 是实现,BeanFactory 是顶层接口,ApplicationContext 是它的增强版。
2.(对比题)构造器注入、Setter 注入、字段注入 @Autowired 三种方式,为什么 Spring 官方推荐构造器注入?
查看答案
答案:① 依赖可声明为 final,不可变;② 启动时立刻暴露缺失依赖(字段注入要跑到用的时候才 NPE);③ 单元测试直接 new,不用反射。解析:字段注入写着爽,但隐藏耦合、不利于测试,Spring 团队早就不推荐了。
3.(排错题)同一个接口有两个实现类都标了 @Service,启动直接报 NoUniqueBeanDefinitionException。有哪几种解法?
查看答案
答案:① 在其中一个实现上贴 @Primary,告诉容器"默认选它";② 在注入点用 @Qualifier("bean名字") 点名;③ 把 Bean 名改成和变量名一致,Spring 会按名字兜底匹配。解析:@Primary 适合"大多数时候用这个",@Qualifier 适合"这次我就要指定的那个"。