1. 为什么我们需要IoC和DI
在传统Java开发中,对象创建和依赖管理通常是这样的:
java复制public class OrderService {
private OrderDao orderDao = new OrderDaoImpl();
public void createOrder() {
orderDao.save();
}
}
这种硬编码方式存在几个明显问题:
- 对象创建逻辑散落在各个业务类中
- 依赖关系难以修改(比如要换OrderDao实现)
- 单元测试困难(无法mock依赖)
Spring的IoC容器通过以下方式解决这些问题:
- 将对象的创建权从程序员手中"反转"给容器
- 通过依赖注入(DI)自动装配对象关系
- 提供统一的配置管理入口
关键理解:IoC是一种设计思想,DI是实现这种思想的具体方式。Spring通过ApplicationContext作为IoC容器实现这一机制。
2. Spring IoC容器核心实现机制
2.1 容器启动过程解析
Spring IoC容器的初始化流程:
- 资源定位:找到配置文件(XML/注解/JavaConfig)
- 加载解析:将配置转换为BeanDefinition
- 注册存储:将BeanDefinition存入注册表
- 依赖注入:根据依赖关系完成装配
- 初始化回调:执行初始化方法(@PostConstruct等)
java复制// 典型容器初始化代码
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
OrderService orderService = context.getBean(OrderService.class);
2.2 BeanDefinition详解
每个托管对象在容器中都对应一个BeanDefinition,包含关键元数据:
| 属性 | 说明 | 示例 |
|---|---|---|
| className | Bean的全限定类名 | com.example.OrderServiceImpl |
| scope | 作用域(singleton/prototype等) | singleton |
| lazyInit | 是否延迟初始化 | false |
| initMethodName | 初始化方法 | init |
| propertyValues | 属性值 |
2.3 依赖注入的三种方式
2.3.1 构造器注入
java复制public class OrderService {
private final OrderDao orderDao;
public OrderService(OrderDao orderDao) {
this.orderDao = orderDao;
}
}
优点:
- 保证依赖不可变(final)
- 保证依赖不为null
- 适合强制依赖
2.3.2 Setter注入
java复制public class OrderService {
private OrderDao orderDao;
public void setOrderDao(OrderDao orderDao) {
this.orderDao = orderDao;
}
}
适用场景:
- 可选依赖
- 需要重新配置的依赖
2.3.3 字段注入(不推荐)
java复制public class OrderService {
@Autowired
private OrderDao orderDao;
}
问题:
- 破坏了封装性
- 难以进行单元测试
- 可能产生NPE
最佳实践:优先使用构造器注入,可选依赖使用Setter注入,避免字段注入。
3. 高级配置与特性
3.1 Bean作用域管理
Spring支持多种作用域:
| 作用域 | 说明 | 适用场景 |
|---|---|---|
| singleton | 单例(默认) | 无状态服务 |
| prototype | 每次获取新实例 | 有状态对象 |
| request | 每个HTTP请求一个实例 | Web应用 |
| session | 每个会话一个实例 | 用户会话数据 |
| application | ServletContext生命周期 | 全局数据 |
配置示例:
xml复制<bean id="userPreference" class="com.example.UserPreference" scope="session">
<aop:scoped-proxy/>
</bean>
3.2 生命周期回调
完整生命周期:
- 实例化
- 属性填充
- BeanNameAware.setBeanName()
- BeanFactoryAware.setBeanFactory()
- ApplicationContextAware.setApplicationContext()
- BeanPostProcessor.postProcessBeforeInitialization()
- @PostConstruct
- InitializingBean.afterPropertiesSet()
- 自定义init-method
- BeanPostProcessor.postProcessAfterInitialization()
- 使用中...
- @PreDestroy
- DisposableBean.destroy()
- 自定义destroy-method
示例配置:
java复制public class ExampleBean implements InitializingBean, DisposableBean {
@PostConstruct
public void init() {
// 初始化逻辑
}
@PreDestroy
public void cleanup() {
// 清理逻辑
}
@Override
public void afterPropertiesSet() throws Exception {
// 属性设置后处理
}
@Override
public void destroy() throws Exception {
// 销毁处理
}
}
3.3 条件化装配
使用@Conditional实现条件装配:
java复制@Configuration
public class DataSourceConfig {
@Bean
@Conditional(DevEnvironmentCondition.class)
public DataSource devDataSource() {
return new EmbeddedDatabaseBuilder().build();
}
@Bean
@Conditional(ProdEnvironmentCondition.class)
public DataSource prodDataSource() {
// 生产环境数据源配置
}
}
public class DevEnvironmentCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return "dev".equals(context.getEnvironment().getProperty("env"));
}
}
4. 实战中的典型问题与解决方案
4.1 循环依赖问题
场景:
java复制@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA;
}
Spring的解决方案:
- 构造器注入无法解决循环依赖
- 属性/Setter注入通过三级缓存解决:
- 一级缓存:单例对象池(完全初始化好的Bean)
- 二级缓存:早期曝光对象(已实例化但未初始化)
- 三级缓存:对象工厂(用于生成早期曝光对象)
最佳实践:
- 尽量避免循环依赖
- 使用@Lazy延迟加载
- 重构设计(提取公共逻辑到第三个类)
4.2 自动装配的歧义问题
当存在多个同类型Bean时:
java复制@Autowired
private DataSource dataSource; // 报错:期望单个Bean但找到多个
解决方案:
- 使用@Primary标记首选Bean
java复制@Bean @Primary public DataSource primaryDataSource() {...} - 使用@Qualifier指定Bean名称
java复制@Autowired @Qualifier("secondaryDataSource") private DataSource dataSource; - 使用自定义限定注解
java复制@Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Production {}
4.3 性能优化技巧
-
延迟初始化:
xml复制<bean id="lazyBean" class="com.example.ExpensiveBean" lazy-init="true"/>或全局配置:
xml复制<beans default-lazy-init="true"> <!-- beans --> </beans> -
BeanDefinition合并:
- 父
定义通用配置 - 子
通过parent属性继承 - 减少重复配置
- 父
-
使用原型作用域需谨慎:
- 原型Bean不会缓存
- 频繁创建影响性能
- 考虑使用方法注入替代
5. 现代Spring的最佳实践
5.1 注解驱动开发
推荐配置方式:
java复制@Configuration
@ComponentScan(basePackages = "com.example")
@PropertySource("classpath:app.properties")
public class AppConfig {
@Bean
public DataSource dataSource(Environment env) {
// 基于环境的配置
}
@Bean
@Profile("dev")
public MockService mockService() {
return new MockService();
}
}
5.2 组合注解的使用
创建自定义注解简化配置:
java复制@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@EnableWebMvc
public @interface RestApplication {
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] value() default {};
}
使用方式:
java复制@RestApplication("com.example")
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}
5.3 测试支持
集成测试示例:
java复制@SpringBootTest
@ContextConfiguration(classes = TestConfig.class)
public class OrderServiceTest {
@Autowired
private OrderService orderService;
@MockBean
private OrderDao orderDao;
@Test
public void testCreateOrder() {
// 测试逻辑
}
}
配置测试专用Bean:
java复制@TestConfiguration
public class TestConfig {
@Bean
@Primary
public PaymentService mockPaymentService() {
return new MockPaymentService();
}
}
在实际项目中,理解IoC和DI的核心原理后,应该根据项目规模选择适当的配置方式。小型项目可以完全使用注解驱动,大型企业应用可能需要结合XML配置进行模块化管理。关键是要保持一致性,避免混合使用多种风格导致维护困难。
