之前我们项目中用到的依赖注入方式是通过@Autowired 或者@Resource来实现依赖注入。下面是通过lombok提供的@RequiredArgsConstructor实现依赖注入:
以下两种代码的区别
– 代码片段1
@Service
@Slf4j
@RequiredArgsConstructor
public class UniversalViewService {
private final StringRedisTemplate redis;
}
– 代码片段2
@Service
@Slf4j
public class UniversalViewService {
@Autowired
private StringRedisTemplate redis;
}
核心区别对比
| 特性 | 写法一 (构造函数注入 + Lombok) | 写法二 (字段注入) |
|---|---|---|
| 工作原理 | Lombok 在编译时生成一个包含所有 final 字段的构造函数。Spring 在创建 Bean 时调用此构造函数来注入依赖。 | Spring 在创建完 Bean 实例后,通过反射机制直接为标记了 @Autowired 的字段赋值。 |
| 不可变性 | 支持。字段被声明为 final,必须在对象创建时初始化,之后不可更改。更安全,线程友好。 | 不支持。字段不是 final 的,理论上可以在任何时候被修改(尽管通常不会这么做)。 |
| 代码简洁性 | 非常简洁。无需编写构造函数,只需一个注解和一个 final 关键字。 | 简洁。只需一个注解。 |
| 明确定义 | 明确。清晰地声明了“这些依赖是我正常工作所必需的”。 | 隐含。依赖关系被隐藏了,无法一眼看出哪些是必需的。 |
| 测试友好度 | 更容易测试。在单元测试中,你可以通过调用构造函数轻松传入模拟(Mock)依赖。 | 更困难。你必须使用反射(如 ReflectionTestUtils)来注入模拟依赖,或者依赖 Spring 的测试上下文。 |
| 循环依赖 | 容易暴露问题。构造函数注入能更早地发现循环依赖,迫使你重构设计。 | 隐藏问题。Spring 通过三级缓存等机制可以解决部分循环依赖,可能掩盖了糟糕的设计。 |
| Spring 官方推荐 | 推荐。这是 Spring 团队自 4.x 版本以来推荐的方式。 | 不推荐。Spring 官方文档已不推荐使用字段注入。 |
| 空指针安全 | 更安全。对象被创建时,所有依赖都已就绪,不会出现依赖为 null 的情况。 | 有风险。如果你在类的内部方法(如 @PostConstruct 方法)中使用了依赖,而 Spring 尚未注入,则可能引发 NPE。 |
| 依赖过多时 | 构造函数参数会变多,这是一个信号,提醒你这个类可能职责过重(违反单一职责原则),需要重构。 | 没有明显的信号。类可以轻易地注入很多依赖,而无法直观地意识到问题。 |
详细解释与最佳实践
为什么写法一更好?
-
不可变性 (Immutability):
final关键字保证了依赖项在对象生命周期内不会被意外改变或重新赋值,这使得对象的状态更加稳定和可预测,尤其是在并发环境中。 -
完全初始化的对象: 当
UniversalViewService的实例被创建后,你可以百分之百确定redis字段已经被正确赋值。你不必担心在调用@PostConstruct方法或其他初始化方法时遇到NullPointerException。 -
更好的测试体验: 假设你要对
UniversalViewService进行单元测试,而不启动整个 Spring 容器。- 写法一:你可以直接
new UniversalViewService(mockRedis),非常简单。 - 写法二:你必须使用
ReflectionTestUtils.setField(service, "redis", mockRedis),这更繁琐且容易出错。
- 写法一:你可以直接
-
遵循最佳实践: 构造函数注入明确地定义了类所需的依赖合同。如果一个类有 5 个依赖,它的构造函数就有 5 个参数,这清楚地表明了类的复杂度,并促使开发者思考是否违反了单一职责原则。
字段注入的缺点:
字段注入的主要问题是它隐藏了依赖关系,并且破坏了对象的封装性。你无法通过公共接口(构造函数或 Setter 方法)来了解创建一个对象需要什么,而是必须深入到类的内部实现中去查看。
结论
你应该优先选择写法一(@RequiredArgsConstructor + final 字段)。
这是一种现代、简洁、安全且符合最佳实践的依赖注入方式。它结合了 Lombok 的便利性和 Spring 构造函数注入的所有优点。
写法二(字段注入)虽然看起来更简单,但因其诸多缺点,在现代 Spring 应用开发中已被视为一种反模式(anti-pattern),应避免在新项目中使用。
核心逻辑:分工明确
- Spring 的职责(实现注入): Spring 容器负责创建 Bean 实例、管理 Bean 的生命周期,并根据配置(如
@Autowired,构造函数等)将所需的依赖项(其他 Bean)“注入”到目标 Bean 中。这是依赖注入的真正实现者。 - Lombok 的职责(生成代码): Lombok 是一个代码生成库,它在编译时运行。
@RequiredArgsConstructor的职责是:为一个类的所有final字段或者标记了@NonNull的字段自动生成一个公共构造函数。它不关心这些依赖从哪里来,它只负责生成接收这些依赖的构造函数的代码。
当两者结合时,就实现了简洁且可靠的依赖注入。
具体工作流程(以构造函数注入为例)
假设我们有一个 MyService 类,它依赖于 MyRepository。
没有 Lombok 的传统写法:
@Service
public class MyService {
private final MyRepository myRepository;
// 程序员需要手动编写这个构造函数
@Autowired // Spring 4.3 以后,如果只有一个构造函数,可以省略 @Autowired
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
public void doSomething() {
myRepository.findData();
}
}
使用 @RequiredArgsConstructor 的写法:
@Service
@RequiredArgsConstructor // Lombok 注解
public class MyService {
private final MyRepository myRepository;
public void doSomething() {
myRepository.findData();
}
}
发生了什么?
- 代码编写阶段:你只在
MyService中声明了一个final的myRepository字段,并添加了@RequiredArgsConstructor注解。 - 编译阶段(Lombok 介入): Lombok 在 Java 编译器编译源码之前,会读取这个注解,并自动生成以下代码:
public class MyService { private final MyRepository myRepository; // Lombok 自动生成的构造函数 public MyService(MyRepository myRepository) { this.myRepository = myRepository; } // ... 其他方法 } - 运行时(Spring 介入): Spring 容器启动时,要创建
MyService这个 Bean。它发现MyService有一个构造函数需要一个MyRepository类型的参数。 - 依赖查找与注入: Spring 会在自己的容器中查找类型为
MyRepository的 Bean(比如MyRepository接口的一个实现类MyRepositoryImpl,通常用@Repository注解标记),找到后,Spring 会调用 Lombok 生成的那个构造函数,并将MyRepository的实例作为参数传入,从而完成依赖注入。
5万+

被折叠的 条评论
为什么被折叠?



