文章目录
一、依赖注入基础概念
1.1 什么是依赖注入
依赖注入(Dependency Injection, DI)是一种设计模式,也是Spring框架的核心特性之一。它通过外部容器将对象所依赖的其他对象"注入"到对象中,而不是由对象自己创建依赖对象。
通俗理解:想象你去餐厅吃饭:
- 没有DI:你需要自己去菜市场买菜、自己做饭、自己洗碗(自己管理所有依赖)
- 有DI:你只需要告诉服务员你想吃什么(声明依赖),餐厅(容器)会把做好的菜送到你面前(注入依赖)
1.2 为什么需要依赖注入
传统方式问题 | DI解决方案 |
---|---|
紧耦合:类直接创建依赖对象,难以替换 | 松耦合:依赖通过接口注入,易于替换实现 |
难以测试:依赖硬编码,无法mock | 易于测试:可以注入mock对象 |
代码重复:每个类自己管理依赖 | 集中管理:容器统一管理对象生命周期 |
难以扩展:修改依赖需要改代码 | 灵活配置:通过配置即可修改依赖 |
1.3 Spring中的三种依赖注入方式
// 1. 构造器注入(推荐)
public class UserService {
private final UserRepository userRepo;
@Autowired // Spring 4.3+ 可省略
public UserService(UserRepository userRepo) {
this.userRepo = userRepo;
}
}
// 2. Setter注入
public class OrderService {
private PaymentService paymentService;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
// 3. 字段注入(不推荐)
public class ProductService {
@Autowired
private InventoryService inventoryService;
}
对比表:
注入方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
构造器注入 | 不可变、完全初始化的对象;易于测试 | 参数多时构造函数会很长 | 推荐的主要方式,特别是必需依赖 |
Setter注入 | 可选依赖灵活;支持重新配置 | 对象可能在非完全初始化状态下被使用 | 可选依赖或需要重新绑定的情况 |
字段注入 | 简洁;不需要样板代码 | 难以测试;隐藏依赖;不能声明final字段 | 不推荐生产代码使用 |
二、Spring Boot中的DI实战
2.1 基本组件定义与注入
步骤1:定义服务组件
// 商品仓库接口
public interface ProductRepository {
Product findById(Long id);
void save(Product product);
}
// 实现类 - 加上@Component注解让Spring管理
@Repository // @Repository是@Component的特化,用于DAO层
public class JpaProductRepository implements ProductRepository {
@Override
public Product findById(Long id) {
// 实际实现会使用JPA操作数据库
return new Product(id, "Sample Product", 99.99);
}
@Override
public void save(Product product) {
System.out.println("Saving product: " + product.getName());
}
}
步骤2:定义业务服务
@Service // @Service是@Component的特化,用于服务层
public class ProductService {
private final ProductRepository productRepo;
// 构造器注入
public ProductService(ProductRepository productRepo) {
this.productRepo = productRepo;
}
public Product getProductDetails(Long productId) {
return productRepo.findById(productId);
}
public void addProduct(Product product) {
productRepo.save(product);
}
}
步骤3:控制器层使用服务
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
// 构造器注入
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.getProductDetails(id);
}
@PostMapping
public void createProduct(@RequestBody Product product) {
productService.addProduct(product);
}
}
2.2 依赖注入的流程解析
Spring Boot中DI的工作流程可以用以下mermaid图表示:
三、高级依赖注入特性
3.1 处理多个同类型Bean
当有多个同类型Bean时,Spring会抛出NoUniqueBeanDefinitionException
。解决方案:
方案1:使用@Primary
@Repository
@Primary // 当有多个ProductRepository时优先选择这个
public class JpaProductRepository implements ProductRepository {
// 实现代码
}
@Repository
public class MongoProductRepository implements ProductRepository {
// 实现代码
}
方案2:使用@Qualifier
@Service
public class ProductService {
private final ProductRepository productRepo;
public ProductService(
@Qualifier("mongoProductRepository") ProductRepository productRepo) {
this.productRepo = productRepo;
}
}
方案3:使用自定义限定符
@Repository
@Qualifier("jpa")
public class JpaProductRepository implements ProductRepository {
// 实现代码
}
@Service
public class ProductService {
@Autowired
@Qualifier("jpa")
private ProductRepository productRepo;
}
3.2 条件化Bean装配
Spring Boot提供了多种条件注解来控制Bean的装配:
@Configuration
public class DataSourceConfig {
// 只有classpath下有H2驱动时才创建
@Bean
@ConditionalOnClass(name = "org.h2.Driver")
public DataSource h2DataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
// 只有特定属性设置为true时才创建
@Bean
@ConditionalOnProperty(name = "app.feature.mongo.enabled", havingValue = "true")
public MongoClient mongoClient() {
return new MongoClient("localhost");
}
// 只有缺少某种Bean时才创建
@Bean
@ConditionalOnMissingBean
public CacheManager cacheManager() {
return new SimpleCacheManager();
}
}
3.3 延迟初始化
@Configuration
public class AppConfig {
@Bean
@Lazy // 只有第一次被使用时才会初始化
public ExpensiveService expensiveService() {
return new ExpensiveService();
}
}
初始化时机对比:
初始化方式 | 时机 | 优点 | 缺点 |
---|---|---|---|
默认 | 应用启动时 | 提前发现问题 | 启动时间较长 |
@Lazy | 第一次被依赖时 | 启动快 | 运行时可能发现问题较晚 |
四、依赖注入原理深度解析
4.1 Spring容器核心接口
- BeanFactory:基础容器接口,提供基本的DI功能
- ApplicationContext:扩展了BeanFactory,添加了企业级功能(国际化、事件发布等)
- DefaultListableBeanFactory:完整的IoC实现,可独立使用
4.2 依赖解析过程
- Bean定义注册:扫描@Component等注解或XML配置,注册Bean定义
- 依赖分析:解析每个Bean的依赖关系(构造器参数、@Autowired字段等)
- 创建顺序确定:根据依赖关系确定Bean创建顺序
- 实例化:通过反射创建Bean实例
- 属性填充:注入依赖(递归处理依赖的依赖)
- 初始化:调用@PostConstruct方法、实现InitializingBean接口的方法等
- 使用:完全初始化的Bean可以被其他Bean使用
4.3 循环依赖处理
Spring通过三级缓存解决构造器注入外的循环依赖:
// 三级缓存结构示例(简化概念)
Map<String, Object> singletonObjects = ... // 一级缓存:完整Bean
Map<String, Object> earlySingletonObjects = ... // 二级缓存:提前暴露的原始Bean
Map<String, ObjectFactory<?>> singletonFactories = ... // 三级缓存:Bean工厂
// 处理流程
A依赖B -> 创建A半成品 -> 放入三级缓存 -> 创建B -> B需要A -> 从三级缓存获取A的工厂 -> 获取A早期引用 -> B完成 -> 注入到A -> A完成
限制:
- 构造器注入无法解决循环依赖
- 原型(prototype)作用域的Bean无法解决循环依赖
五、最佳实践与常见问题
5.1 依赖注入最佳实践
-
优先使用构造器注入
- 保证依赖不可变(final字段)
- 明确必须的依赖
- 易于测试
-
接口编程
// 好:依赖抽象接口 public class OrderService { private final PaymentGateway gateway; public OrderService(PaymentGateway gateway) { this.gateway = gateway; } } // 不好:依赖具体实现 public class OrderService { private final PayPalGateway gateway; }
-
避免使用字段注入
- 难以测试(需要反射)
- 隐藏依赖关系
- 不能声明final
-
合理使用@ComponentScan
- 指定明确的basePackages避免扫描过多类
@SpringBootApplication @ComponentScan(basePackages = "com.example.myapp") public class MyApp {}
5.2 常见问题解决方案
问题1:NoSuchBeanDefinitionException
可能原因:
- 类没有加@Component或其衍生注解
- 包不在@ComponentScan范围内
- Bean的条件不满足(@Conditional)
问题2:Bean创建顺序问题
解决方案:
- 使用@DependsOn明确依赖顺序
@Service @DependsOn("databaseInitializer") public class StartupService {}
问题3:代理导致的注入问题
当使用AOP时,Spring会创建代理对象,可能导致:
- 自我注入(self-injection)失败
- 类型转换异常
解决方案:
- 通过AopContext获取当前代理(需要开启exposeProxy)
@Service public class MyService { public void doSomething() { MyService proxy = (MyService) AopContext.currentProxy(); proxy.internalMethod(); // 确保AOP生效 } }
六、实战案例:电商系统DI设计
6.1 领域模型设计
// 商品领域
public interface ProductCatalog {
List<Product> listProducts();
Product getProduct(Long id);
}
@Repository
public class JpaProductCatalog implements ProductCatalog {
// JPA实现
}
// 订单领域
public interface OrderService {
Order createOrder(Long productId, int quantity);
}
@Service
public class DefaultOrderService implements OrderService {
private final ProductCatalog productCatalog;
private final PaymentGateway paymentGateway;
public DefaultOrderService(ProductCatalog catalog, PaymentGateway gateway) {
this.productCatalog = catalog;
this.paymentGateway = gateway;
}
// 实现方法
}
6.2 支付模块的多实现
// 支付网关接口
public interface PaymentGateway {
PaymentResult process(PaymentRequest request);
}
// 支付宝实现
@Service
@Qualifier("alipay")
public class AlipayGateway implements PaymentGateway {
@Override
public PaymentResult process(PaymentRequest request) {
// 支付宝具体逻辑
}
}
// 微信支付实现
@Service
@Qualifier("wechat")
public class WechatPayGateway implements PaymentGateway {
@Override
public PaymentResult process(PaymentRequest request) {
// 微信支付具体逻辑
}
}
// 根据配置动态选择
@Service
public class PaymentService {
private final PaymentGateway gateway;
@Autowired
public PaymentService(@Value("${payment.provider}") String provider,
@Qualifier("alipay") PaymentGateway alipay,
@Qualifier("wechat") PaymentGateway wechat) {
this.gateway = "alipay".equals(provider) ? alipay : wechat;
}
}
6.3 测试策略
@SpringBootTest
public class OrderServiceTest {
@MockBean
private ProductCatalog mockCatalog;
@MockBean
private PaymentGateway mockGateway;
@Autowired
private OrderService orderService;
@Test
public void testCreateOrder() {
// 准备mock数据
when(mockCatalog.getProduct(anyLong()))
.thenReturn(new Product(1L, "Test", 100.0));
// 调用测试方法
Order order = orderService.createOrder(1L, 2);
// 验证
assertEquals(200.0, order.getTotalAmount());
verify(mockGateway).process(any());
}
}
七、总结
Spring Boot的依赖注入是现代Java应用开发的核心技术,正确理解和使用DI可以带来以下好处:
- 松耦合:组件之间通过接口交互,减少直接依赖
- 可测试性:易于注入mock对象进行单元测试
- 可维护性:配置集中管理,修改依赖无需改动代码
- 灵活性:通过配置切换不同实现,适应不同环境
记住DI的核心原则:应用组件不应该负责查找或创建它们所依赖的对象,而应该由外部容器提供这些依赖。
关注不关注,你自己决定(但正确的决定只有一个)。
喜欢的点个关注,想了解更多的可以关注微信公众号 “Eric的技术杂货库” ,提供更多的干货以及资料下载保存!