pos机网站建设方案,优化seo软件,木疙瘩h5制作教程,移动商城网站开发选择停止在 SpringBoot 中使用字段注入#xff01;
本文为翻译文#xff0c;同时加入了一些自己的理解#xff0c;翻译来源#xff1a;https://medium.com
在 Spring Boot 依赖项注入的上下文中#xff0c;存在关于注入依赖项最佳实践的争论:字段注入、Setter注入和构造函数…停止在 SpringBoot 中使用字段注入
本文为翻译文同时加入了一些自己的理解翻译来源https://medium.com
在 Spring Boot 依赖项注入的上下文中存在关于注入依赖项最佳实践的争论:字段注入、Setter注入和构造函数注入。
❝在本文中我们将重点讨论字段注入的缺陷并提出一个远离它的案例。❞
1 什么是字段注入
字段注入涉及直接用 Autowired 注释类的私有字段。这是一个例子
Component
public class OrderService {Autowiredprivate OrderRepository orderRepository;public Order findOrderById(Long id) {return orderRepository.findById(id);}
}2 为什么应该停止使用字段注入
2.1 可测试性
字段注入使组件的单元测试变得复杂。 由于依赖项直接注入到字段中因此我们无法在 Spring 上下文之外轻松提供模拟或替代实现。
让我们以 sameOrderService 类为例。
如果我们希望对 OrderService 进行单元测试那么在模拟 OrderRepository 时会遇到困难因为它是一个私有字段。下面是对 OrderService 进行单元测试的方法:
RunWith(SpringJUnit4ClassRunner.class)
public class OrderServiceTest {private OrderService orderService;Mockprivate OrderRepository orderRepository;Beforepublic void setUp() throws Exception {orderService new OrderService();// This will set the mock orderRepository into orderServices private fieldReflectionTestUtils.setField(orderService, orderRepository, orderRepository);}...
}尽管可以实现但使用反射来替换私有字段并不是一个很好的设计。它违背了面向对象的设计原则使测试难以阅读和维护。
但是如果我们使用构造函数注入
Component
public class OrderService {private final OrderRepository orderRepository;public OrderService(OrderRepository orderRepository) {this.orderRepository orderRepository;}
}我们可以在测试期间轻松提供模拟 OrderRepository
OrderRepository mockRepo mock(OrderRepository.class);
OrderService orderService new OrderService(mockRepo);2.2 不变性
字段注入使我们的 Bean 在构建后可变。而通过构造函数注入一旦构造了一个对象它的依赖关系就会保持不变。
举例来说
字段注入类
Component
public class UserService {Autowiredprivate UserRepository userRepository;
}这里userRepository 在创建对象后可以重新分配引用这就打破了不变性原则。
如果我们使用构造函数注入
Component
public class UserService {private final UserRepository userRepository;public UserService(UserRepository userRepository) {this.userRepository userRepository;}
}该 userRepository 字段可以声明为最终字段在构造完成后就会一直保持不变。
2.2.1 这里的 Autowired private UserRepository userRepository; 不能在private后加一个final吗
在Spring框架中使用字段注入时你通常不会在被Autowired注解的字段后加上final关键字。原因在于当Spring创建Bean的实例时它需要能够设置这些没有通过构造函数提供的依赖关系。如果字段被声明为final那么它必须在构造对象的时候初始化之后就不能再改变了。
字段注入通常是通过反射来完成的反射可以允许即使字段被声明为private仍然能够被外部类修改。但是如果你使用final来修饰字段反射也不能用于改变其值因为final字段在构造对象后就不可变了。这就是为什么你会在构造函数注入中看到final字段而在字段注入中不会看到。
构造函数注入提供了不变性因为你可以将所有依赖项声明为final。这意味着一旦构造了对象这些依赖项就不能更改从而可以避免很多因为状态改变导致的问题。这也符合不变性原则是当前推荐的注入方式因为它提高了代码的安全性和健壮性。
总结一下Autowired注解的字段不能声明为final因为Spring需要在对象构造后设置这些字段。如果你想要不变性你应该使用构造函数注入并将依赖项声明为final。
2.2.2 如果我使用构造函数注入一个bean要怎么样使用他呢
当你使用构造函数注入来注入一个Bean时Spring容器会负责创建这个Bean的实例并且会自动把构造函数需要的依赖项注入进去。这里是一个使用构造函数注入的基本步骤
定义依赖项接口。创建依赖项的实现类并使用例如Component注解将其标记为Spring管理的Bean。在使用依赖项的类中创建一个构造函数该构造函数接受依赖项作为参数并使用例如Autowired注解在Spring 4.3之后如果类只有一个构造函数可以省略Autowired注解。
以下是一个简单的示例
// 依赖项接口
public interface UserRepository {// 定义所需要的操作例如查找用户等
}// 依赖项的实现类
Component
public class UserRepositoryImpl implements UserRepository {// 实现UserRepository接口中定义的方法
}// 使用依赖项的类
Component
public class UserService {private final UserRepository userRepository;// 构造函数注入public UserService(UserRepository userRepository) {this.userRepository userRepository;}// 类中的其他方法可以使用userRepositorypublic void performAction() {// 使用userRepository执行一些操作}
}在这个例子中UserService需要一个UserRepository的实例。Spring会自动找到匹配UserRepository类型的Bean在这个例子中是UserRepositoryImpl的实例然后创建一个UserService的实例将UserRepositoryImpl作为参数传递给UserService的构造函数。
在Spring应用中你不需要自己去创建UserService的实例。Spring容器会自动处理这一切。当你需要使用UserService时你可以让Spring自动注入它例如
RestController
public class UserController {private final UserService userService;// 在控制器中通过构造函数注入UserServicepublic UserController(UserService userService) {this.userService userService;}GetMapping(/users)public ResponseEntityListUser getUsers() {// 使用userService来处理获取用户的请求}
}在上面的控制器中UserService将被自动注入到UserController中。这样你就可以在控制器中使用UserService提供的方法来处理请求了。
2.3 与Spring更紧密的耦合
字段注入使我们的类与 Spring 耦合更紧密因为它直接在我们的字段上使用 Spring 特定的注释 ( Autowired)。这可能会在以下场景中出现问题
「不使用 Spring 的情况」假设我们正在构建一个不使用 Spring 的轻量级命令行应用程序但我们仍然想利用 UserService 的逻辑。在这种情况下Autowired 注释没有任何意义不能用于注入依赖项。我们就必须重构该类或实现繁琐的解决方法才能重用UserService.
「切换到另一个 DI 框架」如果我们决定切换到另一个依赖注入框架比如 Google GuiceSpring 特定的框架 Autowired 就会成为一个障碍。那时我们必须重构使用 Spring 特定注释的每一个地方这会是十分繁琐的。
「可读性和理解性」对于不熟悉 Spring 的开发人员来说遇到 Autowired 注解可能会感到困惑。他们可能想知道如何解决依赖关系从而增加学习成本ps虽然不熟悉 Spring 开发的Java程序员可能很少了。
2.4 空指针异常
当类利用字段注入并通过其默认构造函数实例化时依赖字段保持未初始化。
举例来讲
Component
public class PaymentGateway {Autowiredprivate PaymentQueue paymentQueue;public void initiate (PaymentRequest request){paymentQueue.add(request);...}
}public class PaymentService {public void process (PaymentRequest request) {PaymentGateway gateway new PaymentGateway();gateway.initiate(request);}
}通过上面的代码我们不难看出如果在运行时以这种状态访问PaymentGateway则会发生 NullPointerException。在Spring上下文之外手动初始化这些字段的唯一方法是使用反射反射机制的语法比较繁琐且易错在程序可读性方面存在一定问题所以不建议这样做。
2.4.1 为什么直接在非spring类中new一个spring的bean会报NPE
在您提供的代码示例中PaymentService类直接通过new关键字创建了PaymentGateway的实例而不是通过Spring的依赖注入来获取。当直接使用new关键字创建实例时Spring容器不会介入该对象的生命周期这意味着Spring不会自动注入PaymentGateway中的依赖paymentQueue。
由于paymentQueue没有被初始化因为Spring没有注入它当initiate方法被调用时它尝试访问paymentQueue的add方法。因为此时paymentQueue是null所以尝试调用其方法会导致NullPointerExceptionNPE。
在Spring应用程序中为了避免此类问题应该总是通过Spring容器获取Bean实例这样Spring就能自动管理Bean的生命周期和依赖注入。如果你需要在Spring管理的Bean中使用PaymentGateway你应该让Spring注入它而不是自己创建实例。
例如改正后的PaymentService可能会看起来像这样
Service
public class PaymentService {private final PaymentGateway paymentGateway;Autowiredpublic PaymentService(PaymentGateway paymentGateway) {this.paymentGateway paymentGateway;}public void process(PaymentRequest request) {paymentGateway.initiate(request);}
}在这个修改后的版本中PaymentGateway由Spring通过构造函数注入到PaymentService中这样就确保了PaymentGateway的paymentQueue依赖会被Spring容器自动注入从而避免了NPE。
2.5 循环依赖
字段注入可能会掩盖循环依赖问题使它们在开发过程中更难被发现。 举例来讲 考虑两个相互依赖的服务AService和BService:
Service public class AService { Autowired private BService bService; }
Service public class BService { Autowired private AService aService; }
以上可能会导致应用程序中出现意想不到的问题。
使用构造函数注入Spring会在启动期间立即抛出 BeanCurrentlyInCreationException让我们意识到循环依赖。不过要解决循环依赖问题可以使用Lazy延迟加载其中一个依赖项。
2.5.1 我记得spring中是允许循环依赖的吧如何解决的
是的Spring框架确实支持循环依赖但是这种支持仅限于字段注入setter注入和方法注入。Spring通过使用三级缓存来解决单例作用域下的循环依赖问题使得在构造函数中注入循环依赖的Bean成为可能。
对于构造函数注入来说Spring无法处理循环依赖因为在调用构造函数之前每个Bean的依赖必须先被解决。如果A需要B才能创建而B同时也需要A才能创建Spring就无法决定应该先创建哪个Bean因此会抛出BeanCurrentlyInCreationException异常。
对于您提到的例子如果两个服务A和B都通过构造函数相互注入Spring会在应用程序启动时检测到循环依赖并抛出异常。如果使用字段注入Spring可以通过先实例化一个Bean然后在设置属性时实现注入从而解决循环依赖的问题。
2.5.2 构造器注入如何解决循环依赖问题
如果想要在构造函数注入中解决循环依赖问题可以使用Lazy注解来延迟依赖项的加载。例如
Service
public class AService {private final BService bService;Autowiredpublic AService(Lazy BService bService) {this.bService bService;}
}Service
public class BService {private final AService aService;Autowiredpublic BService(AService aService) {this.aService aService;}
}在上面的代码中Lazy注解确保了BService在AService实例化时不会立即被创建而是在首次访问BService时才创建。这样Spring可以先完成AService的创建然后在需要时创建BService实例避免了循环依赖问题。
2.6 Autowired注入不推荐那么Resource呢能够避免Autowired的这些问题吗
Resource 是 Java EE 6 中的注解它可以用来注入依赖项其行为略有不同于 Spring 的 Autowired。下面是这两个注解的一些区别 来源 Autowired 是 Spring 特有的注解。Resource 来自于 Java 的 javax.annotation 包。 注入方式 Autowired 默认按类型进行自动装配。当需要按名称装配时可以结合 Qualifier 注解使用。Resource 默认按名称进行注入如果没有找到与名称匹配的bean则会按类型进行注入。它有两个重要属性name 和 type。 兼容性 Autowired 与 Spring 紧密集成支持Spring特有的功能如 Qualifier、Primary 等。Resource 是标准的 Java 注解因此它不依赖于 Spring并且可以用在任何兼容 Java EE 的容器中。
关于能否避免 Autowired 的问题 循环依赖Resource 并不能解决构造器注入时的循环依赖问题这是因为循环依赖的问题与注入机制本身相关而不是特定于某个注解。Spring容器需要创建bean实例时必须解决所有必需的依赖不管这些依赖是通过 Autowired 还是 Resource 注入的。 依赖不明确Resource 默认按名称注入这使得它在有多个类型相同的bean时可以通过名称指定具体依赖哪个bean从而在某种程度上提高了注入的明确性。 自动装配的灵活性使用 Resource 时你失去了 Spring 提供的一些灵活性比如使用 Primary 注解来指定首选的bean。
总的来说Resource 注解提供了另一种依赖注入的方式但它并不能解决所有 Autowired 可能引发的问题。在Spring框架中建议优先使用构造器注入无论是通过 Autowired 还是通过参数解析因为它可以帮助你避免上述提到的大多数问题并且还能提高代码的可测试性。
2.7 在互联网大厂中哪一种注入用的多
在互联网大厂中构造器注入Constructor Injection通常是首选的依赖注入方法原因如下 不变性和安全性构造器注入使得依赖项可以是 final 的这意味着一旦构造了对象其依赖就不能更改。这种不变性可以减少在多线程环境下的问题并确保依赖项在使用前已经被完全初始化。 可测试性使用构造器注入可以在不启动整个Spring容器的情况下更容易地进行单元测试。 明确的依赖构造器参数强制要求在创建对象时提供依赖项这使得依赖关系更加明确避免了 null 引用的可能性。 框架无关性构造器注入不依赖于Spring或者任何其他依赖注入框架这使得代码更容易迁移和重构。
尽管构造器注入在很多情况下是更好的选择但在实际开发中还是会根据具体场景和需求来决定使用哪种注入方式。例如当存在多个构造器参数并且这些参数中的某些是可选的或者在某些复杂的依赖场景中开发者可能会选择字段注入Field Injection或者设值注入Setter Injection。
Autowired 和 Resource 注解都用于自动装配Spring Bean但由于 Autowired 提供了与Spring更紧密的集成和更多的灵活性它通常是更受青睐的选择。不过实际使用哪一个还是要基于项目的具体需求、团队习惯以及编码规范来决定。在一些遵循严格的Java EE标准的项目中可能会倾向于使用 Resource。
最终无论哪种注入方式重要的是保持一致性、清晰性和可维护性。大厂的代码规范会倾向于推崇这些原则并通过代码审查、文档和团队培训来确保最佳实践的贯彻执行。
3 结论
虽然字段注入可能看起来更简洁但它的缺点远远超过了它的简洁性。构造函数注入在应用程序的可测试性、不变性和整体稳健性方面提供了明显的优势。
它与 SOLID 原则非常一致确保我们的 Spring Boot 应用程序可维护且不易出错。
所以建议大家停止在 Spring Boot 中使用字段注入