构造注入详解:Spring 依赖注入的首选方式

构造注入详解:Spring 依赖注入的首选方式

一、什么是构造注入?

构造注入(Constructor Injection)是 Spring 依赖注入的一种方式,它通过类的构造方法将依赖对象传递给目标类。

java 复制代码
@Service
public class UserService {
    private final UserDao userDao;
    
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}

Spring 在创建 UserService 实例时,会从容器中查找 UserDao 类型的 Bean,作为参数调用这个构造方法。依赖在对象创建的一刻就已经就绪。

二、基本用法

2.1 单个构造方法

如果类只有一个构造方法,Spring 4.3 之后不需要显式加 @Autowired,Spring 会自动使用它进行注入。

java 复制代码
@Service
public class UserService {
    private final UserDao userDao;
    private final EmailService emailService;
    
    public UserService(UserDao userDao, EmailService emailService) {
        this.userDao = userDao;
        this.emailService = emailService;
    }
}

2.2 多个构造方法

如果类有多个构造方法,需要标注 @Autowired 告诉 Spring 使用哪一个。

java 复制代码
@Service
public class UserService {
    private final UserDao userDao;
    private final EmailService emailService;
    
    @Autowired
    public UserService(UserDao userDao, EmailService emailService) {
        this.userDao = userDao;
        this.emailService = emailService;
    }
    
    public UserService(UserDao userDao) {
        this(userDao, null);
    }
}

2.3 配合 Lombok

使用 Lombok 的 @RequiredArgsConstructor 可以自动生成包含所有 final 字段的构造方法,代码更简洁。

java 复制代码
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserDao userDao;
    private final EmailService emailService;
}

编译后等价于手写了带这两个参数的构造方法。

三、构造注入的核心优势

3.1 依赖不可变

构造注入的字段可以用 final 修饰,对象一旦创建,依赖就不可更改。这保证了线程安全和状态一致性。

java 复制代码
private final UserDao userDao;  // 不可变

3.2 依赖完全就绪

对象创建完成后,所有依赖都已经注入完毕。不会出现字段注入那种"对象创建了但依赖还是 null"的中间状态。

java 复制代码
UserService service = new UserService(userDao);  // 创建即就绪
service.getUser(1L);  // 安全,userDao 一定不为 null

3.3 便于单元测试

测试时直接通过构造方法传入 Mock 对象,不需要启动 Spring 容器,也不需要反射工具。

java 复制代码
@Test
void testGetUser() {
    UserDao mockDao = mock(UserDao.class);
    when(mockDao.findById(1L)).thenReturn(new User(1L, "张三"));
    
    UserService service = new UserService(mockDao);  // 直接传入 mock
    User user = service.getUser(1L);
    
    assertEquals("张三", user.getName());
}

3.4 明确依赖关系

构造方法的参数列表直接反映了类的依赖。如果参数过多,说明类承担了过多职责,需要拆分------这是一个早期设计信号。

3.5 避免循环依赖

构造注入无法解决循环依赖。两个类互相通过构造方法注入对方时,Spring 会直接抛出 BeanCurrentlyInCreationException

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; }
}

启动时会报错,因为创建 A 需要 B,创建 B 需要 A,互相等待,无法完成。

这看似是缺点,但实际上是一个设计上的保护。循环依赖通常意味着两个类的职责边界不清晰,构造注入强制暴露这个问题,促使开发者重构。而字段注入和 Setter 注入可以通过三级缓存"绕过"循环依赖,但可能掩盖了设计缺陷。

四、三种注入方式对比

对比维度 构造注入 Setter 注入 字段注入
依赖可变性 不可变(final 可变 可变
空指针风险
单元测试便利性
循环依赖支持 不支持 支持 支持
依赖过多时提示 明显(参数列表长) 不明显 不明显
Spring 官方推荐 ✅ 推荐 ⚠️ 可选 ❌ 不推荐

五、底层原理

构造注入的处理入口是 AbstractAutowireCapableBeanFactory.createBeanInstance()。当 Bean 定义中指定了构造参数,或类只有一个有参构造方法时,Spring 会走 autowireConstructor() 方法。

简化流程:

  1. 获取候选构造方法列表(determineConstructorsFromBeanPostProcessorsgetDeclaredConstructors
  2. 按参数数量排序,优先匹配参数最多的构造方法
  3. 对每个构造方法参数,调用 beanFactory.resolveDependency() 查找匹配的 Bean
  4. 解析出所有参数后,通过反射调用 Constructor.newInstance(args) 创建实例
  5. 如果找不到匹配的 Bean,根据 required 决定是否跳过或抛异常

核心类 ConstructorResolver 负责解析构造方法参数、匹配依赖、处理 @Autowired(required=false) 等逻辑。

六、构造注入的常见问题

参数过多

当一个类的构造方法参数超过 5 个,通常意味着类承担了太多职责,应考虑拆分。构造注入强制暴露这个问题,是一种"设计预警"。

可选依赖

如果某个依赖是可选的,构造注入不太适合,需要配合 ObjectProvider@Nullable

java 复制代码
@Service
public class UserService {
    private final UserDao userDao;
    private final Optional<CacheService> cacheService;
    
    public UserService(UserDao userDao, 
                       ObjectProvider<CacheService> cacheServiceProvider) {
        this.userDao = userDao;
        this.cacheService = Optional.ofNullable(cacheServiceProvider.getIfAvailable());
    }
}

多个同类型 Bean

当容器中有多个同类型的 Bean 时,构造参数需要配合 @Qualifier 指定具体名称。

java 复制代码
@Service
public class OrderService {
    private final PaymentService paymentService;
    
    public OrderService(@Qualifier("aliPayService") PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

七、最佳实践

优先使用构造注入。 Spring 官方从 4.3 开始就推荐构造注入,它能保证依赖完整性和不可变性。

final 修饰所有通过构造注入的字段。 表达依赖的不可变性,也防止意外修改。

依赖过多时拆分职责。 如果一个类的构造方法参数超过 5 个,通常说明它做了太多事情。考虑拆分为多个类,或者提取一个聚合依赖对象。

避免构造注入的循环依赖。 如果出现循环依赖,不要试图用 @Lazy 或改成字段注入来"绕过",而应该分析两个类之间的关系,提取公共依赖或调整职责边界。

配合 Lombok 使用 @RequiredArgsConstructor 让代码保持简洁,同时保留构造注入的所有优点。

八、总结

构造注入是 Spring 依赖注入中最推荐的方式。它通过构造方法将依赖在对象创建时一次性注入,保证了依赖的不可变性和完整性,同时便于单元测试和设计审查。

与字段注入和 Setter 注入相比,构造注入在依赖明确性、测试友好性和线程安全性上都更优。它无法解决循环依赖,但这恰恰是它的一个设计优势------强制暴露不健康的依赖关系。

在 Spring Boot 项目中,结合 Lombok 的 @RequiredArgsConstructor,可以以最简洁的方式实现构造注入,同时保持代码的清晰和健壮。

相关推荐
吃饱了得干活1 小时前
Spring Boot + Redis 分布式锁:一个订单重复提交引发的六次迭代
java·redis·后端
步行cgn1 小时前
依赖注入详解:Spring IoC 的实现方式
后端
平头哥AI2 小时前
Day 21 _ error 是个普通值_errors.New 与 fmt.Errorf 造出来,沿调用栈抛到 main 接住
后端·学习·golang·go
写后端的胖头鱼2 小时前
一文讲懂JVM与调优
jvm·后端·算法·架构·jvm调优
步行cgn2 小时前
Spring Boot 指定数据来源详解
spring boot·后端·python
一个有温度的技术博主2 小时前
深入理解 Spring Boot 自动装配
java·spring boot·后端
IT_陈寒2 小时前
我TM竟然被Java的空指针坑了第三次!
前端·人工智能·后端
小灰灰搞电子3 小时前
Rust suppaftp 库详解:基于 FTP 客户端实战指南
开发语言·后端·rust
Ramble_Naylor3 小时前
枚举与 match:一个值只能是几种情况之一
后端·rust