首页
学习
活动
专区
圈层
工具
发布

为什么 Spring 和 IDEA 都不推荐使用 @Autowired 注解?

@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不是不能用,是字段注入这种方式,太会制造“看起来没问题”的错觉。

代码短了两行,代价是:

依赖藏起来了; 测试更难写了; 对象不能保持不可变; 循环依赖更容易被糊过去; 很多问题从编译期推迟到了运行期。

这种便宜,我一般不太想占。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OybJ66ZP7f_Mit0e_h6p33Aw0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券