java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring依赖注入方式

Spring依赖注入的三种方式

作者:yoothey

这篇文章主要介绍了Spring依赖注入三种方式:字段注入、Setter注入、构造器注入,字段注入简洁但有隐患,构造注入官方推荐,确保依赖不可变、单元测试友好,掌握三种注入方式,重构代码更安心,需要的朋友可以参考下

一、导读卡片

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

二、背景与目标

为什么需要依赖注入?

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

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

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

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

学完本文,你能够:

  • 掌握三种注入方式的写法与适用场景
  • 理解为什么构造器注入是官方推荐方案
  • 用代码证明字段注入有哪些潜在风险
  • 在 Code Review 中自信地指出不合理注入方式

三、概念与原理

三种注入方式概览

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

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

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

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

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

@Service
public class OrderService {
    private OrderRepository orderRepository;
    
    @Autowired
    public void setOrderRepository(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
}

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

构造器注入:官方推荐

@Service
public class OrderService {
    private final OrderRepository orderRepository; // final!
    
    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
}

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

底层原理:Spring 如何注入?

Spring 通过 AutowiredAnnotationBeanPostProcessor 这个 BeanPostProcessor 处理 @Autowired

// 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 字段)

// ❌ 字段注入 —— 依赖可变,可被意外修改
@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:循环依赖暴露

// 两个类互相依赖
@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:空值安全

// ✅ 构造器注入 + 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!
    }
}

实际后果

详解 4:单元测试友好

// ❌ 字段注入 —— 测试需要框架辅助
@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 解耦

// ✅ 构造器注入 —— 不依赖任何 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 简化构造器注入

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

等同于:

@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:从字段注入重构到构造器注入

重构前(字段注入):

@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 用构造器参数):

@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+ 新特性)

// 使用 @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:多实现类注入

// 定义接口
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 关键字

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

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

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

消除循环依赖的方法

  1. 使用接口分离(最常见的解法)
  2. 使用 @Lazy 延迟加载(临时方案)
  3. 提取公共逻辑到新类

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

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

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

坑 4:静态字段注入

@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?评论区聊聊

以上就是Spring依赖注入的三种方式的详细内容,更多关于Spring依赖注入方式的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文