Spring踩坑:抽象类作为父类,利用子类@Autowired属性进行添补,属性值为nu ...

诗林  高级会员 | 2024-7-26 14:06:53 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 227|帖子 227|积分 681

Spring Boot中抽象类和依靠注入的最佳实践

引言

在Spring Boot应用程序中,抽象类常常被用作一种强大的设计模式,用于封装共同的行为和属性。然而,当涉及到依靠注入时,特殊是在抽象类中,我们必要格外小心。本文将深入探讨在Spring Boot 2.0及以上版本中利用抽象类作为父类时的最佳实践,特殊关注依靠注入的正确利用方式。
在抽象类中利用@Autowired注解

在Spring Boot 2.0及以上版本中,我们可以直接在抽象类的属性上利用@Autowired注解进行依靠注入。这为我们提供了一种方便的方式来在父类中界说共同的依靠,供子类利用。
protected vs private修饰符

当在抽象类中利用@Autowired注解时,我们通常有两种选择来修饰这些属性:protected或private。

  • 利用protected修饰符:
    1. public abstract class AbstractService {
    2.     @Autowired
    3.     protected SomeRepository repository;
    4. }
    复制代码
    长处:

    • 允许子类直接访问注入的依靠
    • 提供了更大的灵活性,子类可以根据必要重写或扩展这些依靠的利用
    缺点:
       

    • 可能会破坏封装性,因为子类可以直接修改这些依靠

  • 利用private修饰符:
    1. public abstract class AbstractService {
    2.     @Autowired
    3.     private SomeRepository repository;
    4.     protected SomeRepository getRepository() {
    5.         return repository;
    6.     }
    7. }
    复制代码
    长处:

    • 保持了良好的封装性
    • 父类可以控制子类怎样访问这些依靠
    缺点:
       

    • 必要额外的getter方法来允许子类访问这些依靠

在Spring Boot 2.0中,这两种方式都是可行的。选择哪种方式主要取决于你的设计需求和偏好。如果你希望严格控制依靠的访问,利用private加getter方法可能是更好的选择。如果你希望提供最大的灵活性给子类,利用protected可能更符合。
低版本Spring Boot的注意事项

在低于2.0的Spring Boot版本中,利用protected修饰符通常是更安全的选择。这是因为在一些早期版本中,private字段的自动注入可能会遇到问题。如果你正在利用较旧的Spring Boot版本,建议利用protected修饰符来确保依靠能够正确注入。
构造器中的依靠注入陷阱

在抽象类中,我们常常必要在构造器中执行一些初始化逻辑。然而,这里有一个重要的陷阱必要注意:不应该在构造器中引用通过@Autowired注入的属性。
为什么不能在构造器中利用注入的属性?

原因在于Spring的bean生命周期和依靠注入的机遇。当Spring创建一个bean时,它遵循以下步调:

  • 实例化bean(调用构造器)
  • 注入依靠(设置@Autowired字段)
  • 调用初始化方法(如@PostConstruct注解的方法)
这意味着在构造器执行时,@Autowired注解的属性还没有被注入,它们的值为null。如果你在构造器中实验利用这些属性,很可能会遇到NullPointerException。
让我们看一个错误的例子:
  1. public abstract class AbstractService {
  2.     @Autowired
  3.     private SomeRepository repository;
  4.     public AbstractService() {
  5.         // 错误:此时repository还是null
  6.         repository.doSomething();
  7.     }
  8. }
复制代码
这段代码会在运行时抛出NullPointerException,因为在构造器执行时,repository还没有被注入。
子类构造的问题

这个问题在子类中更加复杂。当你创建一个抽象类的子类时,子类的构造器会首先调用父类的构造器。这意味着即使是在子类的构造器中,父类中@Autowired注解的属性仍然是null。
  1. public class ConcreteService extends AbstractService {
  2.     public ConcreteService() {
  3.         super(); // 调用AbstractService的构造器
  4.         // 错误:此时父类中的repository仍然是null
  5.         getRepository().doSomething();
  6.     }
  7. }
复制代码
这段代码同样会抛出NullPointerException,因为在调用子类构造器时,父类中的依靠还没有被注入。
@PostConstruct的利用

为相识决构造器中无法利用注入依靠的问题,Spring提供了@PostConstruct注解。被@PostConstruct注解的方法会在依靠注入完成后被自动调用,这使得它成为执行初始化逻辑的理想位置。
正确利用@PostConstruct的例子

  1. public abstract class AbstractService {
  2.     @Autowired
  3.     private SomeRepository repository;
  4.     @PostConstruct
  5.     public void init() {
  6.         // 正确:此时repository已经被注入
  7.         repository.doSomething();
  8.     }
  9. }
复制代码
在这个例子中,init()方法会在全部依靠注入完成后被调用,因此可以安全地利用repository。
子类中的@PostConstruct

子类也可以界说自己的@PostConstruct方法,这些方法会在父类的@PostConstruct方法之后被调用:
  1. public class ConcreteService extends AbstractService {
  2.     @Autowired
  3.     private AnotherDependency anotherDependency;
  4.     @PostConstruct
  5.     public void initChild() {
  6.         // 父类的init()方法已经被调用
  7.         // 可以安全地使用父类和子类的所有依赖
  8.         getRepository().doSomething();
  9.         anotherDependency.doSomethingElse();
  10.     }
  11. }
复制代码
这种方式确保了全部的初始化逻辑都在依靠注入完成后执行,避免了NullPointerException的风险。
避免在构造器中利用ApplicationContext.getBean

另一个常见的陷阱是在构造器中利用ApplicationContext.getBean()方法来获取bean。这种做法应该被避免,原因如下:

  • 在构造器执行时,ApplicationContextAware接口可能还没有被调用,这意味着ApplicationContext可能还不可用。
  • 即使ApplicationContext可用,其他bean可能还没有被完全初始化,调用getBean()可能会返回未完全初始化的bean或触发意外的初始化。
  • 利用ApplicationContext.getBean()会使你的代码与Spring框架细密耦合,降低了可测试性和可维护性。
错误示例

  1. public abstract class AbstractService implements ApplicationContextAware {
  2.     private ApplicationContext context;
  3.     public AbstractService() {
  4.         // 错误:此时context还是null
  5.         SomeBean someBean = context.getBean(SomeBean.class);
  6.     }
  7.     @Override
  8.     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  9.         this.context = applicationContext;
  10.     }
  11. }
复制代码
这段代码会抛出NullPointerException,因为在构造器执行时,setApplicationContext()方法还没有被调用。
正确做法

正确的做法是利用依靠注入,让Spring容器管理对象的创建和依靠关系:
  1. public abstract class AbstractService {
  2.     @Autowired
  3.     private SomeBean someBean;
  4.     @PostConstruct
  5.     public void init() {
  6.         // 正确:此时someBean已经被注入
  7.         someBean.doSomething();
  8.     }
  9. }
复制代码
这种方式不仅避免了NullPointerException,还降低了与Spring框架的耦合度,使代码更易于测试和维护。
最佳实践示例

让我们通过一个完备的例子来展示这些最佳实践:
  1. @Service
  2. public abstract class AbstractUserService {
  3.     @Autowired
  4.     private UserRepository userRepository;
  5.     @Autowired
  6.     private EmailService emailService;
  7.     protected AbstractUserService() {
  8.         // 构造器中不做任何依赖相关的操作
  9.     }
  10.     @PostConstruct
  11.     protected void init() {
  12.         // 初始化逻辑
  13.         System.out.println("AbstractUserService initialized with " + userRepository.getClass().getSimpleName());
  14.     }
  15.     public User findUserById(Long id) {
  16.         return userRepository.findById(id).orElse(null);
  17.     }
  18.     protected void sendEmail(User user, String message) {
  19.         emailService.sendEmail(user.getEmail(), message);
  20.     }
  21.     // 抽象方法,由子类实现
  22.     public abstract void processUser(User user);
  23. }
  24. @Service
  25. public class ConcreteUserService extends AbstractUserService {
  26.     @Autowired
  27.     private SpecialProcessor specialProcessor;
  28.     @PostConstruct
  29.     protected void initChild() {
  30.         System.out.println("ConcreteUserService initialized with " + specialProcessor.getClass().getSimpleName());
  31.     }
  32.     @Override
  33.     public void processUser(User user) {
  34.         User processedUser = specialProcessor.process(user);
  35.         sendEmail(processedUser, "Your account has been processed.");
  36.     }
  37. }
  38. // 使用示例
  39. @RestController
  40. @RequestMapping("/users")
  41. public class UserController {
  42.     @Autowired
  43.     private ConcreteUserService userService;
  44.     @GetMapping("/{id}")
  45.     public ResponseEntity<User> getUser(@PathVariable Long id) {
  46.         User user = userService.findUserById(id);
  47.         if (user != null) {
  48.             userService.processUser(user);
  49.             return ResponseEntity.ok(user);
  50.         } else {
  51.             return ResponseEntity.notFound().build();
  52.         }
  53.     }
  54. }
复制代码
在这个例子中:

  • ​AbstractUserService​ 是一个抽象类,它界说了一些通用的用户服务逻辑。
  • 依靠(UserRepository​ 和 EmailService​)通过 @Autowired​ 注入到抽象类中。
  • 初始化逻辑放在 @PostConstruct​ 注解的 init()​ 方法中,确保在全部依靠注入完成后执行。
  • ​ConcreteUserService​ 继承自 AbstractUserService​,并实现了抽象方法。
  • ​ConcreteUserService​ 有自己的依靠(SpecialProcessor​)和初始化逻辑。
  • 在 UserController​ 中,我们注入并利用 ConcreteUserService​。
这个设计遵循了我们讨论的全部最佳实践:


  • 在抽象类中利用 @Autowired​ 注入依靠
  • 避免在构造器中利用注入的依靠
  • 利用 @PostConstruct​ 进行初始化
  • 倒霉用 ApplicationContext.getBean()​
常见问题和办理方案

在利用抽象类和依靠注入时,开辟者可能会遇到一些常见问题。以下是一些问题及其办理方案:
1. 循环依靠

问题:当两个类相互依靠时,可能会导致循环依靠问题。
办理方案:


  • 重新设计以消除循环依靠
  • 利用 @Lazy​ 注解来耽误此中一个依靠的初始化
  • 利用 setter 注入而不是构造器注入
  1. @Service
  2. public class ServiceA {
  3.     private ServiceB serviceB;
  4.     @Autowired
  5.     public void setServiceB(@Lazy ServiceB serviceB) {
  6.         this.serviceB = serviceB;
  7.     }
  8. }
  9. @Service
  10. public class ServiceB {
  11.     @Autowired
  12.     private ServiceA serviceA;
  13. }
复制代码
2. 依靠注入在单元测试中的问题

问题:在单元测试中,可能难以模拟复杂的依靠注入场景。
办理方案:


  • 利用 Spring 的测试支持,如 @SpringBootTest​
  • 为测试创建一个简化的配置类
  • 利用模拟框架如 Mockito 来模拟依靠
  1. @SpringBootTest
  2. class ConcreteUserServiceTest {
  3.     @MockBean
  4.     private UserRepository userRepository;
  5.     @Autowired
  6.     private ConcreteUserService userService;
  7.     @Test
  8.     void testFindUserById() {
  9.         when(userRepository.findById(1L)).thenReturn(Optional.of(new User(1L, "Test User")));
  10.         User user = userService.findUserById(1L);
  11.         assertNotNull(user);
  12.         assertEquals("Test User", user.getName());
  13.     }
  14. }
复制代码
3. 属性注入vs构造器注入

问题:虽然属性注入(利用 @Autowired​ on fields)很方便,但它可能使得依靠关系不那么明显。
办理方案:思量利用构造器注入,特殊是对于必须的依靠。这使得依靠关系更加明白,并有助于创建不可变的服务。
  1. @Service
  2. public abstract class AbstractUserService {
  3.     private final UserRepository userRepository;
  4.     private final EmailService emailService;
  5.     @Autowired
  6.     protected AbstractUserService(UserRepository userRepository, EmailService emailService) {
  7.         this.userRepository = userRepository;
  8.         this.emailService = emailService;
  9.     }
  10.     // ... 其他方法
  11. }
  12. @Service
  13. public class ConcreteUserService extends AbstractUserService {
  14.     private final SpecialProcessor specialProcessor;
  15.     @Autowired
  16.     public ConcreteUserService(UserRepository userRepository,
  17.                                EmailService emailService,
  18.                                SpecialProcessor specialProcessor) {
  19.         super(userRepository, emailService);
  20.         this.specialProcessor = specialProcessor;
  21.     }
  22.     // ... 其他方法
  23. }
复制代码
这种方法的长处是:


  • 依靠关系更加明白
  • 有助于创建不可变的服务
  • 更易于单元测试
4. 抽象类中的 @Autowired 方法

问题:偶然我们可能想在抽象类中有一个被 @Autowired 注解的方法,但这个方法在子类中被重写了。
办理方案:利用 @Autowired 注解抽象方法,并在子类中实现它。
  1. public abstract class AbstractService {
  2.     @Autowired
  3.     protected abstract Dependencies getDependencies();
  4.     @PostConstruct
  5.     public void init() {
  6.         getDependencies().doSomething();
  7.     }
  8. }
  9. @Service
  10. public class ConcreteService extends AbstractService {
  11.     @Autowired
  12.     private Dependencies dependencies;
  13.     @Override
  14.     protected Dependencies getDependencies() {
  15.         return dependencies;
  16.     }
  17. }
复制代码
这种方法允许子类控制依靠的具体实现,同时保持父类的通用逻辑。
5. 运行时依靠注入

问题:偶然我们可能必要在运行时动态注入依靠,而不是在启动时。
办理方案:利用 ObjectProvider<T>​ 来耽误依靠的分析。
  1. @Service
  2. public abstract class AbstractDynamicService {
  3.     @Autowired
  4.     private ObjectProvider<DynamicDependency> dependencyProvider;
  5.     protected DynamicDependency getDependency() {
  6.         return dependencyProvider.getIfAvailable();
  7.     }
  8.     // ... 其他方法
  9. }
复制代码
这种方法允许我们在必要时才分析依靠,这在某些场景下可能很有用,比如条件性的bean创建。
最佳实践总结

基于我们的讨论,以下是在Spring Boot中利用抽象类和依靠注入的最佳实践总结:

  • 在抽象类中利用 @Autowired: 可以直接在抽象类的字段上利用 @Autowired 注解。利用 protected 修饰符可以让子类直接访问这些依靠,而利用 private 加 getter 方法可以提供更好的封装。
  • 避免在构造器中利用注入的依靠: 构造器执行时,依靠还没有被注入,因此不应该在构造器中利用它们。
  • 利用 @PostConstruct 进行初始化: 将必要依靠的初始化逻辑放在 @PostConstruct 注解的方法中,确保全部依靠都已注入。
  • 不要在构造器中利用 ApplicationContext.getBean: 这可能导致意外的行为,因为在构造器执行时,ApplicationContext 可能还未完全准备好。
  • 思量利用构造器注入: 对于必须的依靠,构造器注入可以使依靠关系更加明白,并有助于创建不可变的服务。
  • 处理循环依靠: 利用 @Lazy 注解或 setter 注入来办理循环依靠问题。
  • 公道利用抽象方法: 在抽象类中界说抽象方法可以让子类控制某些依靠的具体实现。
  • 利用 ObjectProvider 进办法态依靠注入: 当必要在运行时动态分析依靠时,思量利用 ObjectProvider。
  • 注意测试: 在单元测试中,利用 Spring 的测试支持和模拟框架来处理复杂的依靠注入场景。
  • 遵循 SOLID 原则: 特殊是单一责任原则和依靠倒置原则,这有助于创建更易维护和测试的代码。
结论

在Spring Boot中利用抽象类和依靠注入是一种强大的技术,可以帮助我们创建灵活、可维护的代码。然而,它也带来了一些挑战,特殊是在处理依靠注入的机遇和方式上。
通过遵循本文讨论的最佳实践,我们可以避免常见的陷阱,充实利用Spring Boot提供的依靠注入功能。记住,关键是要明白Spring Bean的生命周期,公道利用 @PostConstruct 注解,避免在不适当的时候访问依靠,并选择适合你的项目的依靠注入方式。
最后,虽然这些是广泛认可的最佳实践,但每个项目都有其独特的需求。因此,始终要根据你的具体情况来调解这些实践。一连学习和实践是掌握Spring Boot中抽象类和依靠注入的关键。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

诗林

高级会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表