使用智能依赖注入设计更好的 Spring Boot 应用程序
Source: Dev.to
大多数 Spring Boot 开发者严重依赖 @Autowired——它在大多数情况下能正常工作——但也会出现失效的情况。
随着应用规模的扩大,这种便利性可能悄然引入隐藏的依赖、紧耦合,以及难以测试和维护的代码。
本文探讨了更智能的方式来构建可扩展、易维护的 Spring Boot 应用,超越了基础的依赖注入。
介绍
Spring Boot 最大的优势之一是依赖注入的使用非常简便——只需添加 @Autowired,一切即可工作。
然而,随着应用程序的演进,这种简易性可能会变成负担。你可能会开始遇到以下问题:
- 多个实现以及 Bean 之间的冲突
- 难以测试的隐藏依赖
- 由于不当的依赖注入导致的耦合
- 需要覆盖或自定义现有功能的某些部分
在此阶段,仅仅知道 如何 使用 @Autowired 已不够;你需要理解 何时 使用它——以及何时应该避免使用。
在本文中,我们将探讨实用的、面向真实场景的模式,以设计更简洁、更易维护的 Spring Boot 应用程序。
为什么依赖注入很重要
依赖注入不仅仅是对象的装配——它定义了职责在整个应用中的流动方式。
关键好处
- 松耦合 – 组件依赖抽象而不是具体实现。
- 可测试性 – 可以用 mock 或 stub 替换依赖进行单元测试。
- 可维护性 – 系统某一部分的更改对其他部分的影响最小。
- 关注点分离 – 每个类专注于单一职责。
- 灵活性 – 可以在不修改使用方代码的情况下替换实现。
Spring 消除了手动使用 new 创建对象的需求;它会为你管理对象的创建和装配。
Typical Spring Boot Architecture
Controller → Service → Repository → Database
↓
Helper / Validator这些层之间的依赖由 Spring 无缝管理。
何时使用 @Autowired
在使用 Spring 管理的 bean 时使用 Spring 注入。
示例:服务注入
@Service
public class ReportService {
private final DataExportService dataExportService;
public ReportService(DataExportService dataExportService) {
this.dataExportService = dataExportService;
}
public void generateReport(String type) {
dataExportService.export(type);
}
}常见使用场景
- Service → service 通信
- Repository 注入
- Helper/utility 组件
- Strategy‑pattern 实现
Source:
当你 不需要 @Autowired
1. 单构造函数注入
@Service
public class ReportService {
private final DataProcessor dataProcessor;
public ReportService(DataProcessor dataProcessor) {
this.dataProcessor = dataProcessor;
}
}如果一个类只有 一个 构造函数,Spring 会自动注入其依赖——不需要 @Autowired 注解。
2. 工具类
public class DateUtil {
public static String format(LocalDate date) {
return date.toString();
}
}对 无状态的帮助类(格式化器、映射器等)使用这种模式,根本不需要依赖注入。
3. 运行时对象
public class ReportCriteria {
private final String reportTypeKey;
public ReportCriteria(String reportTypeKey) {
this.reportTypeKey = reportTypeKey;
}
}将此类对象 在运行时动态创建(例如基于请求的数据、DTO、用户输入),而不是交给 Spring 管理。
避免字段注入
不推荐
@Autowired
private AddressValidator validator;推荐
private final InputValidator inputValidator;
public ProcessingService(InputValidator inputValidator) {
this.inputValidator = inputValidator;
}为什么推荐这样做
- 显式依赖
- 不可变字段
- 更容易进行单元测试
- 更好的整体设计
多个 Bean 引起混淆?使用 @Qualifier
@Service
public class ProcessingService {
private final Handler handler;
public ProcessingService(@Qualifier("primaryHandler") Handler handler) {
this.handler = handler;
}
}当同一依赖有多个实现时,使用 @Qualifier。
隐式注入(简洁方式)
@Service
public class ProfileService {
private final ProfileRepository profileRepository;
public ProfileService(ProfileRepository profileRepository) {
this.profileRepository = profileRepository;
}
}Spring 即使没有 @Autowired 也会自动注入依赖。
排除 Bean 的扫描
@ComponentScan(
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = LegacyAuthHandler.class
)
)防止不需要的 Bean 被加载到应用程序上下文中。
手动 Bean 注册
当 Spring 无法自动检测到某个类且需要手动控制时,显式定义 Bean:
@Configuration
public class SystemConfig {
@Bean
public ObsoleteProcessor processor() {
return new ObsoleteProcessor();
}
}在自动组件扫描不足以满足需求时,请使用此方法。
Multiple Beans? Let @Primary Pick the Default
@Component
@Primary
public class PrimaryHandler implements Handler {
}将 bean 标记为 @Primary,这样在存在多个候选 bean 时,Spring 会自动选择它。
要点
- 首选 构造函数注入(隐式或显式),而不是字段注入。
- 使用
@Qualifier或@Primary来解决多 Bean 场景。 - 仅在必要时才排除或手动注册 Bean。
- 将工具类和运行时对象 保持在 Spring 容器之外。
通过应用这些模式,您将构建更易于测试、扩展和维护的 Spring Boot 应用程序,随着项目的增长也能保持良好。
部分覆盖(Inheritance)
public class LogFormatter extends StandardFormatter {
@Override
public String getHeader() {
return "Log Header";
}
}当您需要修改现有类的少量部分时使用此方法。
更佳方法:组合
@Component
public class CustomBillingCalculator {
private final BaseBillingCalculator baseBillingCalculator;
public CustomBillingCalculator(BaseBillingCalculator baseBillingCalculator) {
this.baseBillingCalculator = baseBillingCalculator;
}
public double calculate(double amount) {
if (amount > 1000) {
return amount * 0.08;
}
return baseBillingCalculator.calculate(amount);
}
}更倾向于使用组合而非继承,以获得灵活性。
组合允许你通过组合更小的组件来构建行为,而继承会创建难以更改的僵硬结构。
注入多个实现
public PromotionEngine(List<PromotionStrategy> promotionStrategies) {
this.promotionStrategies = promotionStrategies;
}使用此模式来构建动态、可扩展的系统(例如插件架构)。
可选依赖
public MessageHandler(Optional<NotificationSender> notificationSender) {
this.notificationSender = notificationSender;
}当某些功能是可选的且缺失时不应导致应用程序崩溃,请使用此方式。
Spring DI: What to Use and When
| Situation | Recommended Approach | Avoid |
|---|---|---|
| 跨层共享依赖 | 使用 Spring 注入 | 手动使用 new 创建 |
| 存在多个实现 | 使用 @Qualifier | 依赖模糊注入 |
| 需要一个默认实现 | 使用 @Primary | 到处添加 @Qualifier |
| 需要细粒度控制 | 使用带排除的 @Bean | 盲目组件扫描 |
| 部分自定义行为 | 使用组合/覆盖 | 深层继承链 |
| 可选功能(短信、邮件等) | 使用 Optional 注入 | 强制 bean 存在 |
Spring DI:何时不使用 Spring 注入
| 场景 | 替代做法 |
|---|---|
| 运行时创建的对象(DTO,请求数据) | 使用 new 创建 |
| 工具/辅助类 | 使用静态方法或普通类 |
| 不需要生命周期管理 | 保持在 Spring 之外 |
实际场景模式
- 当存在多个 bean 时使用
@Qualifier。 - 使用
@Primary定义默认实现。 - 使用组件排除 + 自定义
@Bean进行精细控制。 - 使用组合或部分覆盖实现灵活定制。
这种组合实现了清晰、可扩展且面向企业的架构。
结论:更智能的 Spring DI
@Autowired 功能强大——但盲目在所有地方使用会导致设计不佳。
真正的优势在于为不同情境选择合适的工具:
- 优先使用构造函数注入,以获得清晰性和不可变性。
- 在存在多个实现时使用
@Qualifier。 - 使用
@Primary设定合理的默认实现。 - 定义自定义
@Bean方法,以获得完整控制。 - 优先采用组合而非继承。
黄金法则:
让 Spring 管理共享依赖——但在需要定制化和灵活性时要自行掌控。