楼层: 首页/ 软件技术/ Spring 核心框架/ IoC 与 DI:对象不用自己 new
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 适合"这次我就要指定的那个"。