Spring踩坑:抽象类作为父类,使用子类@Autowired属性进行填充,属性值为null

Spring踩坑:抽象类作为父类,使用子类@Autowired属性进行填充,属性值为null

  • Spring Boot中抽象类和依赖注入的最佳实践
    • 引言
    • 在抽象类中使用@Autowired注解
      • protected vs private修饰符
      • 低版本Spring Boot的注意事项
    • 构造器中的依赖注入陷阱
      • 为什么不能在构造器中使用注入的属性?
      • 子类构造的问题
    • @PostConstruct的使用
      • 正确使用@PostConstruct的例子
      • 子类中的@PostConstruct
    • 避免在构造器中使用ApplicationContext.getBean
      • 错误示例
      • 正确做法
    • 最佳实践示例
    • 常见问题和解决方案
      • 1. 循环依赖
      • 2. 依赖注入在单元测试中的问题
      • 3. 属性注入vs构造器注入
      • 4. 抽象类中的 @Autowired 方法
      • 5. 运行时依赖注入
    • 最佳实践总结
    • 结论

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

引言

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

在抽象类中使用@Autowired注解

在Spring Boot 2.0及以上版本中,我们可以直接在抽象类的属性上使用@Autowired注解进行依赖注入。这为我们提供了一种方便的方式来在父类中定义共同的依赖,供子类使用。

protected vs private修饰符

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

  1. 使用protected修饰符:

    public abstract class AbstractService {@Autowiredprotected SomeRepository repository;
    }
    

    优点:

    • 允许子类直接访问注入的依赖
    • 提供了更大的灵活性,子类可以根据需要重写或扩展这些依赖的使用

    缺点:

    • 可能会破坏封装性,因为子类可以直接修改这些依赖
  2. 使用private修饰符:

    public abstract class AbstractService {@Autowiredprivate SomeRepository repository;protected SomeRepository getRepository() {return repository;}
    }
    

    优点:

    • 保持了良好的封装性
    • 父类可以控制子类如何访问这些依赖

    缺点:

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

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

低版本Spring Boot的注意事项

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

构造器中的依赖注入陷阱

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

为什么不能在构造器中使用注入的属性?

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

  1. 实例化bean(调用构造器)
  2. 注入依赖(设置@Autowired字段)
  3. 调用初始化方法(如@PostConstruct注解的方法)

这意味着在构造器执行时,@Autowired注解的属性还没有被注入,它们的值为null。如果你在构造器中尝试使用这些属性,很可能会遇到NullPointerException。

让我们看一个错误的例子:

public abstract class AbstractService {@Autowiredprivate SomeRepository repository;public AbstractService() {// 错误:此时repository还是nullrepository.doSomething();}
}

这段代码会在运行时抛出NullPointerException,因为在构造器执行时,repository还没有被注入。

子类构造的问题

这个问题在子类中更加复杂。当你创建一个抽象类的子类时,子类的构造器会首先调用父类的构造器。这意味着即使是在子类的构造器中,父类中@Autowired注解的属性仍然是null。

public class ConcreteService extends AbstractService {public ConcreteService() {super(); // 调用AbstractService的构造器// 错误:此时父类中的repository仍然是nullgetRepository().doSomething();}
}

这段代码同样会抛出NullPointerException,因为在调用子类构造器时,父类中的依赖还没有被注入。

@PostConstruct的使用

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

正确使用@PostConstruct的例子

public abstract class AbstractService {@Autowiredprivate SomeRepository repository;@PostConstructpublic void init() {// 正确:此时repository已经被注入repository.doSomething();}
}

在这个例子中,init()方法会在所有依赖注入完成后被调用,因此可以安全地使用repository。

子类中的@PostConstruct

子类也可以定义自己的@PostConstruct方法,这些方法会在父类的@PostConstruct方法之后被调用:

public class ConcreteService extends AbstractService {@Autowiredprivate AnotherDependency anotherDependency;@PostConstructpublic void initChild() {// 父类的init()方法已经被调用// 可以安全地使用父类和子类的所有依赖getRepository().doSomething();anotherDependency.doSomethingElse();}
}

这种方式确保了所有的初始化逻辑都在依赖注入完成后执行,避免了NullPointerException的风险。

避免在构造器中使用ApplicationContext.getBean

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

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

错误示例

public abstract class AbstractService implements ApplicationContextAware {private ApplicationContext context;public AbstractService() {// 错误:此时context还是nullSomeBean someBean = context.getBean(SomeBean.class);}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.context = applicationContext;}
}

这段代码会抛出NullPointerException,因为在构造器执行时,setApplicationContext()方法还没有被调用。

正确做法

正确的做法是使用依赖注入,让Spring容器管理对象的创建和依赖关系:

public abstract class AbstractService {@Autowiredprivate SomeBean someBean;@PostConstructpublic void init() {// 正确:此时someBean已经被注入someBean.doSomething();}
}

这种方式不仅避免了NullPointerException,还降低了与Spring框架的耦合度,使代码更易于测试和维护。

最佳实践示例

让我们通过一个完整的例子来展示这些最佳实践:

@Service
public abstract class AbstractUserService {@Autowiredprivate UserRepository userRepository;@Autowiredprivate EmailService emailService;protected AbstractUserService() {// 构造器中不做任何依赖相关的操作}@PostConstructprotected void init() {// 初始化逻辑System.out.println("AbstractUserService initialized with " + userRepository.getClass().getSimpleName());}public User findUserById(Long id) {return userRepository.findById(id).orElse(null);}protected void sendEmail(User user, String message) {emailService.sendEmail(user.getEmail(), message);}// 抽象方法,由子类实现public abstract void processUser(User user);
}@Service
public class ConcreteUserService extends AbstractUserService {@Autowiredprivate SpecialProcessor specialProcessor;@PostConstructprotected void initChild() {System.out.println("ConcreteUserService initialized with " + specialProcessor.getClass().getSimpleName());}@Overridepublic void processUser(User user) {User processedUser = specialProcessor.process(user);sendEmail(processedUser, "Your account has been processed.");}
}// 使用示例
@RestController
@RequestMapping("/users")
public class UserController {@Autowiredprivate ConcreteUserService userService;@GetMapping("/{id}")public ResponseEntity<User> getUser(@PathVariable Long id) {User user = userService.findUserById(id);if (user != null) {userService.processUser(user);return ResponseEntity.ok(user);} else {return ResponseEntity.notFound().build();}}
}

在这个例子中:

  1. AbstractUserService​ 是一个抽象类,它定义了一些通用的用户服务逻辑。
  2. 依赖(UserRepository​ 和 EmailService​)通过 @Autowired​ 注入到抽象类中。
  3. 初始化逻辑放在 @PostConstruct​ 注解的 init()​ 方法中,确保在所有依赖注入完成后执行。
  4. ConcreteUserService​ 继承自 AbstractUserService​,并实现了抽象方法。
  5. ConcreteUserService​ 有自己的依赖(SpecialProcessor​)和初始化逻辑。
  6. UserController​ 中,我们注入并使用 ConcreteUserService​。

这个设计遵循了我们讨论的所有最佳实践:

  • 在抽象类中使用 @Autowired​ 注入依赖
  • 避免在构造器中使用注入的依赖
  • 使用 @PostConstruct​ 进行初始化
  • 不使用 ApplicationContext.getBean()

常见问题和解决方案

在使用抽象类和依赖注入时,开发者可能会遇到一些常见问题。以下是一些问题及其解决方案:

1. 循环依赖

问题:当两个类相互依赖时,可能会导致循环依赖问题。

解决方案:

  • 重新设计以消除循环依赖
  • 使用 @Lazy​ 注解来延迟其中一个依赖的初始化
  • 使用 setter 注入而不是构造器注入
@Service
public class ServiceA {private ServiceB serviceB;@Autowiredpublic void setServiceB(@Lazy ServiceB serviceB) {this.serviceB = serviceB;}
}@Service
public class ServiceB {@Autowiredprivate ServiceA serviceA;
}

2. 依赖注入在单元测试中的问题

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

解决方案:

  • 使用 Spring 的测试支持,如 @SpringBootTest
  • 为测试创建一个简化的配置类
  • 使用模拟框架如 Mockito 来模拟依赖
@SpringBootTest
class ConcreteUserServiceTest {@MockBeanprivate UserRepository userRepository;@Autowiredprivate ConcreteUserService userService;@Testvoid testFindUserById() {when(userRepository.findById(1L)).thenReturn(Optional.of(new User(1L, "Test User")));User user = userService.findUserById(1L);assertNotNull(user);assertEquals("Test User", user.getName());}
}

3. 属性注入vs构造器注入

问题:虽然属性注入(使用 @Autowired​ on fields)很方便,但它可能使得依赖关系不那么明显。

解决方案:考虑使用构造器注入,特别是对于必需的依赖。这使得依赖关系更加明确,并有助于创建不可变的服务。

@Service
public abstract class AbstractUserService {private final UserRepository userRepository;private final EmailService emailService;@Autowiredprotected AbstractUserService(UserRepository userRepository, EmailService emailService) {this.userRepository = userRepository;this.emailService = emailService;}// ... 其他方法
}@Service
public class ConcreteUserService extends AbstractUserService {private final SpecialProcessor specialProcessor;@Autowiredpublic ConcreteUserService(UserRepository userRepository, EmailService emailService,SpecialProcessor specialProcessor) {super(userRepository, emailService);this.specialProcessor = specialProcessor;}// ... 其他方法
}

这种方法的优点是:

  • 依赖关系更加明确
  • 有助于创建不可变的服务
  • 更易于单元测试

4. 抽象类中的 @Autowired 方法

问题:有时我们可能想在抽象类中有一个被 @Autowired 注解的方法,但这个方法在子类中被重写了。

解决方案:使用 @Autowired 注解抽象方法,并在子类中实现它。

public abstract class AbstractService {@Autowiredprotected abstract Dependencies getDependencies();@PostConstructpublic void init() {getDependencies().doSomething();}
}@Service
public class ConcreteService extends AbstractService {@Autowiredprivate Dependencies dependencies;@Overrideprotected Dependencies getDependencies() {return dependencies;}
}

这种方法允许子类控制依赖的具体实现,同时保持父类的通用逻辑。

5. 运行时依赖注入

问题:有时我们可能需要在运行时动态注入依赖,而不是在启动时。

解决方案:使用 ObjectProvider<T>​ 来延迟依赖的解析。

@Service
public abstract class AbstractDynamicService {@Autowiredprivate ObjectProvider<DynamicDependency> dependencyProvider;protected DynamicDependency getDependency() {return dependencyProvider.getIfAvailable();}// ... 其他方法
}

这种方法允许我们在需要时才解析依赖,这在某些场景下可能很有用,比如条件性的bean创建。

最佳实践总结

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

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

结论

在Spring Boot中使用抽象类和依赖注入是一种强大的技术,可以帮助我们创建灵活、可维护的代码。然而,它也带来了一些挑战,特别是在处理依赖注入的时机和方式上。

通过遵循本文讨论的最佳实践,我们可以避免常见的陷阱,充分利用Spring Boot提供的依赖注入功能。记住,关键是要理解Spring Bean的生命周期,合理使用 @PostConstruct 注解,避免在不适当的时候访问依赖,并选择适合你的项目的依赖注入方式。

最后,虽然这些是普遍认可的最佳实践,但每个项目都有其独特的需求。因此,始终要根据你的具体情况来调整这些实践。持续学习和实践是掌握Spring Boot中抽象类和依赖注入的关键。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/386558.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Hive之扩展函数(UDF)

Hive之扩展函数(UDF) 1、概念讲解 当所提供的函数无法解决遇到的问题时&#xff0c;我们通常会进行自定义函数&#xff0c;即&#xff1a;扩展函数。Hive的扩展函数可分为三种&#xff1a;UDF,UDTF,UDAF。 UDF&#xff1a;一进一出 UDTF&#xff1a;一进多出 UDAF&#xff1a…

作业帮6-19笔试-选填题

可以看到10在第一位&#xff0c;说明用的是挖坑法快速排序&#xff0c;过程如下&#xff1a; 右指针从最右边开始&#xff0c;找到第一个比30小的数10&#xff0c;与30交换。 10、15、40、28、50、30、70 左指针从位置1开始&#xff0c;找到40&#xff0c;与30互换。 10、15、3…

C语言 ——— 函数指针的定义 函数指针的使用

目录 何为函数指针 打印 函数名的地址 及 &函数名的地址 函数指针的代码&#xff08;如何正确存储函数地址&#xff09; 函数指针的使用 何为函数指针 类比&#xff1a; 整型指针 - 指向整型数据的指针&#xff0c;整型指针存放的是整型数据的地址 字符指针 - 指向字…

Lc63---1859将句子排序(排序)---Java版(未写完)

1.题目描述 2.思路 &#xff08;1&#xff09;首先将句子按空格分割成若干单词。 &#xff08;2&#xff09;每个单词的最后一个字符是它的位置索引。我们可以通过这个索引将单词恢复到正确的位置。 &#xff08;3&#xff09;按照单词的索引顺序排序这些单词。 &#xff08;4…

分布式搜索引擎ES--Elasticsearch集群

1.Elasticsearch集群的概念 分片机制&#xff1a;每个索引都可以被分片 索引my_doc只有一个主分片&#xff1b;索引shop有三个主分片&#xff1b;索引shop2有5个主分片;(参考前面案例) 每个主分片都包含索引的数据&#xff0c;由于目前是单机&#xff0c;所以副分片是没有的&a…

shardingsphere的学习(二):sharingjdbc操作读写分离

简介 mysql配置读写分离以及使用shardingjdbc配置操作读写分离 读写分离 主数据库负责增删改操作&#xff08;写&#xff09;&#xff0c;从数据库负责查询操作&#xff08;读&#xff09;&#xff0c;主数据库和从数据库之间会数据同步&#xff08;主从复制&#xff09;。 …

【前端】一文带你了解 CSS

文章目录 1. CSS 是什么2. CSS 引入方式2.1 内部样式2.2 外部样式2.3 内联样式 3. CSS 常见选择器3.1 基础选择器3.1.1 标签选择器3.1.2 类选择器3.1.3 id 选择器3.1.4 通配符选择器 3.2 复合选择器3.2.1 后代选择器 4. CSS 常用属性4.1 字体相关4.2 文本相关4.3 背景相关4.4 设…

敢不敢跟我一起搭建一个Agent!不写一行代码,10分钟搞出你的智能体!纯配置也能真正掌握AI最有潜力的技术?AI圈内人必备技能

说一千道一万&#xff0c;不如实地转一转。学了那么久的AI Agent的概念了&#xff0c;是时候该落地一个Agent看看自己的掌握程度了对不对&#xff0c;我们都理解大脑是自动节能的&#xff0c;但是知识的确需要倒逼自己一把才能真的掌握&#xff0c;不瞒大家说&#xff0c;笔者对…

论文阅读:面向自动驾驶场景的多目标点云检测算法

论文地址:面向自动驾驶场景的多目标点云检测算法 概要 点云在自动驾驶系统中的三维目标检测是关键技术之一。目前主流的基于体素的无锚框检测算法通常采用复杂的二阶段修正模块,虽然在算法性能上有所提升,但往往伴随着较大的延迟。单阶段无锚框点云检测算法简化了检测流程,…

日程管理多源归一,服务场景一键直达

时间对于每个人来说都是非常宝贵的&#xff0c;曾经我们使用台历、挂历来标记和查看重要日程&#xff0c;通过翻页来见证时光的流逝&#xff0c;随着信息化时代的不断发展&#xff0c;更加灵活简洁的电子日历成为主流&#xff0c;日历也从一个最简单的日期看板&#xff0c;慢慢…

正余弦算法作者又提出新算法!徒步优化算法(HOA)-2024年一区顶刊新算法-公式原理详解与性能测评 Matlab代码免费获取

声明&#xff1a;文章是从本人公众号中复制而来&#xff0c;因此&#xff0c;想最新最快了解各类智能优化算法及其改进的朋友&#xff0c;可关注我的公众号&#xff1a;强盛机器学习&#xff0c;不定期会有很多免费代码分享~ 目录 原理简介 算法伪代码 性能测评 参考文献 …

ruoyi vue3版本web端隐藏侧边栏及其顶部导航栏

做项目时有个需求是在web端里面嵌入一个页面全屏的大屏&#xff0c;但若依web自带的侧边栏导航和顶部导航一时还不知道怎么隐藏起来&#xff0c;于是在网上到处查找资料&#xff0c;终于&#xff0c;还是在若依的gitee文档中发现了线索 怎么隐藏侧边栏和顶部导航栏实现完全的全…

从数据时代到智能时代,星环科技信雅达联合发布金融全栈解决方案

近年来&#xff0c;星环科技与信雅达在金融行业的多个关键领域展开了广泛而深入的合作&#xff0c;推出了一系列面向金融科技领域的联合解决方案。此次合作基于星环科技在大数据、人工智能和云计算领域的先进技术&#xff0c;以及信雅达在金融领域的深厚积累&#xff0c;围绕数…

C语言程序设计(二)

四.找素数 素数&#xff1a;除了1和它本身不再有其他因数的自然数。换句话说&#xff1a;一个大于1的自然数 &#xff0c;如果只能被1和它本身整除&#xff0c;那就是素数&#xff08;质数&#xff09;。 在打印中遇到的问题就是&#xff0c;知道怎么写却总是运行不起来。主要…

Python missingno和Vaex库:高性能的大数据分析

在数据分析和处理过程中&#xff0c;数据缺失是常见的问题。处理和理解数据缺失情况是确保数据质量和分析准确性的关键步骤。Python的missingno库提供了一种便捷且直观的方式来可视化数据缺失情况&#xff0c;从而帮助我们更好地理解和处理缺失值。本文将详细介绍missingno库的…

一文看懂:数据产品的3种输出形式和4大服务层次

企业要想提升数据资产的价值&#xff0c;就必须了解数据产品。那么&#xff0c;什么是数据产品&#xff0c;我们该如何认识它&#xff1f; 在由WakeData惟客数据联合星光数智推出的直播栏目《星光对话》第5期中&#xff0c;星光数智首席数据架构师魏战松&#xff0c;分享了对于…

分布式事务解决方案(一) 2PC、3PC、TCC、Sega

目录 1.绪论 2.2PC 2.1 基本原理 2.1.1 组成 2.1.2 步骤 1.prepare阶段 2.commit阶段 2.2 2PC 存在的问题 2.2.1 阻塞问题 2.2.2 单点故障问题 1. 事务协调器宕机 2.部分数据不一致问题 2.资源管理器宕机 3. 事务协调器和资源管理管理器同时宕机 2.2 实现 2.2.1…

JCR一区级 | Matlab实现SO-Transformer-LSTM多变量回归预测(蛇群算法优化)

JCR一区级 | Matlab实现SO-Transformer-LSTM多变量回归预测&#xff08;蛇群算法优化&#xff09; 目录 JCR一区级 | Matlab实现SO-Transformer-LSTM多变量回归预测&#xff08;蛇群算法优化&#xff09;效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.【JCR一区级】M…

跨网段 IP 地址通信故障分析

现如今计算机网络的规模和复杂性不断增加&#xff0c;跨网段通信成为网络运行中的常见需求。但如果设备处于不同网段且路由设置出现偏差时就会导致通信故障&#xff0c;严重影响网络的正常运行和数据传输。 1.跨网段通信的基本原理 跨网段通信依赖于路由器的路由功能。路由器根…