Spring依赖注入的三种方式

目录

[① 导读卡片](#① 导读卡片)

[② 背景与目标](#② 背景与目标)

为什么需要依赖注入?

[③ 概念与原理](#③ 概念与原理)

三种注入方式概览

字段注入:最常见但不推荐

[Setter 注入:可选依赖场景可用](#Setter 注入:可选依赖场景可用)

构造器注入:官方推荐

[底层原理:Spring 如何注入?](#底层原理:Spring 如何注入?)

[④ 逻辑与对比](#④ 逻辑与对比)

[核心对比:构造器注入 vs 字段注入](#核心对比:构造器注入 vs 字段注入)

什么场景选什么?

[⑤ 核心详解](#⑤ 核心详解)

[详解 1:依赖不可变(final 字段)](#详解 1:依赖不可变(final 字段))

[详解 2:循环依赖暴露](#详解 2:循环依赖暴露)

[详解 3:空值安全](#详解 3:空值安全)

[详解 4:单元测试友好](#详解 4:单元测试友好)

[详解 5:与 Spring 解耦](#详解 5:与 Spring 解耦)

[详解 6:Lombok 简化构造器注入](#详解 6:Lombok 简化构造器注入)

[⑥ 案例实战](#⑥ 案例实战)

[实战 1:从字段注入重构到构造器注入](#实战 1:从字段注入重构到构造器注入)

[实战 2:条件注入(Spring Boot 4.x+ 新特性)](#实战 2:条件注入(Spring Boot 4.x+ 新特性))

[实战 3:多实现类注入](#实战 3:多实现类注入)

[⑦ 避坑 & 最佳实践](#⑦ 避坑 & 最佳实践)

[❌ 常见坑点](#❌ 常见坑点)

[✅ 最佳实践](#✅ 最佳实践)

[Spring 官方怎么说?](#Spring 官方怎么说?)

[⑧ 总结 & 路线图](#⑧ 总结 & 路线图)

一句话总结

三句话记住一天

下一步去哪?


① 导读卡片

项目 内容
一句话定位 一篇讲透 Spring 依赖注入的三种方式(字段注入、Setter 注入、构造器注入),看完你就知道为什么大厂都推荐构造器注入
适合人群 初中级 Java 开发者、Spring 初学者、准备面试的同学
难度 ⭐⭐(基础)
阅读时长 12 分钟
前置知识 知道什么是 IoC(控制反转)、DI(依赖注入)

② 背景与目标

为什么需要依赖注入?

没有 Spring 的时候,代码长这样:

java 复制代码
public class OrderService {
    private OrderRepository orderRepository = new OrderRepository(); // 硬编码!
}

这种写法的问题:OrderServiceOrderRepository 紧耦合,想换一个 OrderRepository 的实现(比如从 MySQL 切到 MongoDB),必须改源码。

依赖注入(Dependency Injection, DI) 解决了这个问题------由 Spring 容器负责创建并注入依赖,业务类只声明「我需要什么」,不关心「谁来创建」。

学完本文,你能够:

  • 掌握三种注入方式的写法与适用场景

  • 理解为什么构造器注入是官方推荐方案

  • 用代码证明字段注入有哪些潜在风险

  • 在 Code Review 中自信地指出不合理注入方式


③ 概念与原理

三种注入方式概览

注入方式 写法特点 是否支持 final 是否需要 Spring 注解 依赖可见性
字段注入 属性上标 @Autowired ❌ 不支持 ✅ 需要 隐藏
Setter 注入 Setter 方法上标 @Autowired ❌ 不支持 ✅ 需要 中等
构造器注入 构造器参数自动注入 ✅ 支持 ❌ 不需要 一目了然

字段注入:最常见但不推荐

java 复制代码
@Service
public class OrderService {
    @Autowired  // 字段上直接标注
    private OrderRepository orderRepository;
}

特点 :代码最简洁,但问题最多

Setter 注入:可选依赖场景可用

java 复制代码
@Service
public class OrderService {
    private OrderRepository orderRepository;
    
    @Autowired
    public void setOrderRepository(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
}

特点:支持运行时更换实现(不常用)。

构造器注入:官方推荐

java 复制代码
@Service
public class OrderService {
    private final OrderRepository orderRepository; // final!
    
    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
}

特点final 保证不可变,没有 Spring 注解也 OK。

底层原理:Spring 如何注入?

Spring 通过 AutowiredAnnotationBeanPostProcessor 这个 BeanPostProcessor 处理 @Autowired

java 复制代码
// Spring 内部伪代码
public class AutowiredAnnotationBeanPostProcessor implements BeanPostProcessor {
    
    @Override
    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
        // 1. 解析类中的 @Autowired 字段/方法
        InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass());
        // 2. 遍历所有需要注入的点
        for (InjectedElement element : metadata) {
            // 3. 从容器中查找匹配的 Bean
            Object dependency = beanFactory.resolveDependency(element.getDependencyDescriptor());
            // 4. 通过反射设置字段值(字段注入本质是反射)
            element.inject(bean, dependency, null);
        }
        return pvs;
    }
}

字段注入的本质是反射注入 ,而构造器注入是正常的 Java 构造器调用


④ 逻辑与对比

核心对比:构造器注入 vs 字段注入

对比维度 构造器注入 字段注入 实际影响
依赖不可变 ✅ 支持 final 字段 ❌ 不支持 防止依赖被意外修改,更安全
循环依赖暴露 ✅ 启动时直接报错 ❌ 可能被三级缓存隐藏 提前发现问题
空值安全 ✅ 构造时检查非空 ❌ 运行时才报 NPE 尽早发现配置错误
单元测试 ✅ 直接 new 传 mock ❌ 需要 @InjectMocks 等框架魔法 测试更简洁
依赖可见性 ✅ 参数列表清晰 ❌ 需要满类找 @Autowired 代码审查更高效
与 Spring 解耦 ✅ 无需 Spring 注解 ❌ 依赖 @Autowired 非 Spring 环境可复用

什么场景选什么?

复制代码
你的场景                    推荐方式
────────────────────────────────────────────
新项目、标准业务代码      →  构造器注入(推荐)
必须用字段注入的老项目     →  维持现状,逐步重构
可选依赖(非必需)         →  Setter 注入
单元测试中的 Mock 对象     →  构造器注入直接传
Spring Boot + Lombok      →  构造器注入 + @RequiredArgsConstructor

⑤ 核心详解

详解 1:依赖不可变(final 字段)

java 复制代码
// ❌ 字段注入 ------ 依赖可变,可被意外修改
@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    
    public void dangerousMethod() {
        this.orderRepository = null; // 编译通过,运行时 NPE!
    }
}
​
// ✅ 构造器注入 ------ 依赖不可变
@Service
public class OrderService {
    private final OrderRepository orderRepository; // final!
    
    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
    
    public void safeMethod() {
        this.orderRepository = null; // ❌ 编译报错!final 字段不能重新赋值
    }
}

详解 2:循环依赖暴露

java 复制代码
// 两个类互相依赖
@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;
    }
}

构造器注入版本:启动时直接报错:

复制代码
Error creating bean with name 'aService': 
Requested bean is currently in creation: Is there an unresolvable circular reference?

字段注入版本:Spring 通过三级缓存勉强解决,启动成功,但:

  • 隐藏了设计问题

  • 未来代码复杂化后可能在某个请求时炸出奇怪的问题

  • 线上事故比启动报错更难排查

核心结论:循环依赖是设计问题,不是技术问题。构造器注入强制你在开发阶段就重构代码(比如拆出一个 C 类),而不是留到生产环境出事。


详解 3:空值安全

java 复制代码
// ✅ 构造器注入 + Lombok @NonNull
import lombok.NonNull;

@Service
public class UserService {
    private final UserRepository repo;
    
    public UserService(@NonNull UserRepository repo) {
        this.repo = repo; // repo 为 null 时立即 NPE
    }
}

// ❌ 字段注入 ------ 缺失依赖时延迟报错
@Service
public class UserService {
    @Autowired
    private UserRepository repo; // 如果这个 Bean 不存在,启动时不报错
    
    public void findUser() {
        repo.findById(1L); // ❌ 第一次调用时才 NPE!
    }
}

实际后果

  • 字段注入:UserService 被成功创建,第一个用户请求进来 → 500 错误 → 线上事故

  • 构造器注入:应用启动直接失败,CI/CD 流水线卡住,开发立刻知晓


详解 4:单元测试友好

java 复制代码
// ❌ 字段注入 ------ 测试需要框架辅助
@Service
public class OrderService {
    @Autowired
    private PaymentService paymentService;
    
    @Autowired
    private InventoryService inventoryService;
}

// 字段注入的测试
@ExtendWith(MockitoExtension.class)  // 必须加这个
class OrderServiceTest {
    @InjectMocks  // 通过反射注入
    private OrderService orderService;
    
    @Mock
    private PaymentService paymentService;
    
    @Mock
    private InventoryService inventoryService;
    
    // @InjectMocks 有时会神秘失败,需要调试半天
}
// ✅ 构造器注入 ------ 测试就是普通 Java 对象
@Service
public class OrderService {
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    
    public OrderService(PaymentService paymentService, InventoryService inventoryService) {
        this.paymentService = paymentService;
        this.inventoryService = inventoryService;
    }
}

// 构造器注入的测试 ------ 无任何注解
class OrderServiceTest {
    private OrderService orderService;
    
    @BeforeEach
    void setUp() {
        PaymentService paymentService = mock(PaymentService.class);
        InventoryService inventoryService = mock(InventoryService.class);
        orderService = new OrderService(paymentService, inventoryService); // 直接 new!
    }
}

详解 5:与 Spring 解耦

java 复制代码
// ✅ 构造器注入 ------ 不依赖任何 Spring 注解
public class PaymentProcessor {
    private final PaymentGateway gateway;
    
    public PaymentProcessor(PaymentGateway gateway) {
        this.gateway = gateway;
    }
}

// 在 Spring 中注册
@Configuration
public class AppConfig {
    @Bean
    public PaymentGateway gateway() {
        return new StripeGateway();
    }
    
    @Bean
    public PaymentProcessor processor() {
        return new PaymentProcessor(gateway());
    }
}

// 在非 Spring 环境(Lambda、批处理、单元测试)中同样可用
PaymentGateway mockGateway = mock(PaymentGateway.class);
PaymentProcessor processor = new PaymentProcessor(mockGateway);

详解 6:Lombok 简化构造器注入

java 复制代码
// Lombok 最优雅的写法 ------ @RequiredArgsConstructor
@Service
@RequiredArgsConstructor  // ✅ 为所有 final 字段生成构造器
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    // 不用写任何构造器代码!
}

等同于:

java 复制代码
@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    
    public OrderService(OrderRepository orderRepository, 
                       PaymentService paymentService,
                       InventoryService inventoryService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
        this.inventoryService = inventoryService;
    }
}

⑥ 案例实战

实战 1:从字段注入重构到构造器注入

重构前(字段注入):

java 复制代码
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private EmailService emailService;
    @Autowired
    private AuditLogService auditLogService;
    @Value("${app.max-login-attempts}")
    private int maxLoginAttempts;
    
    public User login(String username, String password) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            auditLogService.log("LOGIN_FAILED", username);
            throw new LoginException("用户不存在");
        }
        if (!passwordEncoder.matches(password, user.getPassword())) {
            auditLogService.log("WRONG_PASSWORD", username);
            throw new LoginException("密码错误");
        }
        return user;
    }
}

重构后(构造器注入 + @Value 用构造器参数):

java 复制代码
@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    private final AuditLogService auditLogService;
    private final int maxLoginAttempts;
    
    public UserService(UserRepository userRepository,
                      EmailService emailService,
                      AuditLogService auditLogService,
                      @Value("${app.max-login-attempts}") int maxLoginAttempts) {
        this.userRepository = userRepository;
        this.emailService = emailService;
        this.auditLogService = auditLogService;
        this.maxLoginAttempts = maxLoginAttempts;
    }
    
    public User login(String username, String password) {
        // 业务逻辑不变...
    }
}

实战 2:条件注入(Spring Boot 4.x+ 新特性)

java 复制代码
// 使用 @ConditionalOnMissingBean 实现条件注入
@Configuration
public class PaymentConfig {
    
    @Bean
    @ConditionalOnMissingBean
    public PaymentGateway paymentGateway() {
        // 如果没有自定义的 PaymentGateway,使用默认实现
        return new StripePaymentGateway();
    }
    
    @Bean
    public PaymentProcessor paymentProcessor(PaymentGateway gateway) {
        return new PaymentProcessor(gateway);
    }
}

实战 3:多实现类注入

java 复制代码
// 定义接口
public interface PaymentService {
    void pay(BigDecimal amount);
}

// 多个实现
@Component
@Primary  // 默认使用
public class AlipayService implements PaymentService {
    public void pay(BigDecimal amount) {
        System.out.println("支付宝支付: " + amount);
    }
}

@Component
public class WechatPayService implements PaymentService {
    public void pay(BigDecimal amount) {
        System.out.println("微信支付: " + amount);
    }
}

// 注入方式 1:用 @Qualifier 指定
@Service
public class OrderService {
    private final PaymentService paymentService;
    
    public OrderService(@Qualifier("wechatPayService") PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

// 注入方式 2:注入所有实现
@Service
public class PaymentRouter {
    private final List<PaymentService> paymentServices; // 自动注入所有实现
    
    public PaymentRouter(List<PaymentService> paymentServices) {
        this.paymentServices = paymentServices;
    }
}

⑦ 避坑 & 最佳实践

❌ 常见坑点

坑 1:字段注入 + final 关键字

java 复制代码
// ❌ 编译不报错但毫无意义
@Autowired
private final SomeService someService; // final 字段必须在构造器中赋值!

解决:要么去掉 final 用字段注入,要么用构造器注入。


坑 2:循环依赖 + 构造器注入导致启动失败 看到 BeanCurrentlyInCreationException 不要慌,这是 Spring 在保护你。去重构代码结构。

消除循环依赖的方法

  1. 使用接口分离(最常见的解法)

  2. 使用 @Lazy 延迟加载(临时方案)

  3. 提取公共逻辑到新类


坑 3:@Autowired(required=false) 的误用

java 复制代码
@Autowired(required = false)
private SomeService someService; // 如果 Bean 不存在就不注入

// ❌ 使用时忘记判空
public void useService() {
    someService.doSomething(); // NullPointerException!
}

坑 4:静态字段注入

java 复制代码
@Component
public class Utils {
    @Autowired
    private static SomeService someService; // ❌ 静态字段不会被注入!
    
    public static void doSomething() {
        someService.call(); // NullPointerException
    }
}

正确做法 :用构造器注入 + @PostConstruct 赋值给静态字段。


✅ 最佳实践

规则 说明
优先构造器注入 Spring 官方推荐,Spring Boot 团队也在用
Lombok + @RequiredArgsConstructor 一行注解替代 N 行构造器代码
字段注入只用于测试 或者遗留代码不改动
多个同类型 Bean 用 @Qualifier 配合 @Primary 设置默认实现
构造器参数过多 = 类职责过重 考虑拆分 Service 类
单元测试优先选构造器注入 减少测试框架依赖,提升可读性

Spring 官方怎么说?

Spring 团队: 自 Spring 4.x 起,构造器注入就是官方推荐的注入方式。单构造器类甚至不需要 @Autowired 注解,Spring 会自动使用它。


⑧ 总结 & 路线图

一句话总结

字段注入图省事,构造器注入图省心。

三句话记住一天

注入方式 一句话总结
字段注入 简洁但有隐患,小项目可接受,大项目要重构
Setter 注入 可选依赖时用,日常开发不常见
构造器注入 官方首选,final 安全、测试友好、解耦干净

下一步去哪?

学习方向 推荐内容
@Autowired 底层原理 了解 AutowiredAnnotationBeanPostProcessor 源码
Java Config 配置注入 @Configuration + @Bean 的注入方式
Qualifier & Primary 多实现注入的精细控制
Spring 4.x + 自动推断 单构造器如何省掉 @Autowired
依赖注入设计模式 学习策略模式 + 工厂模式在注入中的应用

互动题:你项目中用的哪种注入方式?遇到过哪些因注入方式选错导致的 Bug?评论区聊聊 🚀