第 2 章 · IoC、依赖注入与 Spring 核心
本章目标:理解 控制反转(IoC) 与 依赖注入(DI);熟练使用 @Component、@Service、@Repository、@Autowired 与构造器注入;编写 @Configuration 配置类与 @Bean;用 @EnableConfigurationProperties 绑定 application.yml;能对照 Django 的「模块导入 + settings 单例」说明 Spring 容器如何管理对象生命周期;在 shop-spring-demo 中实现可测试的商品目录服务层。
学时建议:4~5 小时(含 2 小时跟练)
前置:完成 spring-boot-web ch01 与 java-dev(OOP、接口、异常、单元测试基础)。
2.1 场景说明:从硬编码到可注入的服务
ch01 的 HealthController 直接返回 Map。现在要引入商品目录服务 ProductCatalogService,供 ch03 的 Controller 调用。若像下面这样写:
public class ProductController {
private ProductCatalogService service = new ProductCatalogService(); // 紧耦合
}
则难以单元测试、难以切换实现(内存 / 数据库 / Mock)。Spring 的 IoC 容器负责创建对象并注入依赖,类似 Django 中通过 settings 配置 backend、通过 AppConfig 注册组件——但 Spring 更显式、可测试性更强。
| 问题 | 手动 new | Spring DI |
|---|---|---|
| 替换实现 | 改源码 | 换 @Service 实现或 @Primary |
| 单例共享 | 自己写单例 | 默认 Singleton Bean |
| 测试 | 难以 Mock | @MockBean / 构造器注入 Mock |
| 配置外置 | 散落常量 | @ConfigurationProperties |
说明:本章示例域名、数据均为教学虚构,不涉及真实业务系统。
2.2 IoC 与 DI 核心概念
2.2.1 术语对照
| 术语 | 含义 | shop-spring-demo 示例 |
|---|---|---|
| Bean | 由容器管理的对象 | ProductCatalogService 实例 |
| IoC 容器 | 负责创建、装配、销毁 Bean | ApplicationContext |
| DI | 容器把依赖「注入」到使用者 | Controller 注入 Service |
| @Component Scan | 自动发现带注解的类 | @SpringBootApplication 扫描 com.example.shop |
2.2.2 与 Django 对照(概念级)
| Spring | Django 近似 |
|---|---|
@Service Bean | catalog.services 模块中的类,在 views 里 import |
@Autowired | 无内置 DI;常用函数参数或手动从 registry 取 |
@Configuration + @Bean | settings.py 里定义 CACHES、DATABASES backend |
@Profile("dev") | settings/dev.py 条件配置 |
| 构造器注入 | 显式在 __init__ 传依赖(推荐风格) |
Django 更偏「导入即单例」;Spring 强制通过容器管理生命周期,更利于大型 Java 项目。
2.2.3 请求链路中的 Bean(预览)
HTTP 请求
→ DispatcherServlet(框架 Bean)
→ ProductController(@RestController Bean)
→ ProductCatalogService(@Service Bean)
→ (ch05)ProductRepository(@Repository Bean)
2.3 分层注解(Stereotype)
| 注解 | 语义 | 使用场景 |
|---|---|---|
@Component | 通用组件 | 工具类、适配器 |
@Service | 业务服务层 | ProductCatalogService |
@Repository | 持久层(可转异常) | ch05 JPA Repository |
@Controller / @RestController | Web 层 | ch03 |
四者本质都是 @Component 的特化,便于分层阅读。
2.3.1 商品服务接口与实现
catalog/ProductCatalogService.java:
package com.example.shop.catalog;
import java.util.List;
public interface ProductCatalogService {
List<ProductDto> listFeatured(int limit);
}
catalog/ProductDto.java(简单 DTO,后续章节再扩展):
package com.example.shop.catalog;
import java.math.BigDecimal;
public record ProductDto(Long id, String sku, String name, BigDecimal price) {}
catalog/InMemoryProductCatalogService.java:
package com.example.shop.catalog;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
@Service
public class InMemoryProductCatalogService implements ProductCatalogService {
private static final List<ProductDto> SAMPLE = List.of(
new ProductDto(1L, "SKU-001", "示例商品 A", new BigDecimal("99.00")),
new ProductDto(2L, "SKU-002", "示例商品 B", new BigDecimal("199.50"))
);
@Override
public List<ProductDto> listFeatured(int limit) {
return SAMPLE.stream().limit(Math.max(limit, 0)).toList();
}
}
ch05 将替换为 JPA 实现;本章聚焦 接口 + @Service 与注入方式。
2.4 依赖注入的三种方式
2.4.1 构造器注入(推荐)
package com.example.shop.web;
import com.example.shop.catalog.ProductCatalogService;
import com.example.shop.catalog.ProductDto;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class ProductPreviewController {
private final ProductCatalogService catalogService;
public ProductPreviewController(ProductCatalogService catalogService) {
this.catalogService = catalogService;
}
@GetMapping("/api/v1/products/featured")
public List<ProductDto> featured(@RequestParam(defaultValue = "10") int limit) {
return catalogService.listFeatured(limit);
}
}
优点:依赖 final、不可变;便于单元测试;Spring 官方推荐。无需写 @Autowired(单构造器时自动注入)。
2.4.2 字段注入(不推荐)
@RestController
public class BadExample {
@Autowired
private ProductCatalogService catalogService; // 难测、隐藏依赖
}
2.4.3 Setter 注入
适用于可选依赖;业务 Bean 较少用。
2.4.4 对照表
| 方式 | 测试 | 不可变 | 团队规范 |
|---|---|---|---|
| 构造器 | 优 | 优 | 默认采用 |
| 字段 | 差 | 差 | 避免 |
| Setter | 中 | 差 | 可选依赖 |
2.5 @Configuration 与 @Bean
当第三方类无法加 @Service 时,用配置类声明 Bean。
config/CatalogConfig.java:
package com.example.shop.config;
import com.example.shop.catalog.ProductCatalogService;
import com.example.shop.catalog.InMemoryProductCatalogService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CatalogConfig {
/**
* 若去掉 @Service,可在此 @Bean 注册同一实现。
* 本章保留 @Service,此配置类演示 @Bean 写法。
*/
@Bean
public ProductCatalogService productCatalogService() {
return new InMemoryProductCatalogService();
}
}
注意:若 @Service 与 @Bean 同时注册两个 ProductCatalogService,会冲突。实际项目二选一;本章可删除 @Service 或删除 @Bean 方法做实验。
| 注册方式 | 适用 |
|---|---|
@Component / @Service | 本项目类 |
@Bean 方法 | 第三方 SDK、DataSource、RestTemplate |