为什么Spring不推荐@Autowired用于字段注入?

简介: 作为Java程序员,Spring框架在日常开发中使用频繁,其依赖注入机制带来了极大的便利。然而,尽管@Autowired注解简化了依赖注入,Spring官方却不推荐在字段上使用它。本文将探讨字段注入的现状及其存在的问题,如难以进行单元测试、违反单一职责原则及易引发NPE等,并介绍为何Spring推荐构造器注入,包括增强代码可读性和维护性、方便单元测试以及避免NPE等问题。通过示例代码展示如何将字段注入重构为构造器注入,提高代码质量。

作为Java程序员,Spring绝对是我们日常开发中使用频次最高的框架之一。它灵活的依赖注入机制为我们开发高可维护性的代码提供了极大的便利。然而,尽管@Autowired注解让依赖注入变得如此简单,Spring官方却明确不推荐在字段上使用它进行注入。那么,为什么会这样?今天,我们就来深入探讨一下这个问题。

@Autowired字段注入的现状

@AutowiredSpring框架中非常常见的注解,用于自动注入依赖。当我们在类的字段上标注这个注解时,Spring会自动将所需的依赖注入进来。这种方式的确简单明了,代码也相对简洁:

java

代码解读

复制代码

@Component
public class MyService {
    @Autowired
    private UserRepository userRepository;

    public void performOperation() {
        // 使用 userRepository 执行一些操作
    }
}

这段代码看起来非常干净和直接,我们只需要在字段上加上@Autowired注解,Spring就会帮我们处理依赖注入。然而,从Spring 4.0开始,官方就不推荐这种字段注入方式了。那么问题出在哪里?

字段注入的风险与缺点
  1. 难以进行单元测试
    字段注入的一个主要问题是它在单元测试中并不友好。在测试环境中,如果你不使用Spring`上下文,那么你需要手动通过反射来注入依赖,这使得测试代码变得复杂且脆弱。例如:

java

  1. 代码解读
  2. 复制代码
public class MyServiceTest {
    private MyService myService;

    @BeforeEach
    void setUp() {
        myService = new MyService();
        UserRepository userRepository = mock(UserRepository.class);
        // 手动注入依赖(通常通过反射)
        ReflectionTestUtils.setField(myService, "userRepository", userRepository);
    }

    @Test
    void testPerformOperation() {
        // do something....
        // 具体代码就不细写了, 重在讲解
    }
}
  1. 如你所见,手动注入依赖不仅增加了测试的复杂度,还可能导致测试代码的维护成本大大增加。相比之下,构造器注入更为简洁和易测试。
  2. 违反单一职责原则
    当我们通过字段注入依赖时,类的依赖关系变得不那么明确。换句话说,类的构造函数不再明确表达它所依赖的对象。随着项目复杂度的增加,这种隐式的依赖关系可能会导致设计上的混乱,违背单一职责原则。
    案例分析

java

  1. 代码解读
  2. 复制代码
@Component
public class OrderService {
    @Autowired
    private PaymentService paymentService;
    @Autowired
    private ShippingService shippingService;
    @Autowired
    private NotificationService notificationService;

    public void placeOrder(Order order) {
        paymentService.processPayment(order);
        shippingService.shipOrder(order);
        notificationService.sendNotification(order);
    }
}
  1. 在这个示例中,OrderService显然依赖了多个服务,这可能表明它承担了过多的职责。通过构造器注入,我们可以更容易地发现这些依赖关系,从而更容易识别出类是否违反了单一职责原则。
  2. 容易引发NPE(空指针异常)
    使用@Autowired进行字段注入时,Spring容器在实例化对象后才会进行依赖注入。这意味着,如果我们在类的构造函数中或其他初始化代码中访问了这些尚未注入的字段,可能会导致空指针异常(NPE)。例如:

java

  1. 代码解读
  2. 复制代码
@Component
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public UserService() {
        // 如果此时访问 userRepository,会抛出 NPE
        System.out.println(userRepository.findAll());
    }
}
  1. 这种问题在开发过程中非常常见,特别是当类的构造函数或@PostConstruct方法中需要访问这些依赖时。构造器注入可以有效避免这个问题,因为依赖项在对象创建时就已经注入完毕。
为什么Spring推荐构造器注入?

既然字段注入存在这么多问题,Spring官方为什么推荐构造器注入呢?这里有几个原因:

  1. 增强代码的可读性和维护性
    构造器注入使得类的依赖关系一目了然。当我们看到一个类的构造函数时,就能明确知道这个类需要哪些依赖项。这不仅提高了代码的可读性,也使得依赖管理更加明确,符合单一职责原则。

java

  1. 代码解读
  2. 复制代码
@Component
public class OrderService {
    private final PaymentService paymentService;
    private final ShippingService shippingService;
    private final NotificationService notificationService;

    @Autowired
    public OrderService(PaymentService paymentService, ShippingService shippingService, NotificationService notificationService) {
        this.paymentService = paymentService;
        this.shippingService = shippingService;
        this.notificationService = notificationService;
    }

    public void placeOrder(Order order) {
        paymentService.processPayment(order);
        shippingService.shipOrder(order);
        notificationService.sendNotification(order);
    }
}
  1. 通过构造器注入,我们可以直观地看到OrderService依赖于PaymentServiceShippingServiceNotificationService,而且这些依赖项都是不可变的。
  2. 方便单元测试
    构造器注入使得单元测试变得更加简单和直观。我们只需在测试中传递模拟的依赖项即可,而不需要依赖Spring上下文或反射来进行依赖注入。这大大简化了测试代码,并提高了测试的稳定性。

java

  1. 代码解读
  2. 复制代码
public class OrderServiceTest {
    private OrderService orderService;
    private PaymentService paymentService;
    private ShippingService shippingService;
    private NotificationService notificationService;

    @BeforeEach
    void setUp() {
        paymentService = mock(PaymentService.class);
        shippingService = mock(ShippingService.class);
        notificationService = mock(NotificationService.class);
        orderService = new OrderService(paymentService, shippingService, notificationService);
    }

    @Test
    void testPlaceOrder() {
        // do something....
        // 具体代码就不细写了, 重在讲解
    }
}
  1. 这种方式不仅让测试代码更加清晰,也使得依赖关系更加明确和易于管理。
  2. 避免NPE问题
    如前所述,构造器注入确保了依赖项在对象创建时即被注入,避免了使用未初始化的依赖项所引发的空指针异常。构造器注入也意味着所有的依赖都是显式传入的,因此不会因为依赖的缺失或注入顺序的问题而导致运行时错误。
  3. 避免循环依赖虽然构造器注入可以避免许多字段注入的问题,但它仍然可能引发循环依赖的问题。循环依赖是指A类依赖于B类,而B类又依赖于A类。构造器注入下,这种情况会导致Spring无法实例化这两个类。为了避免这种问题,可以通过以下几种方式来处理:
  • 重构代码:重新设计类之间的依赖关系,消除循环依赖。
  • 使用@Lazy注解:将其中一个依赖延迟加载,避免循环依赖的发生。

java

  1. 代码解读
  2. 复制代码
@Component
public class ClassA {
    private final ClassB classB;

    @Autowired
    public ClassA(@Lazy ClassB classB) {
        this.classB = classB;
    }
}

@Component
public class ClassB {
    private final ClassA classA;

    @Autowired
    public ClassB(ClassA classA) {
        this.classA = classA;
    }
}
  1. 在上面的代码中,通过@Lazy注解,将ClassB的依赖延迟加载,从而避免了循环依赖的问题。
重构一个Spring项目中的依赖注入

为了更好地理解构造器注入的优势,我们来实践一下如何将一个使用字段注入的Spring项目重构为使用构造器注入,示例代码如下:

java

代码解读

复制代码

@Component
public class UserService {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private NotificationService notificationService;

    public void registerUser(User user) {
        userRepository.save(user);
        notificationService.sendNotification(user);
    }
}

这个类通过字段注入依赖UserRepositoryNotificationService,虽然代码看起来简洁,但如前所述,这种方式可能引发一系列问题。

对上述代码进行重构

java

代码解读

复制代码

@Component
public class UserService {
    private final UserRepository userRepository;
    private final NotificationService notificationService;

    @Autowired
    public UserService(UserRepository userRepository, NotificationService notificationService) {
        this.userRepository = userRepository;
        this.notificationService = notificationService;
    }

    public void registerUser(User user) {
        userRepository.save(user);
        notificationService.sendNotification(user);
    }
}

在重构后的代码中,我们通过构造器注入将依赖显式地传递给UserService,使得依赖关系更加清晰。同时,这种方式也增强了类的不可变性,并减少了潜在的NPE风险。

测试代码的改进

通过构造器注入,我们的测试代码也变得更加直观和易于管理:

java

代码解读

复制代码

public class UserServiceTest {
    private UserService userService;
    private UserRepository userRepository;
    private NotificationService notificationService;

    @BeforeEach
    void setUp() {
        userRepository = mock(UserRepository.class);
        notificationService = mock(NotificationService.class);
        userService = new UserService(userRepository, notificationService);
    }

    @Test
    void testRegisterUser() {
        // 测试 UserService 的 registerUser 方法
    }
}

通过这种方式,我们可以在不依赖Spring容器的情况下轻松编写单元测试,提高了代码的可测试性和稳定性。

总结如下

虽然@Autowired字段注入简单易用,但它在代码可读性、可维护性和测试性方面存在一些严重的缺陷。Spring官方推荐使用构造器注入,因为它能够提高代码的清晰度,减少NPE的发生,并且更利于单元测试。 而且在实际开发中,我们也应该尽量遵循这些最佳实践,通过构造器注入来增强代码的健壮性和可维护性。如果你还在使用字段注入,不妨可以尝试将你的代码重构为构造器注入,通过实践来看看它带来的好处。


转载来源:https://juejin.cn/post/7402549800547844107

相关文章
|
2月前
|
Java Spring
在使用Spring的`@Value`注解注入属性值时,有一些特殊字符需要注意
【10月更文挑战第9天】在使用Spring的`@Value`注解注入属性值时,需注意一些特殊字符的正确处理方法,包括空格、引号、反斜杠、新行、制表符、逗号、大括号、$、百分号及其他特殊字符。通过适当包裹或转义,确保这些字符能被正确解析和注入。
129 3
|
12天前
|
Java Spring
一键注入 Spring 成员变量,顺序编程
介绍了一款针对Spring框架开发的插件,旨在解决开发中频繁滚动查找成员变量注入位置的问题。通过一键操作(如Ctrl+1),该插件可自动在类顶部添加`@Autowired`注解及其成员变量声明,同时保持光标位置不变,有效提升开发效率和代码编写流畅度。适用于IntelliJ IDEA 2023及以上版本。
一键注入 Spring 成员变量,顺序编程
|
2月前
|
缓存 Java Spring
源码解读:Spring如何解决构造器注入的循环依赖?
本文详细探讨了Spring框架中的循环依赖问题,包括构造器注入和字段注入两种情况,并重点分析了构造器注入循环依赖的解决方案。文章通过具体示例展示了循环依赖的错误信息及常见场景,提出了三种解决方法:重构代码、使用字段依赖注入以及使用`@Lazy`注解。其中,`@Lazy`注解通过延迟初始化和动态代理机制有效解决了循环依赖问题。作者建议优先使用`@Lazy`注解,并提供了详细的源码解析和调试截图,帮助读者深入理解其实现机制。
71 1
|
4月前
|
XML Java 数据格式
Spring5入门到实战------4、IOC容器-Bean管理XML方式、集合的注入(二)
这篇文章是Spring5框架的实战教程,主题是IOC容器中Bean的集合属性注入,通过XML配置方式。文章详细讲解了如何在Spring中注入数组、List、Map和Set类型的集合属性,并提供了相应的XML配置示例和Java类定义。此外,还介绍了如何在集合中注入对象类型值,以及如何使用Spring的util命名空间来实现集合的复用。最后,通过测试代码和结果展示了注入效果。
Spring5入门到实战------4、IOC容器-Bean管理XML方式、集合的注入(二)
|
4月前
|
缓存 Java 数据库连接
Spring Boot 资源文件属性配置,紧跟技术热点,为你的应用注入灵动活力!
【8月更文挑战第29天】在Spring Boot开发中,资源文件属性配置至关重要,它让开发者能灵活定制应用行为而不改动代码,极大提升了可维护性和扩展性。Spring Boot支持多种配置文件类型,如`application.properties`和`application.yml`,分别位于项目的resources目录下。`.properties`文件采用键值对形式,而`yml`文件则具有更清晰的层次结构,适合复杂配置。此外,Spring Boot还支持占位符引用和其他外部来源的属性值,便于不同环境下覆盖默认配置。通过合理配置,应用能快速适应各种环境与需求变化。
55 0
|
4月前
|
安全 Java 开发者
开发者必看!@Resource与private final的较量,Spring Boot注入技巧大揭秘,你不可不知的细节!
【8月更文挑战第29天】Spring Boot作为热门Java框架,其依赖注入机制备受关注。本文通过对比@Resource(JSR-250规范)和@Autowired(Spring特有),并结合private final声明的字段注入,详细探讨了两者的区别与应用场景。通过示例代码展示了@Resource按名称注入及@Autowired按类型注入的特点,并分析了它们在注入时机、依赖性、线程安全性和单一职责原则方面的差异,帮助开发者根据具体需求选择最合适的注入策略。
185 0
|
5月前
|
Java Spring
spring注入的几种方式
spring注入的几种方式
29 0
|
6月前
|
Java Spring 容器
spring如何进行依赖注入,通过set方法把Dao注入到serves
spring如何进行依赖注入,通过set方法把Dao注入到serves
|
XML Java 数据格式
Spring【依赖注入】就是这么简单(二)
在Spring的第二篇中主要讲解了Spring Core模块的使用IOC容器创建对象的问题,Spring Core模块主要是解决对象的创建和对象之间的依赖关系,因此本博文主要讲解如何使用IOC容器来解决对象之间的依赖关系!
140 0
Spring【依赖注入】就是这么简单(二)
|
Java 测试技术 容器
Spring【依赖注入】就是这么简单
前言 在Spring的第二篇中主要讲解了Spring Core模块的使用IOC容器创建对象的问题,Spring Core模块主要是解决对象的创建和对象之间的依赖关系,因此本博文主要讲解如何使用IOC容器来解决对象之间的依赖关系! 回顾以前对象依赖 我们来看一下我们以前关于对象依赖,是怎么的历程 直接new对象 在最开始,我们是直接new对象给serice的userDao属性赋值.
1192 0
下一篇
DataWorks