IOC 与 DI
IOC(Inversion of Control,控制反转) 是 Spring 的核心思想:对象的创建和依赖关系的组装,由开发者自己 new 反转为由 Spring 容器负责。DI(Dependency Injection,依赖注入) 是 IOC 的具体实现方式——容器在创建对象时,把它所需的依赖”注入”进来。
核心容器
Spring 提供两种容器接口:
| 接口 | 说明 | 典型实现 |
|---|---|---|
BeanFactory | 基础容器,懒加载,生产环境几乎不直接使用 | DefaultListableBeanFactory |
ApplicationContext | 扩展 BeanFactory,支持事件、国际化、AOP 等 | AnnotationConfigApplicationContext、SpringApplication 内部 |
Spring Boot 应用中,SpringApplication.run() 返回的就是 ConfigurableApplicationContext(ApplicationContext 的子接口)。
一、注册 Bean 的方式
1. 组件扫描注解
@Component // 通用组件
@Service // 业务层(语义化,本质同 @Component)
@Repository // 数据访问层(额外提供异常转换)
@Controller // MVC 控制器
@RestController // @Controller + @ResponseBody@Service
public class OrderService {
// Spring 自动扫描并注册为 Bean
}2. @Bean 方法
适合注册第三方库的类或需要复杂初始化逻辑的对象:
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(10))
.build();
}
@Bean("primaryDataSource")
public DataSource dataSource(
@Value("${spring.datasource.url}") String url) {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl(url);
return ds;
}
}3. @Import
手动引入配置类或 ImportSelector(自动配置的核心机制):
@Configuration
@Import({SecurityConfig.class, CacheConfig.class})
public class ApplicationConfig { }4. 条件注册
详见 条件注解。
二、依赖注入方式
构造器注入(推荐)
@Service
public class OrderService {
private final OrderRepository orderRepo;
private final InventoryService inventoryService;
private final EventPublisher eventPublisher;
// 单构造器无需 @Autowired(Spring 4.3+)
public OrderService(OrderRepository orderRepo,
InventoryService inventoryService,
EventPublisher eventPublisher) {
this.orderRepo = orderRepo;
this.inventoryService = inventoryService;
this.eventPublisher = eventPublisher;
}
}优势:
- 依赖不可变(
final字段),线程安全 - 强制显式声明依赖,依赖过多时构造器参数爆炸,天然提示重构
- 便于单元测试(直接
new,无需反射) - 在 Spring 中唯一能够彻底解决循环依赖的注入方式(配合
@Lazy或重构)
实际项目推荐配合 Lombok:
@Service
@RequiredArgsConstructor // 自动生成包含所有 final 字段的构造器
public class OrderService {
private final OrderRepository orderRepo;
private final InventoryService inventoryService;
}Setter 注入
适合可选依赖或需要在注入后重新设置的场景:
@Service
public class NotificationService {
private MailSender mailSender;
private SmsSender smsSender;
@Autowired(required = false) // 可选依赖,Bean 不存在时不报错
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
@Autowired(required = false)
public void setSmsSender(SmsSender smsSender) {
this.smsSender = smsSender;
}
}字段注入(不推荐)
@Service
public class UserService {
@Autowired // 不推荐:隐藏依赖、难以测试、无法 final
private UserRepository userRepo;
}字段注入使依赖关系隐式,且无法在测试中直接
new来替换依赖,只能靠反射或 Spring 容器。
三、@Autowired 解析规则
注入时 Spring 的查找顺序:
1. 按类型(Type)匹配 —— 若唯一则直接注入
2. 若有多个同类型 Bean:
a. 按 @Primary 标记的优先
b. 按字段/参数名称匹配 Bean 名称
c. 均不满足则报 NoUniqueBeanDefinitionException
@Primary
@Configuration
public class CacheConfig {
@Bean
@Primary // 存在多个 CacheManager 时优先选择此 Bean
public CacheManager redisCacheManager(RedisConnectionFactory factory) {
return RedisCacheManager.create(factory);
}
@Bean
public CacheManager caffeineCacheManager() {
return new CaffeineCacheManager();
}
}@Qualifier
精确指定 Bean 名称,优先级高于 @Primary:
@Service
public class PaymentService {
private final PaymentGateway gateway;
public PaymentService(
@Qualifier("stripeGateway") PaymentGateway gateway) {
this.gateway = gateway;
}
}注入集合 / Map
@Service
public class ExportService {
// 注入所有实现了 Exporter 接口的 Bean
private final List<Exporter> exporters;
// Key = Bean 名称,Value = Bean 实例
private final Map<String, Exporter> exporterMap;
public ExportService(List<Exporter> exporters,
Map<String, Exporter> exporterMap) {
this.exporters = exporters;
this.exporterMap = exporterMap;
}
public void export(String format, Object data) {
Exporter exporter = exporterMap.get(format + "Exporter");
if (exporter == null) {
throw new IllegalArgumentException("不支持的导出格式: " + format);
}
exporter.export(data);
}
}四、@Value 注入配置值
@Component
public class AppProperties {
// 注入简单值(带默认值)
@Value("${app.timeout:30}")
private int timeout;
// 注入列表
@Value("${app.allowed-origins:http://localhost:3000}")
private String[] allowedOrigins;
// SpEL 表达式
@Value("#{systemProperties['user.timezone'] ?: 'Asia/Shanghai'}")
private String timezone;
// 注入其他 Bean 的属性(SpEL)
@Value("#{appConfig.maxRetries * 2}")
private int maxRetries;
}复杂配置属性推荐改用
@ConfigurationProperties,详见 属性绑定。
五、作用域与注入陷阱
不同作用域 Bean 互相注入时需注意生命周期匹配:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String traceId = UUID.randomUUID().toString();
// 每次 HTTP 请求创建新实例
}
@Service
public class TraceService {
// RequestContext 是 request 作用域,TraceService 是 singleton
// 必须使用 proxyMode,否则注入的是同一个实例(请求 1 的实例)
@Autowired
private RequestContext requestContext;
}作用域详见 Bean作用域。
六、延迟注入
@Service
public class HeavyService {
// 延迟初始化:首次调用 get() 时才创建 Bean
@Autowired
private ObjectProvider<SlowDependency> slowDependencyProvider;
@Autowired
@Lazy
private AnotherHeavyService heavyService;
public void process() {
SlowDependency dep = slowDependencyProvider.getIfAvailable();
if (dep != null) {
dep.doWork();
}
}
}ObjectProvider 是处理可选依赖和延迟注入的推荐方式,同时支持 getIfUnique() 处理多 Bean 情况。
七、编程式获取 Bean
非注入场景(工具类、静态方法)中可通过 ApplicationContext 获取:
@Component
public class BeanLocator implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext ctx) {
context = ctx;
}
public static <T> T getBean(Class<T> type) {
return context.getBean(type);
}
public static <T> T getBean(String name, Class<T> type) {
return context.getBean(name, type);
}
}尽量通过注入使用 Bean;仅在无法注入时(如静态工具类、框架回调)才使用编程式获取。
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
NoSuchBeanDefinitionException | Bean 未注册或包未扫描到 | 检查 @ComponentScan 路径或添加 @Bean |
NoUniqueBeanDefinitionException | 多个同类型 Bean | 加 @Qualifier 或 @Primary |
UnsatisfiedDependencyException | 依赖无法满足 | 检查依赖是否已注册,或加 @Autowired(required=false) |
| 字段注入为 null | 在构造器中访问 @Autowired 字段 | 改用构造器注入 |
| 循环依赖 | A 依赖 B,B 依赖 A | 见 循环依赖 |