其实就两个:
- 一个final关键字;
- 一个@RequiredArgsConstructor注解
够了,就这两个,你看看下面的:
@RequiredArgsConstructor
@Service
@Slf4j
public class OrderApplicationService {
private final OrderDomainService orderDomainService;
private final TransactionTemplate transactionTemplate;
}
具体的,我下面会说明。我们需要先看看为啥不推荐使用@Autowired了。
你的IDEA里给一个字段加上@Autowired,会看到一条黄色波浪线。鼠标悬停上去,提示信息是:Field injection is not recommended。
这不是IDEA的问题。IntelliJ这条检查规则,依据的是Spring官方说明。Spring团队在官方文档里明确写了,推荐使用构造器注入。
那@Autowired字段注入有啥问题?
先看一段典型的字段注入代码:
@Service
public class OrderService {
@Autowired
private UserRepository userRepository;
@Autowired
private OrderRepository orderRepository;
}
这种写法在老项目里到处都是。每个依赖一行注解加一行声明,看着还行。但是问题在两个地方。
依赖没法用final修饰
字段注入的工作原理是:Spring先创建对象实例,再通过反射把依赖设置到字段上。赋值发生在对象构造之后,所以这些字段不能声明为final。
不能用final,意味着依赖可以在运行时被修改:
@Service
public class OrderService {
@Autowired
private UserRepository userRepository;
public void someMethod() {
// 编译不报错,运行时也不会立刻出问题
this.userRepository = null;
}
}
下次调用userRepository的方法时,直接空指针异常。
换成构造器注入,字段可以声明为final:
@Service
public class OrderService {
private final UserRepository userRepository;
public OrderService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void someMethod() {
// 编译直接报错:Cannot assign a value to final variable
this.userRepository = null;
}
}
依赖一旦注入就不可更改,编译器直接拦住。
脱离容器单元测试不好搞
字段注入的类,所有依赖都通过Spring容器的反射机制设置。如果不启动容器,直接new一个对象:
// 所有依赖都是null
OrderService service = new OrderService();
想让测试跑起来,要么用@SpringBootTest启动整个容器,要么用反射工具手动设值。
构造器注入的类可以直接实例化:
OrderService service = new OrderService(
mockUserRepository,
mockOrderRepository,
mockNotificationService
);
不依赖框架,不启动容器,直接传Mock对象进去测业务逻辑。
我们来看Spring官方是怎么说的
Spring Framework官方文档里有一段专门讨论这个问题,原文如下:
The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state.
这段话出自Spring Framework 6.0的官方文档,是框架团队的明确立场,翻译一下关键信息:
- Spring团队主张使用构造器注入
- 构造器注入让组件成为不可变对象
- 确保必需的依赖不会为null
- 组件返回给调用方时,一定处于完全初始化的状态
Spring从4.3版本开始还做了一个改动:如果一个类只有一个构造器,@Autowired注解可以省略,Spring会自动用这个构造器进行注入。这个设计本身就是在引导开发者往构造器注入迁移。
另外,Spring官方文档里也提到了这一点:大量的构造器参数是一个代码坏味道,说明这个类承担了太多职责,应该重构。
构造器注入的写法
手写构造器
最基础的方式,不依赖额外工具:
@Service
public class OpenAppService {
private final OpenAppMapper openAppMapper;
public OpenAppService(OpenAppMapper openAppMapper) {
this.openAppMapper = openAppMapper;
}
}
类里只有一个构造器,Spring自动识别并注入,不需要加@Autowired。字段用final修饰,保证不可变。
这种写法在依赖少的时候足够用。一旦依赖超过三四个,手写构造器和赋值语句会显得比较啰嗦,「代码颜值也不高」。
该@RequiredArgsConstructor + final出场了
目前主流项目的标准做法:
@RequiredArgsConstructor
@Service
@Slf4j
public class OrderApplicationService {
private final OrderDomainService orderDomainService;
private final TransactionTemplate transactionTemplate;
}
@RequiredArgsConstructor是Lombok提供的注解,编译期自动生成一个包含所有final字段的构造器。效果和手写完全一样,但省掉了模板代码。
新加一个依赖,只需要加一行private final字段声明。Lombok自动把它加到构造器参数里,不需要手动维护构造器。
整个项目统一这种写法,代码风格干净一致。
对比速查表
| 维度 | @Autowired字段注入 | 构造器注入 |
|---|---|---|
| 不可变性 | 不支持final | 支持final |
| 单元测试 | 需要反射或启动容器 | 直接new,传Mock对象 |
| 依赖可见性 | 分散在各个字段上 | 集中在构造器参数 |
| 空指针风险 | 运行时可能为null | 容器启动时校验 |
| Spring官方态度 | 不推荐 | 明确推荐 |
小结
适当的约束比完全的自由更有价值。 final是约束,构造器参数太长时的不适感也是约束。这些约束会推着你在更早的阶段发现设计问题。
如果你在维护一个老项目,里面到处都是@Autowired字段注入,不用急着一次性改完。新代码统一用构造器注入,老代码改动时顺手切过来。
希望这篇内容可以帮到你。