@Autowired这玩意,线上不一定先炸,单测里先给你一巴掌。
最常见的场景就是你手写了个new OrderService(),代码一跑,NullPointerException来得非常直接。业务没多复杂,空指针却打在了userService、payClient、couponMapper这种字段上。第一眼看像是你代码没写完,实际上问题更早:依赖是偷偷塞进去的,类自己没把话说清楚。
这也是为什么 Spring 自己不太推荐字段注入,IDEA 也总在旁边黄线提示你。不是它俩闲得慌,是这种写法看着省事,后面排查、测试、重构,都会变得别扭。这个判断我平时是认的。
先看一段很多项目里都见过的代码:
@Service
publicclass OrderService {
@Autowired
private UserService userService;
@Autowired
private StockService stockService;
@Autowired
private PaymentService paymentService;
public void createOrder(Long userId, Long skuId) {
User user = userService.load(userId);
stockService.lock(skuId);
paymentService.pay(user.getAccountId(), skuId);
}
}
这段代码表面挺顺。问题是,OrderService到底依赖什么,只有你点进类里一行一行扫字段才知道。依赖全藏在成员变量里,构造方法空着,类的“使用说明书”基本等于没有。
这种代码有几个坑,不是理论上的,是一改需求就能踩到的。
第一个坑,依赖不透明。
你把这个类交给别人,别人只看到一个无参构造器,还以为这是个随手就能 new 出来的普通对象。结果一运行,三个字段全是空。字段注入最大的问题,不是注入本身,而是它把依赖关系藏起来了。类明明离不开UserService、StockService、PaymentService,代码却装得像谁都不需要。
第二个坑,单测难写。
我写单测时,最烦这种类。你想 mock 一个依赖,要么起 Spring 容器,要么反射塞值,要么上@InjectMocks之类的曲线救国。测个业务方法,最后把测试环境搞得比生产还热闹,没必要。
比如这种测试,味道就不对了:
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@InjectMocks
private OrderService orderService;
@Mock
private UserService userService;
@Mock
private StockService stockService;
@Mock
private PaymentService paymentService;
@Test
void should_create_order() {
when(userService.load(1L)).thenReturn(new User(1L, 2001L));
orderService.createOrder(1L, 10001L);
verify(stockService).lock(10001L);
verify(paymentService).pay(2001L, 10001L);
}
}
这还算好的。项目里一旦掺上@Resource、@Autowired、@Lazy,测试代码会越来越像修电路,不像在验证业务。
第三个坑,不能做成不可变对象。
真正稳一点的服务类,我更愿意让依赖在构造阶段一次性传完,后面不允许改。字段注入做不到这件事,因为它要求字段可变,final基本就别想了。可依赖这种东西,本来就不该半路换掉。一个 Service 被造出来时该依赖谁,应该当场说清楚。
所以我更习惯这样写:
@Service
publicclass OrderService {
privatefinal UserService userService;
privatefinal StockService stockService;
privatefinal PaymentService paymentService;
public OrderService(UserService userService,
StockService stockService,
PaymentService paymentService) {
this.userService = userService;
this.stockService = stockService;
this.paymentService = paymentService;
}
public void createOrder(Long userId, Long skuId) {
User user = userService.load(userId);
stockService.lock(skuId);
paymentService.pay(user.getAccountId(), skuId);
}
}
这段代码有个很实在的好处:你不需要读注解,也知道这个类活着要靠谁。谁是必须依赖,构造器里一眼就看到了。少一个参数,编译期就过不去,比运行时空指针靠谱多了。
再往前走一步,Spring Boot 里其实连@Autowired都经常可以省掉。只有一个构造器时,Spring 会自动注入:
@Service
publicclass RefundService {
privatefinal OrderRepository orderRepository;
privatefinal PaymentGateway paymentGateway;
public RefundService(OrderRepository orderRepository,
PaymentGateway paymentGateway) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
}
public void refund(Long orderId) {
Order order = orderRepository.findRequired(orderId);
paymentGateway.refund(order.getPayNo(), order.getAmount());
}
}
这就是 IDEA 老提示你“Field injection is not recommended”的原因。它不是在教条,它是在提醒你:这类依赖应该显式声明,不该偷偷摸摸塞字段里。
还有个问题,很多人平时没感觉,就是字段注入更容易把循环依赖养出来。
两个 Service 互相@Autowired,项目照样能跑起来,于是大家以为没问题。等哪天你想改成final、想抽构造器、想拆模块,才发现两边早缠死了。构造器注入反而更早把问题暴露出来,容器起不来就对了,这种错就该早点报,不该留到线上。
说到底,@Autowired不是不能用,是字段注入这种方式,太会制造“看起来没问题”的错觉。
代码短了两行,代价是:
依赖藏起来了; 测试更难写了; 对象不能保持不可变; 循环依赖更容易被糊过去; 很多问题从编译期推迟到了运行期。
这种便宜,我一般不太想占。