别再乱用 @Autowired!Spring官方推荐的构造函数注入详解

164 阅读7分钟

1. 前言

在我们日常开发SpringBoot项目中,依赖注入(Dependency Injection, DI)是核心机制之一,最常见的写法有两种:

字段注入:在类的属性上直接使用 @Autowired 注解
构造函数注入 :通过构造函数参数注入依赖

相信不少小伙伴都喜欢在字段上直接使用@Autowired,因为字段注入用起来更简洁,但是Spring官方却明确推荐优先使用构造函数注入!这两种方式有何本质区别?为什么官方有明确的推荐?

本文博主将从代码对比、构造函数注入优点、实际单元测试场景 三个维度,带小伙伴们彻底搞懂其中的门道
在这里插入图片描述


2. 两种注入方式对比

假设我们有一个UserService,它依赖于UserMapper(数据访问层),我们需要在UserService中注入UserMapper

2.1 字段注入(@Autowired 在字段上)

这是很多小伙伴喜欢的方式,写法相对简洁直接:

@Service
public class UserService {

    // 直接在字段上使用@Autowired注入
    @Autowired
    private UserMapper userMapper;

    // 业务方法
    public String getUser(Long id) {
        return userMapper.findUserById(id);
    }
}

AI写代码java
运行
123456789101112

2.2 构造函数注入(官方推荐)

通过类的构造方法注入依赖,无需在字段上添加注解:

@Service
public class UserService {

    // 依赖被声明为final,确保不可变
    private final UserMapper userMapper;

    // 通过构造函数注入
    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    // 业务方法
    public String getUser(Long id) {
        return userMapper.findUserById(id);
    }
}

AI写代码java
运行
12345678910111213141516

使用Lombok 简化写法

如果你的项目中有使用了Lombok,那么构造函数注入的方式可以大大简化我们的写法,因为Lombok@AllArgsConstructor注解会自动为类中的所有 final 或非 static 字段生成一个全参数构造函数

在这里插入图片描述

为了演示 :我们假设还有一个订单Mapper也需要注入

@Service
@AllArgsConstructor //Lombok注解
public class UserService {

    // 依赖被声明为final,确保不可变
    private final UserMapper userMapper;
    private final OrderMapper orderMapper;
	
	/** 
	* 无需手动编写构造函数,Lombok会在编译时生成:
    public UserService(UserMapper userMapper, OrderMapper orderMapper) {
       this.userMapper = userMapper;
       this.orderMapper = orderMapper;
     }
	**/
	
    // 业务方法
    public String getUser(Long id) {
        return userMapper.findUserById(id);
    }
}

AI写代码java
运行
123456789101112131415161718192021

通过Lombok的注解大家会发现写法也简洁了很多!


3. 构造函数注入方式的优点

3.1 依赖不可变

通过上面的例子我们可以看出,使用构造函数注入可以声明把依赖声明为final

private final UserMapper userMapper;

AI写代码java
运行
1

使用了final,字段在对象创建时必须初始化,且初始化后无法修改,这就表示了:

  • 依赖不会被意外篡改(线程安全
  • 强制依赖在对象创建时就必须注入(避免后续使用时为null

@Autowired字段注入无法使用final(因为字段注入是在对象创建后通过反射赋值的),依赖可能被中途修改,存在安全隐患

3.2 依赖明确,避免空指针

还是上述示例代码

public UserService(UserMapper userMapper, OrderMapper orderMapper) {
       this.userMapper = userMapper;
       this.orderMapper = orderMapper;
     }

AI写代码java
运行
1234

小伙伴们会发现构造函数注入时,Spring容器会在创建UserService对象时,通过构造方法传入所有依赖(UserMapper、OrderMapper)。
如果依赖缺失(比如UserMapper未被 Spring 管理时),容器启动时就会报错,那么在项目启动阶段就能发现问题。

而字段注入是 “隐式” 的:如果依赖缺失,Spring不会在启动时报错,而只有在调用UserMapper时才抛出NullPointerException,项目运行时才会出现异常,增加了我们调试难度

在这里插入图片描述

3.3 更利于测试

使用字段注入依赖于 Spring 容器,而单元测试的核心原则是 “隔离依赖”,但字段注入严重依赖 Spring 容器,导致测试困难

3.4 避免循环依赖隐患

在构造阶段就能发现依赖关系异常,而不是运行时才出现异常。

如果 A 依赖 B,B 又依赖 A(循环依赖):

构造函数注入时Spring 容器启动阶段就会直接抛出BeanCurrentlyInCreationException,强制开发者解决循环依赖(比如通过拆分服务、引入中间层等),从根源上优化代码设计

字段注入时Spring 会通过 “三级缓存” 暂时解决循环依赖,但仅仅是一种的解决的妥协,本质上是代码设计问题

3.5 总结对比

两种注入方式的对比

特性构造函数注入字段注入(@Autowired)
依赖声明显式(构造方法参数)隐式(类内部字段)
依赖不可变支持(final修饰)不支持(无法用final)
依赖缺失检测启动时检测(快速失败)运行时检测(隐藏风险)
单元测试友好性高(直接传参,无需容器)低(需反射或Spring容器)
循环依赖处理启动时报错(强制解决设计问题)允许存在(依赖容器妥协)
与Spring耦合度低(无注解也可工作)高(必须依赖@Autowired)
代码可读性高(依赖一目了然)低(需通读字段)

4. 两种注入方式单元测试对比

上述讲解了两种注入方式的区别,我们用一个单元测试的示例来进行以下演示

4.1 字段注入的测试问题

假如我们用 字段注入,代码是这样的:

@Service
public class UserService {
    @Autowired
    private OrderService orderService;

    public void createUser() {
        orderService.createOrder();
        System.out.println("用户创建成功");
    }
}

AI写代码java
运行
12345678910

测试代码:

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

public class UserServiceFieldInjectionTest {

    @Test
    void testCreateUser() {
        UserService userService = new UserService();

        // 由于是 private 字段注入,无法直接传入 mock
        // 只能用反射去强行设置
        OrderService mockOrderService = Mockito.mock(OrderService.class);

        try {
            java.lang.reflect.Field field = UserService.class.getDeclaredField("orderService");
            field.setAccessible(true);
            field.set(userService, mockOrderService);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        userService.createUser();
        Mockito.verify(mockOrderService).createOrder();
    }
}

AI写代码java
运行
12345678910111213141516171819202122232425

问题:
必须使用 反射 修改私有字段,测试代码繁琐、难维护
如果字段名修改了,测试就会失败

4.2 构造函数注入的测试优势

换成 构造函数注入 的写法:

@Service
public class UserService {

    private final OrderService orderService;

    // 构造函数注入
    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }

    public void createUser() {
        orderService.createOrder();
        System.out.println("用户创建成功");
    }
}

AI写代码java
运行
123456789101112131415

测试代码:

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

public class UserServiceConstructorInjectionTest {

    @Test
    void testCreateUser() {
        // ✅ 直接通过构造函数传入 mock
        OrderService mockOrderService = Mockito.mock(OrderService.class);
        UserService userService = new UserService(mockOrderService);
        
        userService.createUser();
        Mockito.verify(mockOrderService).createOrder();
    }
}

AI写代码java
运行
123456789101112131415

优势:
不需要反射,测试代码简洁。
依赖关系在构造函数中显式体现。
如果 UserService 有多个依赖,一眼就能看出它需要什么。


5. 题外话:字段注入真的就无用武之地?

并非所有场景都必须严格禁止字段注入。在一些 非核心业务类 (如配置类、工具类)中,若依赖简单且无需频繁测试,博主也推荐字段注入,例如:

@Configuration
public class AppConfig {
    @Autowired
    private DataSource dataSource; // 简单配置类,偶尔使用字段注入也可接受
    // ...
}

AI写代码java
运行
123456

但核心业务逻辑类(Service、Mapper等),博主还是强烈建议使用构造函数注入!


6. 结语

本文详细介绍了Spring依赖注入的两种方式,从代码对比、构造函数注入优点、实际单元测试场景, 并给小伙伴们两种注入方式的使用建议,希望小伙伴们通过本文能测底理解Spring官方为什么推荐的构造函数注入。

总之下次写依赖注入时,不妨多敲几行构造函数 —— 未来的你会感谢这个决定。