原创

Spring DI 详解:Autowired 属性注入、构造方法注入与 Setter 注入实践

1. DI 与 IoC 的关系:先把概念说清楚

在 Spring 体系里,DI(Dependency Injection,依赖注入)不是孤立概念,它是 IoC(Inversion of Control,控制反转)最常见、最核心的落地方式。

很多文章会把两者混用,但严格来说,它们并不完全等价:

  • IoC 是一种设计思想

    • 对象不再主动创建或查找自己依赖的对象,而是把对象的创建、装配、生命周期管理交给容器。
  • DI 是 IoC 的实现方式

    • 容器把对象依赖的其他对象“注入”进来,而不是让对象自己通过 new、静态工厂或服务定位器去获取。

如果没有 Spring,业务代码通常会写成这样:

public class OrderService {

    private OrderRepository orderRepository = new OrderRepository();

    public void createOrder() {
        orderRepository.save();
    }
}

这段代码的问题非常明显:

  • OrderServiceOrderRepository 强耦合
  • 依赖对象的创建方式被写死
  • 单元测试不方便替换 Mock 实现
  • 后续切换实现类、增加代理、接入事务、AOP 都会变得麻烦

而使用 Spring DI 后:

@Service
public class OrderService {

    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public void createOrder() {
        orderRepository.save();
    }
}

此时:

  • OrderService 只依赖抽象关系或容器提供的对象
  • 依赖的创建与装配交由 Spring 容器完成
  • 更容易测试、扩展和维护
  • Spring 可以在 Bean 创建阶段完成代理增强、生命周期回调、依赖检查

因此,理解 Spring 的 DI,不能只停留在“会写 @Autowired”的层面,而应该理解它背后的容器装配机制、注入方式差异、适用场景和工程实践。


2. Spring 容器是如何完成依赖注入的

Spring DI 的前提是:被管理对象必须先成为 Spring Bean。 也就是说,Spring 先通过配置类、XML、组件扫描、@Bean 方法等方式把对象注册进容器,然后在 Bean 创建和初始化过程中处理依赖关系。

一个 Bean 进入 Spring 容器,大致经历以下过程:

  1. BeanDefinition 注册

    • Spring 扫描 @Component@Service@Repository@Controller
    • 或解析 @Configuration + @Bean
    • 或解析 XML 配置
  2. 实例化 Bean

    • 通过构造器、工厂方法等创建对象
  3. 属性填充

    • 对字段、Setter、其他可注入点进行依赖注入
  4. Aware 接口回调

    • BeanNameAwareBeanFactoryAware
  5. BeanPostProcessor 前置处理

  6. 初始化方法执行

    • @PostConstruct
    • InitializingBean.afterPropertiesSet()
    • 自定义 init-method
  7. BeanPostProcessor 后置处理

    • 例如 AOP 代理通常在这个阶段产生
  8. Bean 可用

  9. 容器关闭时销毁

    • @PreDestroy

在这套流程里,依赖注入主要发生在两个地方:

  • 构造方法注入:实例化阶段完成
  • 字段注入、Setter 注入:属性填充阶段完成

这也是为什么构造器注入和属性注入在使用体验、约束能力、空值控制、测试友好性上会有明显差别。


3. Spring 中常见的三种注入方式

Spring 中最常见的依赖注入方式有三类:

  1. 属性注入(字段注入,Field Injection)

    • 典型写法是 @Autowired 标注在成员变量上
  2. 构造方法注入(Constructor Injection)

    • 依赖通过构造器参数传入
  3. Setter 注入(Setter Injection)

    • 依赖通过 setXxx() 方法传入

三者都能工作,但它们的设计约束、可维护性和工程推荐级别并不一样。

先看一个统一的业务场景:

public interface UserRepository {
    void save(String username);
}
@Repository
public class JdbcUserRepository implements UserRepository {
    @Override
    public void save(String username) {
        System.out.println("save user: " + username);
    }
}

下面分别用三种方式实现 UserService


4. 属性注入 @Autowired:最常见,但并非最佳实践

4.1 基本写法

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public void register(String username) {
        userRepository.save(username);
    }
}

这就是最常见的 Spring 注入写法。 Spring 在创建 UserService 实例后,会通过反射把容器中的 UserRepository Bean 注入到该字段中。

这种方式的优点很直观:

  • 写法最短
  • 上手成本低
  • 代码视觉上简洁
  • 在很多老项目中大量存在

但“简单”并不代表“最优”,它在工程实践里存在很多隐患。


4.2 属性注入的底层机制

当 Spring 创建完 UserService 实例之后,会进入属性填充阶段。 AutowiredAnnotationBeanPostProcessor 会扫描 Bean 中标注了 @Autowired@Value@Inject 等注解的成员,然后解析依赖并通过反射进行赋值。

也就是说,字段注入不是通过构造器,不是通过显示方法调用,而是通过容器后处理器在运行时完成。

因此它的特点是:

  • 注入行为是隐式的
  • 依赖关系不体现在构造器签名中
  • 对象可以在“依赖未注入完成”前被创建出来

这恰恰是很多问题的根源。


4.3 @Autowired 的匹配规则

Spring 默认按类型注入。

例如:

@Autowired
private UserRepository userRepository;

Spring 会去容器里找 UserRepository 类型的 Bean。

场景一:只有一个候选 Bean

直接注入成功。

场景二:有多个候选 Bean

会抛出异常,常见异常是:

NoUniqueBeanDefinitionException

例如:

@Repository
public class JdbcUserRepository implements UserRepository {
}
@Repository
public class RedisUserRepository implements UserRepository {
}
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;
}

此时 Spring 不知道该注入哪个实现。

解决方式有三种。

方式一:配合 @Qualifier

@Service
public class UserService {

    @Autowired
    @Qualifier("jdbcUserRepository")
    private UserRepository userRepository;
}

方式二:使用 @Primary

@Repository
@Primary
public class JdbcUserRepository implements UserRepository {
}

这样当按类型注入且存在多个实现时,优先注入 @Primary 标记的 Bean。

方式三:按名称匹配作为补充

在某些场景下,Spring 会结合字段名、参数名辅助匹配,但核心仍然是按类型解析,再结合限定条件选择 Bean,不应把“字段名碰巧对上”当成主要设计手段。


4.4 required 属性

@Autowired 默认是必须依赖:

@Autowired
private UserRepository userRepository;

等价于:

@Autowired(required = true)
private UserRepository userRepository;

如果找不到依赖,启动时就会报错。

也可以改成非必须:

@Autowired(required = false)
private UserRepository userRepository;

但这通常意味着该字段可能为 null,需要在业务代码里自行判断:

public void register(String username) {
    if (userRepository != null) {
        userRepository.save(username);
    }
}

这种设计会降低对象状态的一致性。 对于核心依赖,不推荐把 required=false 当作常规方案。

更合理的做法一般是:

  • 用构造器明确声明“这是必须依赖”
  • 对可选依赖使用 Optional<T>ObjectProvider<T> 或条件化装配

例如:

@Service
public class UserService {

    private final Optional<UserRepository> userRepository;

    public UserService(Optional<UserRepository> userRepository) {
        this.userRepository = userRepository;
    }

    public void register(String username) {
        userRepository.ifPresent(repo -> repo.save(username));
    }
}

4.5 属性注入的优点

虽然工程上不推荐作为首选,但字段注入也不是一无是处,它的优点主要有:

1. 代码短,初学者容易接受

@Autowired
private UserRepository userRepository;

相比构造器注入,视觉上确实更“省事”。

2. 老项目兼容性高

大量 Spring、Spring Boot 早期项目都使用字段注入。 在维护老系统时,理解和兼容这种写法是必要的。

3. 对样板代码敏感的场景看起来简洁

在依赖很多但结构简单的类里,字段注入会显得比较紧凑。

但这些优点更多是“书写便利性”,而不是“设计质量”。


4.6 属性注入的核心问题

1. 依赖关系不显式

看下面这段代码:

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private EmailService emailService;

    @Autowired
    private SmsService smsService;
}

从类的对外接口上看,根本不知道这个类依赖什么。 只有进入类内部、阅读字段,才能发现它依赖多个组件。

而构造器注入会把依赖关系直接暴露在签名中:

public UserService(UserRepository userRepository,
                   EmailService emailService,
                   SmsService smsService) {
    ...
}

这会让类的设计更加透明。


2. 无法保证对象在构造完成后就是“完整可用”的

字段注入依赖容器后续通过反射赋值。 也就是说,对象先被创建,再被填充依赖。

这带来一个问题:对象构造完成时,并不一定处于完整状态

而好的面向对象设计通常要求: 一个对象一旦构造成功,就应该满足基本可用条件。


3. 不利于单元测试

字段注入的类在脱离 Spring 容器后,测试会比较麻烦。

例如:

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public void register(String username) {
        userRepository.save(username);
    }
}

在单元测试中,你不能优雅地直接构造它并传入 Mock,往往只能:

  • 启动 Spring 测试上下文
  • 或者通过反射设置字段
  • 或者引入 @InjectMocks

而构造器注入则天然适合测试:

UserRepository mockRepo = Mockito.mock(UserRepository.class);
UserService userService = new UserService(mockRepo);

这也是现代 Spring 项目越来越偏向构造器注入的重要原因。


4. 不利于不可变设计

字段注入要求依赖字段通常不能是 final,因为 Spring 需要在实例化后再赋值。

@Autowired
private UserRepository userRepository;

这意味着依赖本身不能天然具备不可变约束。 而构造器注入可以把依赖声明为 final

private final UserRepository userRepository;

这样类的状态更稳定,也更符合线程安全和可维护性思路。


5. 容易掩盖设计问题

一个类如果依赖过多,构造器注入会很快暴露这个问题,因为构造器参数会越来越长。 而字段注入会让这种坏味道被“隐藏”起来,导致类不断膨胀却不易察觉。

例如一个类有 10 个 @Autowired 字段,代码仍然“能写”,但这通常已经意味着职责过重、设计失衡。


4.7 属性注入适合什么场景

严格地说,在现代 Spring 工程中,字段注入更适合以下有限场景:

  • 历史项目维护
  • 测试样例、演示项目、临时代码
  • 对设计约束要求不高的简单组件
  • 某些框架集成代码中,对非核心依赖的快速装配

但在正式业务系统中,不应把字段注入作为默认首选方案


5. 构造方法注入:当前最推荐的依赖注入方式

5.1 基本写法

@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void register(String username) {
        userRepository.save(username);
    }
}

这就是典型构造器注入。

不过在现代 Spring 中,如果类只有一个构造器,那么 @Autowired 可以省略:

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void register(String username) {
        userRepository.save(username);
    }
}

这也是当前最常见、最推荐的写法。


5.2 为什么构造器注入是首选

1. 依赖关系显式

构造器参数就是这个类运行所需的依赖列表。 阅读类签名就能立即知道它依赖什么。

2. 保证对象完整性

对象只有在所有必要依赖都准备好后才能构造成功。 这符合“构造即完成初始化”的设计原则。

3. 支持 final 字段

private final UserRepository userRepository;

这让依赖在对象生命周期内不可变,更安全、更清晰。

4. 更利于单元测试

无需启动 Spring 容器,直接 new 出对象即可测试。

5. 更容易暴露设计问题

构造器参数过多时,说明这个类依赖太多、职责过重,需要拆分。 这是一种非常有价值的设计反馈机制。


5.3 只有一个构造器时,@Autowired 为什么可以省略

从 Spring 4.3 开始,如果一个 Bean 类中只有一个构造器,Spring 会自动使用这个构造器进行注入,即使没有显式标注 @Autowired

例如:

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

这在 Spring Framework 4.3+ 中成立。 Spring Boot 的现代项目通常都基于这一行为,因此实际开发中经常不再写构造器上的 @Autowired

版本说明

  • Spring Framework 4.3 之前:单构造器通常仍建议显式写 @Autowired
  • Spring Framework 4.3 及之后:单构造器可自动注入,无需 @Autowired
  • Spring Boot 2.x / 3.x:通常直接采用“单构造器省略 @Autowired”的写法

5.4 多构造器场景

如果一个类有多个构造器,Spring 就无法自动判断该选哪个,此时需要通过 @Autowired 明确指定注入构造器。

@Service
public class UserService {

    private final UserRepository userRepository;
    private final EmailService emailService;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
        this.emailService = null;
    }

    @Autowired
    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
}

不过从设计角度看,业务 Bean 最好避免保留多个语义混乱的构造器。 通常推荐:

  • 对外只保留一个主构造器
  • 必需依赖全部放进该构造器
  • 可选依赖用 Setter、ObjectProvider 或其他机制处理

5.5 多实现注入时的构造器写法

当接口有多个实现时,也可以在构造器参数上配合 @Qualifier

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(@Qualifier("jdbcUserRepository") UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

这种写法比字段上配 @Qualifier 更清晰,因为依赖约束直接体现在构造器签名中。


5.6 Lombok 与构造器注入

在真实项目中,构造器注入经常和 Lombok 一起使用,以减少模板代码。

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    public void register(String username) {
        userRepository.save(username);
    }
}

@RequiredArgsConstructor 会为所有 final 字段生成构造器。 配合 Spring 单构造器自动注入机制,代码会很简洁。

但要注意:

  • 使用 Lombok 是为了减少样板代码,不是为了掩盖依赖设计
  • 一个类如果有太多 final 字段,即使写法很优雅,也仍然意味着职责可能过重

5.7 构造器注入的潜在问题

构造器注入虽然最推荐,但也不是没有代价。

1. 依赖很多时,构造器会很长

public UserService(A a, B b, C c, D d, E e, F f) { ... }

这会让代码显得冗长。 但这个问题本质上不是“构造器注入的错”,而是类的职责过多。

2. 循环依赖更容易暴露

例如:

  • A 的构造器依赖 B
  • B 的构造器依赖 A
@Service
public class AService {
    public AService(BService bService) {
    }
}
@Service
public class BService {
    public BService(AService aService) {
    }
}

这种构造器循环依赖会导致容器无法创建 Bean。

但从工程角度讲,这恰恰是优点: 它强迫你正视设计问题,而不是让问题潜伏。


6. Setter 注入:适合可选依赖和后置配置

6.1 基本写法

@Service
public class UserService {

    private UserRepository userRepository;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void register(String username) {
        userRepository.save(username);
    }
}

Setter 注入的本质是:Spring 创建对象后,再调用 Setter 方法把依赖设置进去。


6.2 Setter 注入的特点

Setter 注入介于字段注入和构造器注入之间:

  • 比字段注入更显式,因为依赖通过公开方法注入
  • 但仍然是在对象创建后完成赋值
  • 适合“非必须”“可替换”“后置设置”的依赖

6.3 Setter 注入适合哪些场景

1. 可选依赖

如果某个依赖不是对象运行的绝对前提,就可以考虑 Setter 注入。

@Service
public class NotificationService {

    private SmsService smsService;

    @Autowired(required = false)
    public void setSmsService(SmsService smsService) {
        this.smsService = smsService;
    }

    public void notifyUser(String msg) {
        if (smsService != null) {
            smsService.send(msg);
        }
    }
}

2. 配置型依赖、开关型依赖

例如某些策略、扩展点、插件、适配器是可插拔的,不一定在构造阶段强制存在。

3. 遗留框架或 JavaBean 风格对象

有些对象强调无参构造 + Setter 赋值,这种情况下 Setter 注入比较自然。


6.4 Setter 注入的优点

1. 依赖可选性更强

构造器更适合必须依赖,Setter 更适合可选依赖。

2. 可读性比字段注入更好

@Autowired
public void setUserRepository(UserRepository userRepository) {
    this.userRepository = userRepository;
}

至少依赖注入行为通过方法暴露出来了。

3. 支持后续替换

某些场景下,一个依赖在生命周期中可能需要调整或替换,Setter 形式更灵活。


6.5 Setter 注入的缺点

1. 对象可能处于不完整状态

和字段注入一样,Setter 注入是在对象实例化后执行的。 如果依赖没有及时注入,对象可能不能正确工作。

2. 不适合不可变设计

Setter 天然意味着“可修改”,不适合 final 依赖。

3. 依赖是否必须不够直观

一个 Setter 方法到底是必须调用还是可选调用,要靠开发者约定和文档约束,语义不如构造器强。


7. 三种注入方式的对比

下面从工程实践角度做一个系统比较。

对比维度 属性注入 @Autowired 构造方法注入 Setter 注入
依赖是否显式 较明显
是否支持 final 不适合 支持 不支持
是否利于单元测试 较差 最好 较好
对象完整性 较弱 最强 较弱
是否适合必须依赖 一般 最适合 一般
是否适合可选依赖 可用但不优雅 可结合 Optional 最自然
是否容易暴露设计问题 不容易 容易 一般
使用便利性 最高 较高 中等
现代 Spring 推荐程度 不推荐作为首选 最推荐 特定场景可用

结论非常明确:

  • 必须依赖:优先使用构造方法注入
  • 可选依赖:可考虑 Setter 注入
  • 字段注入:不作为正式项目默认方案

8. @Autowired 不只是字段注入,它也能标在构造器、Setter、方法参数上

很多人一提 @Autowired,脑海里就只有:

@Autowired
private XxxService xxxService;

实际上,@Autowired 是一个自动装配注解,它不仅能标在字段上,还能标在:

  • 构造器
  • Setter 方法
  • 普通方法
  • 数组、集合、Map 参数
  • 某些场景下的配置方法参数

例如:

构造器注入

@Autowired
public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
}

Setter 注入

@Autowired
public void setUserRepository(UserRepository userRepository) {
    this.userRepository = userRepository;
}

普通方法注入

@Autowired
public void init(UserRepository userRepository, EmailService emailService) {
    this.userRepository = userRepository;
    this.emailService = emailService;
}

普通方法注入虽然技术上可行,但在日常业务代码中并不常作为主流选择。 它通常用于某些初始化组合场景,而不是通用依赖声明方式。


9. 注入集合、数组、Map:Spring 的批量装配能力

当容器中有多个同类型 Bean 时,Spring 不仅可以注入其中一个,也可以一次性注入全部候选对象。

9.1 注入 List

public interface PayService {
    String type();
}
@Service
public class AliPayService implements PayService {
    @Override
    public String type() {
        return "alipay";
    }
}
@Service
public class WechatPayService implements PayService {
    @Override
    public String type() {
        return "wechat";
    }
}
@Service
public class PaymentRouter {

    private final List<PayService> payServices;

    public PaymentRouter(List<PayService> payServices) {
        this.payServices = payServices;
    }

    public PayService getService(String type) {
        return payServices.stream()
                .filter(service -> service.type().equals(type))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("unsupported pay type"));
    }
}

这是一种很常见的策略模式实现方式。

9.2 注入 Map<String, T>

@Service
public class PaymentRouter {

    private final Map<String, PayService> payServiceMap;

    public PaymentRouter(Map<String, PayService> payServiceMap) {
        this.payServiceMap = payServiceMap;
    }

    public PayService getServiceByBeanName(String beanName) {
        return payServiceMap.get(beanName);
    }
}

这里的 key 默认是 Bean 名称,value 是对应 Bean 实例。

这种能力在做:

  • 策略路由
  • 插件式扩展
  • 多实现分发
  • 规则处理链

时非常有用。


10. @Resource@Inject@Autowired 的区别

虽然本文主题是 Spring DI,但工程中还经常看到 @Resource@Inject,有必要区分。

10.1 @Autowired

  • 来自 Spring
  • 核心是按类型装配
  • 可与 @Qualifier@Primary 配合
  • 是 Spring 体系中最常用的自动注入注解

10.2 @Resource

  • 来自 JSR-250 / Jakarta
  • 常见语义是先按名称,再按类型
  • 更偏向 Java 标准注解风格

例如:

@Resource(name = "jdbcUserRepository")
private UserRepository userRepository;

如果你明确希望按名称注入,@Resource 很直接。

10.3 @Inject

  • 来自 JSR-330 / Jakarta Inject
  • 语义上与 @Autowired 接近
  • 更偏标准化规范

不过在 Spring 项目中,团队通常会统一一种风格。 从可读性和生态习惯来看,现代 Spring 项目仍然以构造器注入 + Spring 原生方式最常见

版本说明

  • 在较新的 Spring 6 / Spring Boot 3 环境中,Jakarta 命名空间已全面替代旧 javax.*
  • 如果项目升级到 Spring Boot 3,需注意 javax.annotationjavax.injectjakarta.* 包迁移问题

11. Spring 循环依赖与注入方式的关系

循环依赖是理解 DI 时绕不开的话题。

11.1 什么是循环依赖

例如:

  • AService 依赖 BService
  • BService 又依赖 AService
@Service
public class AService {

    private final BService bService;

    public AService(BService bService) {
        this.bService = bService;
    }
}
@Service
public class BService {

    private final AService aService;

    public BService(AService aService) {
        this.aService = aService;
    }
}

这是典型的构造器循环依赖。

11.2 构造器注入下的循环依赖

这种场景通常会直接失败,因为:

  • 创建 AService 需要先有 BService
  • 创建 BService 又需要先有 AService

容器无法完成闭环。

11.3 字段注入 / Setter 注入为什么有时“看起来能行”

历史上,Spring 通过三级缓存等机制,对单例 Bean 的部分属性级循环依赖可以做一定程度的提前暴露和解决。 但这并不意味着循环依赖是合理设计,更不意味着应该依赖这种机制。

尤其是在现代 Spring Boot 版本中,循环依赖默认策略更严格,很多项目配置下会直接禁止。

工程结论

  • 不要为了“绕开循环依赖”而退回字段注入

  • 真正正确的做法是重构设计

    • 拆分类职责
    • 引入中间协调层
    • 提取公共能力
    • 使用事件机制解耦
    • 重新审视模块边界

构造器注入让循环依赖更早暴露,这不是缺点,而是设计质量保障。


12. 依赖注入的工程实践建议

12.1 对必须依赖,一律优先构造器注入

这是最重要的一条原则。

例如业务服务、仓储、网关、领域能力等核心依赖,应该通过构造器注入:

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    public OrderService(OrderRepository orderRepository,
                        InventoryService inventoryService,
                        PaymentService paymentService) {
        this.orderRepository = orderRepository;
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
    }
}

12.2 对可选依赖,优先考虑 Setter 注入或延迟获取

例如扩展能力、非核心通知能力、调试型组件等,可以考虑:

  • Setter 注入
  • Optional<T>
  • ObjectProvider<T>
  • 条件装配

例如:

@Service
public class ReportService {

    private final ObjectProvider<ExportService> exportServiceProvider;

    public ReportService(ObjectProvider<ExportService> exportServiceProvider) {
        this.exportServiceProvider = exportServiceProvider;
    }

    public void export() {
        ExportService exportService = exportServiceProvider.getIfAvailable();
        if (exportService != null) {
            exportService.export();
        }
    }
}

这种写法比 required = false 更清晰。


12.3 避免在正式项目中大量使用字段注入

字段注入的便利性很诱人,但不应以牺牲设计质量为代价。 尤其是在以下类型代码中,更不建议字段注入:

  • 领域服务
  • 核心业务服务
  • 复杂聚合协调器
  • 需要高可测试性的组件
  • 公共基础设施模块

12.4 构造器参数过多时,不要急着嫌弃构造器

当你发现一个类的构造器有 7 个、8 个参数时,第一反应不应该是“改成 @Autowired 字段更方便”,而应该是:

  • 这个类是不是职责过多?
  • 是否违反单一职责原则?
  • 是否存在多个子流程可以拆分?
  • 是否把不该耦合的能力揉在了一起?

也就是说,构造器长往往是在提醒你类设计有问题


12.5 团队要统一注入风格

在中大型项目中,最怕的是风格混乱:

  • 有人用字段注入
  • 有人用 Setter 注入
  • 有人写构造器 + @Autowired
  • 有人用 Lombok
  • 有人混用 @Resource

这会导致代码可读性和维护成本下降。 更合理的团队规范通常是:

  • 默认使用构造器注入
  • 单构造器省略 @Autowired
  • 可选依赖用 Setter 或 ObjectProvider
  • 多实现冲突时用 @Qualifier@Primary
  • 尽量减少字段注入

13. 一个完整示例:三种注入方式对照

13.1 依赖组件

public interface UserRepository {
    void save(String username);
}
@Repository
public class JdbcUserRepository implements UserRepository {

    @Override
    public void save(String username) {
        System.out.println("save user: " + username);
    }
}
@Service
public class EmailService {
    public void sendWelcomeMail(String username) {
        System.out.println("send email to: " + username);
    }
}

13.2 字段注入版本

@Service
public class UserServiceFieldInjection {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private EmailService emailService;

    public void register(String username) {
        userRepository.save(username);
        emailService.sendWelcomeMail(username);
    }
}

特点:

  • 写法最短
  • 依赖隐藏在字段中
  • 不利于测试与不可变设计

13.3 构造器注入版本

@Service
public class UserServiceConstructorInjection {

    private final UserRepository userRepository;
    private final EmailService emailService;

    public UserServiceConstructorInjection(UserRepository userRepository,
                                           EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }

    public void register(String username) {
        userRepository.save(username);
        emailService.sendWelcomeMail(username);
    }
}

特点:

  • 依赖关系清晰
  • final 保证状态稳定
  • 最适合正式项目

13.4 Setter 注入版本

@Service
public class UserServiceSetterInjection {

    private UserRepository userRepository;
    private EmailService emailService;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Autowired
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }

    public void register(String username) {
        userRepository.save(username);
        emailService.sendWelcomeMail(username);
    }
}

特点:

  • 比字段注入更显式
  • 适合某些后置设置或可选依赖
  • 但对象完整性不如构造器注入

14. 常见误区与纠正

14.1 误区一:@Autowired 就等于字段注入

错误。 @Autowired 是自动装配注解,不等于某一种注入姿势。 它可以用于字段、构造器、Setter、方法参数。


14.2 误区二:字段注入最简洁,所以最好

“简洁”只是表面书写成本低。 从可测试性、可维护性、对象一致性、设计透明度来看,字段注入并不是最佳方案。


14.3 误区三:构造器太长,所以不适合项目开发

构造器太长说明类设计可能有问题,而不是说明构造器注入有问题。 不要用字段注入去隐藏设计缺陷。


14.4 误区四:Setter 注入已经过时

也不准确。 Setter 注入并没有过时,它在“可选依赖、后置配置、JavaBean 风格对象”中仍有实际价值。 只是对于核心必须依赖,它不是首选。


14.5 误区五:循环依赖可以靠字段注入解决,所以字段注入更灵活

这是一种危险认知。 循环依赖本质上是设计问题,不能靠注入方式“糊过去”。 真正正确的做法是重构。


15. 最终结论:实际项目该怎么选

在 Spring 项目中,DI 的核心不是“会不会写注解”,而是是否能用正确的注入方式表达对象之间的真实依赖关系。

三种方式的定位可以总结为:

构造方法注入

最推荐,适合必须依赖。 它具备最好的显式性、可测试性、不可变性和设计约束能力,是现代 Spring 项目的默认首选。

Setter 注入

适合可选依赖、后置配置、扩展点装配。 它不是主流默认方案,但在合适场景下非常合理。

属性注入 @Autowired

最常见,但不应作为正式项目首选。 它适合历史代码、简单示例、临时场景,不适合承担现代业务系统中的主要依赖声明职责。

真正成熟的 Spring 开发实践,不是盲目追求代码最短,而是让依赖关系清晰、对象状态稳定、系统结构可测试、可维护、可扩展。 从这个角度看,构造器注入之所以成为主流,并不是因为“流行”,而是因为它更符合工程本质。

正文到此结束
评论插件初始化中...
Loading...