Spring Boot 依赖注入说明文档
1. 依赖注入的基本概念
依赖注入的英文是 Dependency Injection,简称 DI。
依赖注入需要先理解两个词:
- 依赖;
- 注入。
所谓依赖,是指一个类完成自己的工作时,需要使用另一个对象。
例如,UserController 需要调用 UserService 查询用户信息:
java
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
在这个例子中:
text
UserController 依赖 UserService
因为如果没有 UserService,UserController 就无法完成用户查询等业务操作。
所谓注入,是指这个 UserService 对象不由 UserController 自己创建,而是由外部创建后传入 UserController。
因此,依赖注入可以定义为:
一个类需要使用另一个对象时,不在类内部直接创建该对象,而是由外部创建后,通过构造方法、Setter 方法或字段等方式传入。
在 Spring Boot 中,这个"外部"通常就是 Spring 容器。
2. 不使用依赖注入的方式
假设有一个用户业务类:
java
public class UserService {
public String getUserName() {
return "张三";
}
}
如果不使用依赖注入,UserController 可以直接在内部创建 UserService:
java
public class UserController {
private final UserService userService;
public UserController() {
this.userService = new UserService();
}
}
也可以简写为:
java
public class UserController {
private final UserService userService =
new UserService();
}
这种写法的特点是:
text
UserController 自己创建 UserService
UserController 自己决定使用哪个实现类
UserController 自己管理 UserService 对象
也就是说,UserController 不仅负责使用 UserService,还负责创建 UserService。
这种方式会使对象的使用逻辑和创建逻辑绑定在一起。
3. 使用依赖注入的方式
将代码修改为:
java
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
此时,UserController 内部不再出现:
java
new UserService();
它只声明:
text
我需要一个 UserService 对象。
至于这个对象:
- 由谁创建;
- 什么时候创建;
- 创建哪个实现类;
- 是否只创建一个;
- 是否需要代理;
这些问题不再由 UserController 处理。
在普通 Java 程序中,可以手动完成依赖注入:
java
public static void main(String[] args) {
UserService userService =
new UserService();
UserController userController =
new UserController(userService);
}
这里,main 方法创建了 UserService,然后将它传入 UserController。
因此,依赖注入并不是 Spring 独有的语法。它首先是一种对象设计方式:
类只负责声明和使用依赖,依赖对象由外部创建和提供。
4. 构造方法中的赋值是什么意思
下面这段代码看起来只是普通的构造方法赋值:
java
public UserController(UserService userService) {
this.userService = userService;
}
它本身确实只是普通 Java 代码。
其中有两个同名变量:
java
private final UserService userService;
这是 UserController 的成员变量。
而构造方法中的:
java
UserService userService
是外部传进来的构造方法参数。
下面这行代码:
java
this.userService = userService;
表示:
text
将外部传进来的 UserService 对象,
保存到当前 UserController 对象的成员变量中。
为了更容易理解,可以暂时将参数改名:
java
public UserController(
UserService serviceFromOutside
) {
this.userService = serviceFromOutside;
}
这样就可以清楚地看到:
text
serviceFromOutside
↓
保存到 this.userService
依赖注入的关键不在这行赋值本身,而在于:
传进来的
UserService对象是谁创建的,又是谁调用构造方法将它传进来的。
在 Spring Boot 中,这个工作由 Spring 容器完成。
5. Spring Boot 中的依赖注入
在 Spring Boot 中,可以将相关类注册到 Spring 容器。
例如:
java
@Service
public class UserService {
public String getUserName() {
return "张三";
}
}
控制器:
java
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
Spring Boot 启动后,会在内部完成类似下面的操作:
java
UserService userService =
new UserService();
UserController userController =
new UserController(userService);
这段代码通常不需要开发人员手动编写。
Spring 会自动完成以下过程:
text
发现 UserService 类
↓
创建 UserService 对象
↓
发现 UserController 类
↓
分析 UserController 构造方法
↓
发现构造方法需要 UserService
↓
从容器中找到 UserService 对象
↓
将 UserService 传入 UserController
最后完成:
java
new UserController(userService);
这一过程就是 Spring Boot 中的构造方法依赖注入。
6. 什么是 Spring 容器
Spring 容器可以理解为一个负责创建、保存和管理对象的对象管理中心。
Spring Boot 启动时:
java
SpringApplication.run(
Application.class,
args
);
会创建 Spring 应用上下文。
常见的核心接口是:
java
ApplicationContext
Spring 容器中可能保存如下对象:
text
userController
userService
userMapper
dataSource
objectMapper
transactionManager
从便于理解的角度,可以暂时将 Spring 容器想象成一个对象集合:
java
Map<String, Object> beans;
例如:
text
userController → UserController 对象
userService → UserService 对象
dataSource → 数据库连接池对象
真实的 Spring 容器比一个普通的 Map 复杂得多,但这种理解有助于认识 Bean 和依赖注入。
7. 什么是 Bean
Bean 是指:
被 Spring 容器创建、保存和管理的对象。
例如:
java
@Service
public class UserService {
}
这里:
java
@Service
是注解。
Spring 根据这个注解创建出来的:
java
UserService 对象
才是 Bean。
因此需要区分:
text
@Service:注解
UserService:类
new UserService() 得到的对象:实例
由 Spring 管理的 UserService 实例:Bean
Bean 不是注解,注解只是告诉 Spring:
text
这个类需要由 Spring 创建和管理。
8. 常见的 Bean 注册方式
8.1 @Component
@Component 是通用组件注解:
java
@Component
public class FileUtils {
}
表示该类需要由 Spring 管理。
8.2 @Service
用于标记业务层组件:
java
@Service
public class UserService {
}
从 Bean 注册角度看,@Service 本质上属于一种特殊的 @Component。
它主要用于表达:
text
这是一个业务逻辑组件。
8.3 @Repository
用于数据访问层:
java
@Repository
public class UserRepository {
}
通常负责数据库访问操作。
8.4 @Controller
用于传统 Spring MVC 控制器:
java
@Controller
public class PageController {
}
通常用于返回页面。
8.5 @RestController
用于 REST 接口控制器:
java
@RestController
public class UserController {
}
通常用于接收 HTTP 请求并返回 JSON 数据。
8.6 @Configuration 和 @Bean
有些对象不能直接在类上添加 @Component,或者对象创建过程比较复杂,可以通过配置类注册:
java
@Configuration
public class AppConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
这里:
java
new BCryptPasswordEncoder()
返回的对象会由 Spring 容器管理,因此也是一个 Bean。
Bean 的默认名称为方法名:
text
passwordEncoder
9. 依赖注入与控制反转
依赖注入经常和控制反转一起出现。
控制反转的英文是 Inversion of Control,简称 IoC。
传统方式中,对象由开发人员自己创建:
java
UserService userService =
new UserService();
开发人员负责:
- 创建对象;
- 选择实现类;
- 管理对象关系;
- 决定对象数量;
- 控制对象生命周期。
使用 Spring 后,对象的创建和管理交给 Spring 容器。
因此,控制反转可以理解为:
对象的创建权和管理权,从业务代码转移给 Spring 容器。
依赖注入是实现控制反转的主要方式。
二者的关系可以概括为:
text
IoC:对象由 Spring 管理
DI:Spring 将对象传给需要它的其他对象
10. 依赖注入并不是完全解耦
依赖注入的作用不是让两个类完全没有联系。
例如:
java
private final UserService userService;
说明 UserController 仍然需要 UserService。
它仍然会调用:
java
userService.getUserName();
所以:
text
UserController 仍然依赖 UserService
依赖注入真正分离的是:
text
对象的使用
对象的创建
使用依赖注入后,UserController 只负责使用 UserService,不再负责创建 UserService。
因此,更准确的说法是:
依赖注入降低了类之间的耦合,但并不是完全消除依赖关系。
11. 面向接口的依赖注入
实际开发中,通常推荐依赖接口,而不是直接依赖具体实现类。
接口:
java
public interface UserService {
String getUserName(Long id);
}
实现类:
java
@Service
public class DatabaseUserService
implements UserService {
@Override
public String getUserName(Long id) {
return "从数据库查询用户";
}
}
控制器:
java
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
这里 UserController 依赖的是:
java
UserService
而不是:
java
DatabaseUserService
以后可以增加新的实现:
java
@Service
public class CacheUserService
implements UserService {
@Override
public String getUserName(Long id) {
return "从缓存查询用户";
}
}
这样,控制器不需要关心数据来自:
- 数据库;
- Redis 缓存;
- 远程接口;
- 模拟数据。
控制器只需要调用接口定义的方法:
java
userService.getUserName(id);
这就是面向接口编程。
12. 接口只有一个实现类时
如果接口只有一个实现类:
java
public interface UserService {
}
实现类:
java
@Service
public class UserServiceImpl
implements UserService {
}
控制器:
java
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
Spring 会按照类型查找:
text
UserService 类型的 Bean
由于容器中只有一个实现对象,所以 Spring 可以直接将它注入。
相当于:
java
UserService userService =
new UserServiceImpl();
UserController userController =
new UserController(userService);
13. 接口存在多个实现类时
假设接口有两个实现类:
java
public interface PaymentService {
void pay();
}
支付宝实现:
java
@Service
public class AlipayService
implements PaymentService {
@Override
public void pay() {
System.out.println("支付宝支付");
}
}
微信支付实现:
java
@Service
public class WechatPayService
implements PaymentService {
@Override
public void pay() {
System.out.println("微信支付");
}
}
如果直接注入:
java
@RestController
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(
PaymentService paymentService
) {
this.paymentService = paymentService;
}
}
Spring 会找到两个 PaymentService Bean:
text
AlipayService
WechatPayService
此时 Spring 无法确定应该注入哪个对象,通常会启动失败,并提示需要一个 Bean,但实际找到了多个 Bean。
需要注意的是:
Spring 不是判断哪个类实现接口,因为两个类都已经实现了接口。Spring 判断的是,当前注入位置应该选择哪个实现对象。
14. 使用 @Qualifier 指定实现类
可以通过 @Qualifier 明确指定 Bean 名称:
java
@RestController
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(
@Qualifier("alipayService")
PaymentService paymentService
) {
this.paymentService = paymentService;
}
}
默认情况下,类名首字母小写通常作为 Bean 名称:
text
AlipayService → alipayService
WechatPayService → wechatPayService
因此:
java
@Qualifier("alipayService")
表示明确选择 AlipayService 对象。
也可以主动指定 Bean 名称:
java
@Service("alipay")
public class AlipayService
implements PaymentService {
}
注入时:
java
public PaymentController(
@Qualifier("alipay")
PaymentService paymentService
) {
this.paymentService = paymentService;
}
15. 使用 @Primary 设置默认实现
如果多个实现类中有一个是默认实现,可以使用 @Primary:
java
@Service
@Primary
public class AlipayService
implements PaymentService {
@Override
public void pay() {
System.out.println("支付宝支付");
}
}
另一个实现:
java
@Service
public class WechatPayService
implements PaymentService {
}
注入时不需要指定名称:
java
public PaymentController(
PaymentService paymentService
) {
this.paymentService = paymentService;
}
Spring 会优先选择带有 @Primary 的实现。
可以这样理解:
text
@Primary:默认选择
@Qualifier:明确点名
当二者同时存在时,通常以明确指定的 @Qualifier 为准。
16. 同时注入所有实现类
有时业务并不是只选择一个实现,而是需要得到全部实现。
可以注入 List:
java
@Service
public class PaymentManager {
private final List<PaymentService> paymentServices;
public PaymentManager(
List<PaymentService> paymentServices
) {
this.paymentServices = paymentServices;
}
}
Spring 会将所有 PaymentService 实现放入集合:
text
AlipayService
WechatPayService
之后可以遍历调用:
java
public void executeAll() {
for (PaymentService service : paymentServices) {
service.pay();
}
}
17. 通过 Map 动态选择实现
还可以将同一接口的全部实现注入为 Map:
java
@Service
public class PaymentManager {
private final Map<String, PaymentService>
paymentServiceMap;
public PaymentManager(
Map<String, PaymentService>
paymentServiceMap
) {
this.paymentServiceMap =
paymentServiceMap;
}
}
Spring 注入后的结构大致为:
text
alipayService
→ AlipayService 对象
wechatPayService
→ WechatPayService 对象
可以根据参数动态选择:
java
public void pay(String paymentType) {
PaymentService paymentService =
paymentServiceMap.get(paymentType);
if (paymentService == null) {
throw new IllegalArgumentException(
"不支持的支付方式:" + paymentType
);
}
paymentService.pay();
}
调用:
java
paymentManager.pay("alipayService");
就会选择支付宝实现。
这种设计在以下场景中非常常见:
- 多种支付方式;
- 多种文件存储方式;
- 多种消息发送方式;
- 多种预测模型;
- 多种数据源;
- 多种业务处理策略。
18. 根据配置选择实现类
有时不同环境需要使用不同实现。
例如:
text
开发环境使用模拟服务
生产环境使用真实远程服务
可以使用条件注解:
java
@Service
@ConditionalOnProperty(
name = "model.provider",
havingValue = "local"
)
public class LocalModelService
implements ModelService {
}
远程实现:
java
@Service
@ConditionalOnProperty(
name = "model.provider",
havingValue = "remote"
)
public class RemoteModelService
implements ModelService {
}
配置文件:
yaml
model:
provider: remote
此时只有符合条件的 RemoteModelService 会被注册为 Bean。
这样容器中可能只存在一个 ModelService 实现,其他业务类就可以正常注入。
19. 三种依赖注入方式
Spring 中常见的注入方式包括:
- 构造方法注入;
- Setter 方法注入;
- 字段注入。
19.1 构造方法注入
java
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
这是最推荐的方式。
优点包括:
- 依赖关系明确;
- 可以使用
final; - 便于单元测试;
- 可以保证对象创建后依赖完整;
- 更容易发现循环依赖。
19.2 Setter 方法注入
java
@RestController
public class UserController {
private UserService userService;
@Autowired
public void setUserService(
UserService userService
) {
this.userService = userService;
}
}
Setter 注入适合表示可以后续更换或属于可选性质的依赖。
19.3 字段注入
java
@RestController
public class UserController {
@Autowired
private UserService userService;
}
字段注入代码较短,但通常不推荐作为主要方式。
主要问题包括:
- 依赖关系不够直观;
- 无法方便地使用
final; - 不启动 Spring 时不方便测试;
- 容易让一个类隐藏过多依赖。
因此,实际开发中通常优先选择构造方法注入。
20. 构造方法是否需要 @Autowired
如果一个类只有一个构造方法,通常可以省略 @Autowired:
java
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
下面的写法也可以:
java
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
但通常没有必要。
如果类中存在多个构造方法,则可能需要使用 @Autowired 明确告诉 Spring 使用哪个构造方法。
21. Lombok 简化构造方法注入
使用 Lombok 时,可以使用:
java
@RequiredArgsConstructor
例如:
java
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
}
Lombok 会自动生成:
java
public UserController(UserService userService) {
this.userService = userService;
}
需要注意:
Lombok 只是帮助生成构造方法,真正完成 Bean 查找和依赖注入的仍然是 Spring。
22. Controller、Service 和 Mapper 的完整依赖关系
一个常见的 Spring Boot 项目结构为:
text
Controller
↓
Service
↓
Mapper
↓
数据库
控制器:
java
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(
UserService userService
) {
this.userService = userService;
}
@GetMapping("/{id}")
public User getUser(
@PathVariable Long id
) {
return userService.findById(id);
}
}
业务接口:
java
public interface UserService {
User findById(Long id);
}
业务实现:
java
@Service
public class UserServiceImpl
implements UserService {
private final UserMapper userMapper;
public UserServiceImpl(
UserMapper userMapper
) {
this.userMapper = userMapper;
}
@Override
public User findById(Long id) {
return userMapper.selectById(id);
}
}
MyBatis Mapper:
java
@Mapper
public interface UserMapper {
User selectById(Long id);
}
Spring Boot 和 MyBatis 启动后,大致完成:
java
UserMapper userMapper =
MyBatis创建的代理对象;
UserService userService =
new UserServiceImpl(userMapper);
UserController userController =
new UserController(userService);
最终的依赖关系为:
text
UserController
↓ 注入
UserServiceImpl
↓ 注入
UserMapper 代理对象
↓
数据库
23. Spring Boot 启动时的注入过程
Spring Boot 应用启动时,可以将依赖注入过程简化为:
text
执行 main 方法
↓
调用 SpringApplication.run
↓
创建 Spring 容器
↓
扫描组件
↓
识别 @Controller、@Service 等注解
↓
注册 Bean 定义
↓
创建 Bean 对象
↓
分析构造方法参数
↓
查找匹配的依赖 Bean
↓
调用构造方法传入依赖
↓
完成 Bean 初始化
↓
启动内嵌 Web 服务器
↓
监听 HTTP 请求
因此,当用户请求到达时,Controller、Service、Mapper 等对象通常已经创建和注入完成。
不是每次请求到达后才重新执行依赖注入。
24. Spring Bean 的默认作用域
Spring Bean 默认是单例作用域:
text
singleton
表示在同一个 Spring 容器中,一个 Bean 通常只有一个实例。
例如两个 Controller 都依赖同一个 UserService:
text
Controller A ──┐
├── 同一个 UserService 对象
Controller B ──┘
因此,Service 中通常不应该保存某个请求专用的可变状态。
例如下面的写法可能存在并发问题:
java
@Service
public class UserService {
private Long currentUserId;
}
多个请求可能共同访问同一个 UserService 对象。
业务方法中的局部变量通常更加安全:
java
public User findById(Long id) {
User user = userMapper.selectById(id);
return user;
}
25. 循环依赖
如果两个类互相依赖:
java
@Service
public class AService {
private final BService bService;
public AService(BService bService) {
this.bService = bService;
}
}
java
@Service
public class BService {
private final AService aService;
public BService(AService aService) {
this.aService = aService;
}
}
依赖关系为:
text
AService
↓
BService
↓
AService
Spring 创建 AService 时需要 BService,创建 BService 时又需要 AService,这就是循环依赖。
循环依赖通常说明职责划分不合理。
更合理的方式可能是提取公共业务:
text
AService ──→ CommonService
BService ──→ CommonService
而不是让两个 Service 相互依赖。
26. 多实现类在实际开发中的常见场景
一个接口存在多个实现类在实际项目中非常常见。
支付方式
text
PaymentService
├── AlipayService
├── WechatPayService
└── BankCardPaymentService
消息发送
text
MessageService
├── EmailMessageService
├── SmsMessageService
└── WechatMessageService
文件存储
text
StorageService
├── LocalStorageService
├── MinioStorageService
└── OssStorageService
登录认证
text
AuthService
├── PasswordAuthService
├── LdapAuthService
└── OAuthAuthService
模型服务
text
ModelService
├── MockModelService
├── LocalModelService
└── RemoteModelService
对应的常见选择方式为:
text
固定选择某个实现
→ @Qualifier
设置默认实现
→ @Primary
根据业务参数动态选择
→ 注入 Map 或 List
根据配置或运行环境选择
→ @ConditionalOnProperty 或 @Profile
27. 依赖注入的主要价值
依赖注入的主要价值包括以下几个方面。
27.1 分离对象创建与对象使用
业务类只关心如何使用依赖,不需要关心依赖如何创建。
27.2 降低对具体实现类的耦合
通过依赖接口,可以方便地替换具体实现。
27.3 便于单元测试
例如:
java
UserService mockUserService =
new MockUserService();
UserController userController =
new UserController(mockUserService);
不启动完整 Spring 容器,也可以测试 UserController。
27.4 统一管理对象生命周期
Spring 可以统一处理:
- 对象创建;
- 单例管理;
- 初始化;
- 销毁;
- 代理;
- 事务;
- 缓存;
- 权限控制。
27.5 便于扩展多种实现
同一个接口可以对应多个实现,并根据配置、参数或注解选择具体对象。
28. 常见认识误区
误区一:依赖注入就是构造方法赋值
构造方法赋值只是保存依赖。
真正的依赖注入还包括:
text
外部创建依赖对象
↓
调用构造方法
↓
将依赖对象传入
误区二:Bean 就是注解
Bean 是由 Spring 管理的对象。
@Service、@Component 等只是帮助注册 Bean 的注解。
误区三:依赖注入可以完全解耦
依赖注入并不会消除类之间的业务依赖。
它主要分离对象创建与对象使用,并降低对具体实现的耦合。
误区四:Spring 会自动实例化所有类
Spring 只会创建已经注册到容器中的 Bean。
普通类如果没有通过注解、@Bean 或其他方式注册,Spring 通常不会自动管理。
误区五:有多个实现类时 Spring 会自动理解业务需求
Spring 只能根据类型、Bean 名称、@Primary、@Qualifier 和条件配置进行选择。
它不会自动理解开发人员的业务意图。
29. 最终总结
Spring Boot 中的依赖注入可以概括为以下过程:
text
开发人员定义类和接口
↓
使用注解或 @Bean 注册组件
↓
Spring 创建并管理对象
↓
这些对象成为 Bean
↓
Spring 分析构造方法参数
↓
查找匹配的 Bean
↓
将 Bean 传入构造方法
↓
完成对象之间的组装
最核心的定义是:
依赖注入就是一个类需要使用另一个对象时,不在类内部直接创建该对象,而是由外部创建后传入。在 Spring Boot 中,依赖对象通常由 Spring 容器创建和管理,并根据构造方法中的参数自动注入。
推荐使用的基本写法为:
java
public interface UserService {
User findById(Long id);
}
java
@Service
public class UserServiceImpl
implements UserService {
private final UserMapper userMapper;
public UserServiceImpl(
UserMapper userMapper
) {
this.userMapper = userMapper;
}
@Override
public User findById(Long id) {
return userMapper.selectById(id);
}
}
java
@RestController
public class UserController {
private final UserService userService;
public UserController(
UserService userService
) {
this.userService = userService;
}
}
当接口存在多个实现时:
text
只有一个实现
→ Spring 自动注入
多个实现并指定 @Qualifier
→ 注入指定 Bean
多个实现并设置 @Primary
→ 注入默认 Bean
需要全部实现
→ 注入 List 或 Map
多个实现但没有选择规则
→ Spring 启动报错
可以用一句话记忆:
我们负责定义类、接口、业务功能和依赖关系;Spring 负责创建对象、管理 Bean,并将正确的依赖对象注入需要它的类中。