为什么 Spring 更推荐构造器注入,而不是字段 `@Autowired`

为什么 Spring 更推荐构造器注入,而不是字段 @Autowired

1. 两种写法

字段注入通常写成:

@Service
public class UserService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void saveUser() {
        redisTemplate.opsForValue().set("name", "Tom");
    }
}

构造器注入写成:

@Service
public class UserService {
    private final RedisTemplate<String, Object> redisTemplate;

    public UserService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void saveUser() {
        redisTemplate.opsForValue().set("name", "Tom");
    }
}

这里需要准确区分:并不是“构造器注入不属于 @Autowired”,而是当 Spring Bean 只有一个构造器时,Spring 可以自动使用该构造器完成依赖注入,因此通常不必再显式标注 @Autowired

2. 字段注入的执行过程

使用字段注入时,Spring 大致会执行以下过程:

调用无参构造器创建 UserService
→ 此时 redisTemplate 为 null
→ Spring 发现字段上的 @Autowired
→ 从容器中查找 RedisTemplate Bean
→ 通过反射给字段赋值

因此,对象刚被创建出来时并不完整,它需要等待 Spring 后续填充字段。如果对象不是由 Spring 创建,而是开发者自己执行:

UserService userService = new UserService();

那么 Spring 不会参与这个对象的创建,也不会处理字段上的 @Autowired,此时 redisTemplate 仍然是 null。调用业务方法时可能出现:

java.lang.NullPointerException

3. 构造器注入的执行过程

使用构造器注入时,Spring 必须先找到构造器需要的依赖,再创建业务对象:

准备创建 UserService
→ 分析构造器参数
→ 从容器中找到 RedisTemplate Bean
→ 调用 new UserService(redisTemplate)
→ 得到依赖完整的 UserService

对象一旦创建完成,必需依赖就已经存在,不会先产生一个依赖为空的“半初始化对象”。

4. 推荐构造器注入的主要原因

4.1 保证必需依赖在创建时就存在

字段注入允许创建一个依赖尚未赋值的对象:

@Service
public class UserService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
}

而构造器注入强制调用者提供依赖:

public UserService(RedisTemplate<String, Object> redisTemplate) {
    this.redisTemplate = redisTemplate;
}

此时不能直接执行:

new UserService();

必须提供 RedisTemplate

new UserService(redisTemplate);

这使无效对象更难被创建。

4.2 可以把依赖声明为 final

构造器注入允许使用:

private final RedisTemplate<String, Object> redisTemplate;

final 表示该引用必须在构造阶段完成初始化,并且之后不能被重新指向其他对象。这能更清楚地表达:RedisTemplateUserService 的固定依赖,而不是运行过程中可以随意替换的普通字段。

字段注入通常不能把字段直接声明为由 Spring 后续赋值的普通 final 字段,因为 final 字段必须在构造阶段完成初始化。

4.3 依赖关系更加清晰

看到下面的构造器,就能直接知道 UserService 创建时依赖哪些对象:

public UserService(
        UserRepository userRepository,
        RedisTemplate<String, Object> redisTemplate) {
    this.userRepository = userRepository;
    this.redisTemplate = redisTemplate;
}

字段注入则把依赖分散在类的不同位置:

@Autowired
private UserRepository userRepository;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

当一个类的依赖很多时,构造器会变得很长。这不完全是构造器注入的缺点,反而是在提醒开发者:这个类可能承担了过多职责,需要考虑拆分。

4.4 更方便进行单元测试

使用构造器注入时,可以在不启动 Spring 容器的情况下直接创建测试对象:

RedisTemplate<String, Object> mockTemplate = mock(RedisTemplate.class);
UserService userService = new UserService(mockTemplate);

测试代码能够明确提供模拟依赖。

字段注入没有公开的依赖入口:

UserService userService = new UserService();

此时 redisTemplate 是私有字段,测试往往需要启动 Spring 上下文、使用反射修改字段,或者使用额外的测试工具完成注入,测试成本更高。

4.5 减少对 Spring 容器的隐式依赖

字段注入写法只有在 Spring 处理对象后才正常工作:

@Autowired
private RedisTemplate<String, Object> redisTemplate;

单看 Java 类本身,并不能保证字段已经赋值。构造器注入则符合普通 Java 的对象创建规则,即使离开 Spring,也可以正常创建和测试对象:

UserService userService = new UserService(redisTemplate);

这使业务类与 Spring 的耦合更弱,类本身也更容易复用。

4.6 更容易发现循环依赖

假设 AService 依赖 BService,而 BService 又依赖 AService

@Service
public class AService {
    public AService(BService bService) {
    }
}

@Service
public class BService {
    public BService(AService aService) {
    }
}

构造器注入会直接暴露循环关系,因为创建 AService 前需要 BService,创建 BService 前又需要 AService。这通常说明类之间的职责划分存在问题。

字段注入可能让循环关系在代码中不够明显。虽然 Spring 在某些情况下曾能通过提前暴露对象处理部分循环依赖,但依赖这种机制会增加设计复杂度。尽早发现并消除循环依赖通常更合理。

5. 为什么单个构造器可以省略 @Autowired

下面两种写法在只有一个构造器时通常效果相同:

@Service
public class UserService {
    private final RedisTemplate<String, Object> redisTemplate;

    @Autowired
    public UserService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}
@Service
public class UserService {
    private final RedisTemplate<String, Object> redisTemplate;

    public UserService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}

Spring 发现这个类只有一个构造器后,会默认把它作为依赖注入入口,因此 @Autowired 可以省略。

如果类中存在多个构造器,Spring需要判断使用哪一个。这时可以显式标注需要用于注入的构造器,或者重新设计构造方式,避免产生歧义。

6. 多个同类型 Bean 时如何处理

构造器注入并不会消除同类型 Bean 的歧义。例如容器中存在两个 RedisTemplate Bean 时,可以使用 @Qualifier 指定名称:

@Service
public class UserService {
    private final RedisTemplate<String, Object> redisTemplate;

    public UserService(
            @Qualifier("redisTemplate")
            RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}

也可以在某个 Bean 上使用 @Primary,将其设为默认候选对象。

7. 可选依赖怎么办

构造器注入更适合“没有它就无法正常工作”的必需依赖。如果某个依赖确实是可选的,可以使用 OptionalObjectProvider,或者在合适场景下使用 Setter 注入。例如:

@Service
public class ReportService {
    private final ObjectProvider<CacheService> cacheServiceProvider;

    public ReportService(ObjectProvider<CacheService> cacheServiceProvider) {
        this.cacheServiceProvider = cacheServiceProvider;
    }
}

使用时再判断容器中是否存在对应 Bean:

CacheService cacheService = cacheServiceProvider.getIfAvailable();

不应为了省事,把所有依赖都改成可空字段,否则会把配置错误推迟到业务运行阶段。

8. Lombok 简化写法

当项目使用 Lombok 时,可以使用 @RequiredArgsConstructor 自动生成包含所有 final 字段的构造器:

@Service
@RequiredArgsConstructor
public class UserService {
    private final RedisTemplate<String, Object> redisTemplate;

    public void saveUser() {
        redisTemplate.opsForValue().set("name", "Tom");
    }
}

它大致等价于:

public UserService(RedisTemplate<String, Object> redisTemplate) {
    this.redisTemplate = redisTemplate;
}

不过阅读代码时仍应理解真正完成注入的是生成后的构造器,而不是 Lombok 注解本身。

9. 三种注入方式对比

对比项字段注入构造器注入Setter 注入
必需依赖是否在创建时提供
是否可以使用 final通常不可以可以不可以
依赖是否清晰一般清晰较清晰
单元测试是否方便一般方便较方便
适合场景简单示例、旧代码必需依赖,推荐默认使用可选或可替换依赖

10. 总结

Spring 更推荐构造器注入,并不是因为字段上的 @Autowired 无法工作,而是因为构造器注入能更好地保证对象完整性:

字段注入:
先创建对象
→ 对象中的依赖暂时为 null
→ Spring 再通过反射填充字段

构造器注入:
先找到全部必需依赖
→ 再创建完整对象

可以用一句话记住:

必需依赖使用构造器注入,可选依赖根据实际情况使用 Setter、OptionalObjectProvider

因此,推荐写法是:

@Service
public class UserService {
    private final RedisTemplate<String, Object> redisTemplate;

    public UserService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}

而不是默认使用:

@Autowired
private RedisTemplate<String, Object> redisTemplate;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值