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.mzph.cn/news/875250.shtml

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

相关文章

MongoDB教程(二十一):MongoDB大文件存储GridFS

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; 文章目录 引言一、GridFS…

学习笔记(数据结构:链表 栈)3

fun为回调函数由主函数决定 栈: 栈是限定仅在表尾进行插入和删除操作的线性表。 先进后出、后进先出 栈顶:允许操作的一端 栈底:不允许操作的一端 入栈&#xff0c;出栈。 顺序栈 链式栈 1.创建 CreateSeqStack 2.销毁 DestroySeqStack 3.判断是否为空栈 IsEm…

AD7606采集控制

过采样&#xff1a;其效果就是在ADC之后增加数字滤波器的功能

观测云加入华为云「新加坡云联盟」,引领亚太创新潮流

7月19日&#xff0c;「新加坡云联盟」在华为云新加坡峰会2024上正式宣告成立。这一创新的联合不仅是新加坡乃至整个亚太地区云服务领域的重要里程碑&#xff0c;更是全球数字化转型进程中的关键一步。 观测云作为联盟的首批成员之一&#xff0c;在成立仪式上精彩亮相&#xff0…

音视频入门基础:PCM专题(3)——使用Audacity工具分析PCM音频文件

音视频入门基础&#xff1a;PCM专题系列文章&#xff1a; 音视频入门基础&#xff1a;PCM专题&#xff08;1&#xff09;——使用FFmpeg命令生成PCM音频文件并播放 音视频入门基础&#xff1a;PCM专题&#xff08;2&#xff09;——使用Qt播放PCM音频文件 音视频入门基础&am…

Centos7下安装配置最新版本Jenkins

1、基础环境配置 1.1 服务器下载Jenkins安装包 下载地址&#xff1a;Download and deploy 下载命令&#xff1a;wget https://get.jenkins.io/war-stable/2.452.3/jenkins.war 1.2 服务器安装配置JDK Jenkins 是基于 Java 语言开发的&#xff0c;因此需要 Java 运行环境支…

【Python】基础学习技能提升代码样例1:简单句式

官方教程 一、简单句式 1.1 数值 # 校验输入是否为有效数字 if not math.isnan(num):xxxx # 浮点数精度问题 print(0.1 0.2) # 0.30000000000000004 0.1 0.1 0.1 0.3 # false math.isclose(0.1 0.1 0.1, 0.3) # true round(0.1 0.1 0.1, ndigits2) round(0.3, n…

MQ传递用户信息

theme: nico 你们好&#xff0c;我是金金金。 场景 购物车里面有5个商品&#xff0c;用户勾选了并且提交订单了&#xff0c;此时需要删除购物车对应勾选的商品&#xff0c;mq的话涉及到传递用户信息~因为删除对应的购物车商品是需要传递用户信息来知晓对应用户的 生产者 消费者…

LeetCode 热题 HOT 100 (010/100)【宇宙最简单版】

【链表】No. 0206 反转链表 【简单】&#x1f449;力扣对应题目指路 希望对你有帮助呀&#xff01;&#xff01;&#x1f49c;&#x1f49c; 如有更好理解的思路&#xff0c;欢迎大家留言补充 ~ 一起加油叭 &#x1f4a6; 欢迎关注、订阅专栏 【力扣详解】谢谢你的支持&#xf…

《操作系统》(学习笔记)(王道)

一、计算机系统概述 1.1 操作系统的基本概念 1.1.1 操作系统的概念 操作系统&#xff08;OperatinggSystem&#xff0c;OS&#xff09;是指控制和管理整个计算机系统的硬件与软件资源&#xff0c;合理地组织、调度计算机的工作与资源的分配&#xff0c;进而为用户和其他软件…

docker安装oracle11g

前言 只从docker国内被ban了之后&#xff0c;oracle的docker镜像就不好找了&#xff0c;这里用一个我亲身经历的例子来举例 安装 # 这个是我的docker 镜像的配置文件"registry-mirrors": ["https://6kx4zyno.mirror.aliyuncs.com","https://hub-mi…

Kotlin 中 标准库函数

在 Kotlin 中&#xff0c;标准库提供了许多实用的函数&#xff0c;这些函数可以帮助简化代码、提高效率&#xff0c;以下是一些常用的标准库函数及其功能&#xff1a; let: let 函数允许你在对象上执行一个操作&#xff0c;并返回结果。它通常与安全调用操作符 ?. 一起使用&a…

-XX:MaxDirectMemorySize和-Dio.netty.maxDirectMemory区别

-XX:MaxDirectMemorySize是java运行参数&#xff0c;用户控制java程序可以使用的最大直接内存&#xff08;堆外/本地&#xff09;&#xff1b; -Dio.netty.maxDirectMemory是netty运行参数&#xff0c;用户控制netty程序可以使用的最大直接内存&#xff08;堆外/本地&#xff…

DP学习——外观模式

学而时习之&#xff0c;温故而知新。 外观模式 角色 2个角色&#xff0c;外观类&#xff0c;子系统类。 个人理解 感觉就是对外接口封装&#xff0c;这个是封装一个功能的对外接口&#xff0c;越简单越好&#xff0c;提供给第三方用。 应用场景 封装为对外库时&#xff…

SQL labs-SQL注入(四,sqlmap对于post传参方式的注入)

本文仅作为学习参考使用&#xff0c;本文作者对任何使用本文进行渗透攻击破坏不负任何责任。 序言&#xff1a;本文主要讲解基于SQL labs靶场&#xff0c;sqlmap工具进行的post传参方式的SQL注入。 传参方式有两类&#xff0c;一类是直接在url栏内进行url编码后进行的传参&am…

反序列化-极客大挑战2019php【I have a cat!】

知道这个题考的是反序列化&#xff0c;那么我们第一反应该拿到他的源码。 根据这句话判断【因为每次猫猫都在我键盘上乱跳&#xff0c;所以我有一个良好的备份网站的习惯 不愧是我&#xff01;&#xff01;&#xff01; 】说明有目录 我们直接使用dir开扫&#xff0c;发现有压…

【Vue3】watch 监视 reactive 定义的数据

【Vue3】watch 监视 reactive 定义的数据 背景简介开发环境开发步骤及源码总结 背景 随着年龄的增长&#xff0c;很多曾经烂熟于心的技术原理已被岁月摩擦得愈发模糊起来&#xff0c;技术出身的人总是很难放下一些执念&#xff0c;遂将这些知识整理成文&#xff0c;以纪念曾经…

【笔记:3D航路规划算法】一、RRT

目录 关键概念3D路径规划算法1. A*算法2. RRT1. 初始化&#xff1a;2. 实例化搜索算法&#xff1a;3. 路径生成&#xff1a;4. 绘制图像&#xff1a; 3D路径规划是在三维空间中寻找从起点到终点的最短或最优路径的一种技术。它广泛应用于无人机导航、机器人运动规划、虚拟现实等…

springboot 缓存预热的几种方案

缓存预热是指在 Spring Boot 项目启动时&#xff0c;预先将数据加载到缓存系统&#xff08;如 Redis&#xff09;中的一种机制。 这里我给大家总结几个缓存预热的方案。 方案1&#xff1a;使用启动监听事件实现缓存预热 可以使用 ApplicationListener 监听 ContextRefreshed…

生成树协议配置与分析

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 一、相关知识 1、生成树协议简介 生成树协议&#xff08;STP&#xff09;是一种避免数据链路层逻辑环路的机制&#xff0c;它通过信息交互识别环路并…