使用智能依赖注入设计更好的 Spring Boot 应用程序

发布: (2026年3月16日 GMT+8 10:17)
10 分钟阅读
原文: Dev.to

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

SituationRecommended ApproachAvoid
跨层共享依赖使用 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 管理共享依赖——但在需要定制化和灵活性时要自行掌控。

0 浏览
Back to Blog

相关文章

阅读更多 »

继承的简单代码示例

简单的银行系统代码示例,java 包 bank.task;public class BankAccount { int accountNumber; double balance; public void deposit(double depositAm...