SpringMVC空指针异常NullPointerException解决及原理解析

 更新时间:2023年08月10日 11:48:37   作者:LYX6666  
这篇文章主要介绍了SpringMVC空指针异常NullPointerException解决及原理解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

前言

在写单元测试的过程中,出现过许多次java.lang.NullPointerException,而这些空指针的错误又是不同原因造成的,本文从实际代码出发,研究一下空指针的产生原因。

一句话概括:空指针异常,是在程序在调用某个对象的某个方法时,由于该对象为null产生的

所以如果出现此异常,大多数情况要判断测试中的对象是否被成功的注入,以及Mock方法是否生效

基础

出现空指针异常的错误信息如下:

java.lang.NullPointerException
    at club.yunzhi.workhome.service.WorkServiceImpl.updateOfCurrentStudent(WorkServiceImpl.java:178)
    at club.yunzhi.workhome.service.WorkServiceImplTest.updateOfCurrentStudent(WorkServiceImplTest.java:137)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

这实际上是方法栈,就是在WorkServiceImplTest.java测试类的137行调用WorkServiceImpl.java被测试类的178行出现问题。

下面从两个实例来具体分析。

实例

(代码仅为了报错时方便分析,请勿仔细阅读,避免浪费时间)

目的:测试服务层的一个用于更新作业的功能。

接口

/**
     * 更新作业分数
     * @param id
     * @param score
     * @return 
     */
    Work updateScore(Long id, int score);

接口实现:

@Service
public class WorkServiceImpl implements WorkService {
    private static final Logger logger = LoggerFactory.getLogger(WorkServiceImpl.class);
    private static final String WORK_PATH = "work/";
    final WorkRepository workRepository;
    final StudentService studentService;
    final UserService userService;
    final ItemRepository itemRepository;
    final AttachmentService attachmentService;
    public WorkServiceImpl(WorkRepository workRepository, StudentService studentService, UserService userService, ItemRepository itemRepository, AttachmentService attachmentService) {
        this.workRepository = workRepository;
        this.studentService = studentService;
        this.userService = userService;
        this.itemRepository = itemRepository;
        this.attachmentService = attachmentService;
    }
    ...
    @Override
    public Work updateScore(Long id, int score) {
        Work work = this.workRepository.findById(id)
                .orElseThrow(() -> new ObjectNotFoundException("未找到ID为" + id + "的作业"));
        if (!this.isTeacher()) {
            throw new AccessDeniedException("无权判定作业");
        }
        work.setScore(score);
        logger.info(String.valueOf(work.getScore()));
        return this.save(work);
    }
    @Override
    public boolean isTeacher() {
        User user = this.userService.getCurrentLoginUser();
 130    if (user.getRole() == 1) {
            return false;
        }
        return true;
    }

测试:

@Test
    public void updateScore() {
        Long id = this.random.nextLong();
        Work oldWork = new Work();
        oldWork.setStudent(this.currentStudent);
        oldWork.setItem(Mockito.spy(new Item()));
        int score = 100;

        Mockito.when(this.workRepository.findById(Mockito.eq(id)))
                .thenReturn(Optional.of(oldWork));

        Mockito.doReturn(true)
                .when(oldWork.getItem())
                .getActive();

        Work work = new Work();
        work.setScore(score);


        Work resultWork = new Work();
        Mockito.when(this.workRepository.save(Mockito.eq(oldWork)))
                .thenReturn(resultWork);

 203    Assertions.assertEquals(resultWork, this.workService.updateScore(id, score));
        Assertions.assertEquals(oldWork.getScore(), work.getScore());

    }

运行测试,出现空指针:java.lang.NullPointerException

at club.yunzhi.workhome.service.WorkServiceImpl.isTeacher(WorkServiceImpl.java:130)
at club.yunzhi.workhome.service.WorkServiceImplTest.updateScore(WorkServiceImplTest.java:203)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

问题出在功能代码的第130行,可以看到报错的代码根本不是要测试的方法,而是被调用的方法。
再看测试代码的203行,测试时的本来目的是为了Mock掉这个方法,但使用的是when().thenReturn方式。

对于Mock对象(完全假的对象),使用when().thenReturndoReturn().when的效果是一样的,都可以制造一个假的返回值。

但是对于Spy对象(半真半假的对象)就不一样了,when().thenReturn会去执行真正的方法,再返回假的返回值,在这个执行真正方法的过程中,就可能出现空指针错误。

doReturn().when会直接返回假的数据,而根本不执行真正的方法。

参考链接:https://sangsoonam.github.io/...

所以把测试代码的改成:

-    Mockito.when(this.workService.isTeacher()).thenReturn(true);
   +    Mockito.doReturn(true).when(workService).isTeacher();

再次运行,就能通过测试。

目的:还是测试之前的方法,只不过新增了功能。

接口

/**
     * 更新作业分数
     * @param id
     * @param score
     * @return 
     */
    Work updateScore(Long id, int score);

接口实现(在原有的储存学生成绩方法上新增了计算总分的功能)

@Override
    public Work updateScore(Long id, int score) {
        Work work = this.workRepository.findById(id)
                .orElseThrow(() -> new ObjectNotFoundException("未找到ID为" + id + "的作业"));
        if (!this.isTeacher()) {
            throw new AccessDeniedException("无权判定作业");
        }
        work.setScore(score);
        work.setReviewed(true);
        logger.info(String.valueOf(work.getScore()));
   +    //取出此学生的所有作业
   +    List<Work> currentStudentWorks = this.workRepository.findAllByStudent(work.getStudent());
   +    //取出此学生
   +    Student currentStudent = this.studentService.findById(work.getStudent().getId());
   +    currentStudent.setTotalScore(0);
   +    int viewed = 0;
   +
   +    for (Work awork : currentStudentWorks) {
   +        if (awork.getReviewed() == true) {
   +            viewed++;
   +            //计算总成绩
   +            currentStudent.setTotalScore(currentStudent.getTotalScore()+awork.getScore());
   +            //计算平均成绩
   +            currentStudent.setAverageScore(currentStudent.getTotalScore()/viewed);
   +        }
   +    }
   +
   +    studentRepository.save(currentStudent);
        return this.save(work);
    }

由于出现了对学生仓库studentRepository的调用,需要注入:

final WorkRepository workRepository;
    final StudentService studentService;
    final UserService userService;
    final ItemRepository itemRepository;
    final AttachmentService attachmentService;
   +final StudentRepository studentRepository;

   -public WorkServiceImpl(WorkRepository workRepository, StudentService studentService, UserService userService, ItemRepository itemRepository, AttachmentService attachmentService) {
   +public WorkServiceImpl(WorkRepository workRepository, StudentService studentService, UserService userService, ItemRepository itemRepository, AttachmentService attachmentService, StudentRepository studentRepository) {
        this.workRepository = workRepository;
        this.studentService = studentService;
        this.userService = userService;
        this.itemRepository = itemRepository;
        this.attachmentService = attachmentService;
   +    this.studentRepository = studentRepository;
    }

然后是测试代码

class WorkServiceImplTest extends ServiceTest {
    private static final Logger logger = LoggerFactory.getLogger(WorkServiceImplTest.class);
    WorkRepository workRepository;
    UserService userService;
    ItemRepository itemRepository;
    ItemService itemService;
    WorkServiceImpl workService;
    AttachmentService attachmentService;
   +StudentService studentService;
   +StudentRepository studentRepository;
    @Autowired
    private ResourceLoader loader;
    @BeforeEach
    public void beforeEach() {
        super.beforeEach();
        this.itemService = Mockito.mock(ItemService.class);
        this.workRepository = Mockito.mock(WorkRepository.class);
        this.userService = Mockito.mock(UserService.class);
        this.itemRepository = Mockito.mock(ItemRepository.class);
        this.studentService = Mockito.mock(StudentService.class);
        this.studentRepository = Mockito.mock(StudentRepository.class);
        this.workService = Mockito.spy(new WorkServiceImpl(this.workRepository, this.studentService,
   +            this.userService, this.itemRepository, this.attachmentService, this.studentRepository));
    }
    ...
    @Test
    public void updateScore() {
        Long id = this.random.nextLong();
        Work oldWork = new Work();
        oldWork.setScore(0);
        oldWork.setStudent(this.currentStudent);
        oldWork.setItem(Mockito.spy(new Item()));
   +    Work testWork = new Work();
   +    testWork.setScore(0);
   +    testWork.setReviewed(true);
   +    testWork.setStudent(this.currentStudent);
   +    testWork.setItem(Mockito.spy(new Item()));
        int score = 100;
   +    List<Work> works= Arrays.asList(oldWork, testWork);
   +
   +    Mockito.doReturn(Optional.of(oldWork))
   +            .when(this.workRepository)
   +            .findById(Mockito.eq(id));
   +    Mockito.doReturn(works)
   +            .when(this.workRepository)
   +            .findAllByStudent(oldWork.getStudent());
        Mockito.doReturn(true)
                .when(oldWork.getItem())
                .getActive();
   +    Mockito.doReturn(this.currentStudent)
   +            .when(this.studentService)
                .findById(oldWork.getStudent().getId());
        Work work = new Work();
        work.setScore(score);
        work.setReviewed(true);
        Work resultWork = new Work();
        Mockito.when(this.workRepository.save(Mockito.eq(oldWork)))
                .thenReturn(resultWork);
        Mockito.doReturn(true).when(workService).isTeacher();
        Assertions.assertEquals(resultWork, this.workService.updateScore(id, score));
        Assertions.assertEquals(oldWork.getScore(), work.getScore());
        Assertions.assertEquals(oldWork.getReviewed(),work.getReviewed());
   +    Assertions.assertEquals(oldWork.getStudent().getTotalScore(), 100);
   +    Assertions.assertEquals(oldWork.getStudent().getAverageScore(), 50);
    }
    ...
}

顺利通过测试,看似没什么问题,可是一跑全局单元测试,就崩了。

[ERROR] Failures: 
492[ERROR]   WorkServiceImplTest.saveWorkByItemIdOfCurrentStudent:105 expected: <club.yunzhi.workhome.entity.Student@1eb207c3> but was: <null>
493[ERROR] Errors: 
494[ERROR]   WorkServiceImplTest.getByItemIdOfCurrentStudent:73 » NullPointer
495[ERROR]   WorkServiceImplTest.updateOfCurrentStudent:138 » NullPointer
496[INFO] 
497[ERROR] Tests run: 18, Failures: 1, Errors: 2, Skipped: 0

一个断言错误,两个空指针错误。

可是这些三个功能我根本就没有改,而且是之前已经通过测试的功能,为什么会出错呢?

拿出一个具体的错误,从本地跑一下测试:

测试代码

@Test
    public void updateOfCurrentStudent() {
        Long id = this.random.nextLong();
        Work oldWork = new Work();
        oldWork.setStudent(this.currentStudent);
        oldWork.setItem(Mockito.spy(new Item()));
        Mockito.when(this.workRepository.findById(Mockito.eq(id)))
                .thenReturn(Optional.of(oldWork));
        //Mockito.when(this.studentService.getCurrentStudent()).thenReturn(this.currentStudent);
        Mockito.doReturn(true)
                .when(oldWork.getItem())
                .getActive();
        Work work = new Work();
        work.setContent(RandomString.make(10));
        work.setAttachments(Arrays.asList(new Attachment()));
        Work resultWork = new Work();
        Mockito.when(this.workRepository.save(Mockito.eq(oldWork)))
                .thenReturn(resultWork);
 137    Assertions.assertEquals(resultWork, this.workService.updateOfCurrentStudent(id, work));
        Assertions.assertEquals(oldWork.getContent(), work.getContent());
        Assertions.assertEquals(oldWork.getAttachments(), work.getAttachments());
    }

功能代码

@Override
    public Work updateOfCurrentStudent(Long id, @NotNull Work work) {
        Assert.notNull(work, "更新的作业实体不能为null");
        Work oldWork = this.workRepository.findById(id)
                .orElseThrow(() -> new ObjectNotFoundException("未找到ID为" + id + "的作业"));
 178    if (!oldWork.getStudent().getId().equals(this.studentService.getCurrentStudent().getId())) {
            throw new AccessDeniedException("无权更新其它学生的作业");
        }
        if (!oldWork.getItem().getActive()) {
            throw new ValidationException("禁止提交已关闭的实验作业");
        }
        oldWork.setContent(work.getContent());
        oldWork.setAttachments(work.getAttachments());
        return this.workRepository.save(oldWork);
    }

报错信息java.lang.NullPointerException

at club.yunzhi.workhome.service.WorkServiceImpl.updateOfCurrentStudent(WorkServiceImpl.java:178)
at club.yunzhi.workhome.service.WorkServiceImplTest.updateOfCurrentStudent(WorkServiceImplTest.java:137)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

根据报错信息来看,是测试类在调用功能代码178行时,出现了空指针,
经过分析,在执行this.studentService.getCurrentStudent().getId()时出现的。

然后就来判断studentService的注入情况,

//父类的BeforeEach
    public void beforeEach() {
        this.studentService = Mockito.mock(StudentService.class);
        this.currentStudent.setId(this.random.nextLong());
        Mockito.doReturn(currentStudent)
                .when(this.studentService)
                .getCurrentStudent();
    }
//测试类的BeforeEach
    @BeforeEach
    public void beforeEach() {
        super.beforeEach();
        this.itemService = Mockito.mock(ItemService.class);
        this.workRepository = Mockito.mock(WorkRepository.class);
        this.userService = Mockito.mock(UserService.class);
        this.itemRepository = Mockito.mock(ItemRepository.class);
        this.studentService = Mockito.mock(StudentService.class);
        this.studentRepository = Mockito.mock(StudentRepository.class);
        this.workService = Mockito.spy(new WorkServiceImpl(this.workRepository, this.studentService,
                this.userService, this.itemRepository, this.attachmentService, this.studentRepository));
    }

问题就出在这里,由于测试类执行了继承,父类已经Mock了一个studentService并且成功的设定了Moockito的返回值,但测试类又进行了一次赋值,这就使得父类的Mock失效了,于是导致之前本来能通过的单元测试报错了。

所以本实例的根本问题是,重复注入了对象

这导致了原有的mock方法被覆盖,以至于执行了真实的studentService中的方法,返回了空的学生。

解决方法:

  • 在测试类WorkServiceImplTest中删除studentService的注入,使用父类。
  • 使用子类的studentService,并在所有的报错位置,加入对应的mock方法

总结

java.lang.NullPointerException直接翻译过来是空指针,但根本原因却不是空对象,一定是由于某种错误的操作(错误的注入),导致了空对象。

最常见的情况,就是在测试时执行了真正的方法,而不是mock方法。
此时的解决方案,就是检查所有的依赖注入和Mock是否完全正确,如果正确,就不会出现空指针异常了。

最根本的办法,还是去分析,找到谁是那个空对象,问题就迎刃而解。

以上就是SpringMVC空指针异常NullPointerException解决及原理解析的详细内容,更多关于SpringMVC空指针异常解决的资料请关注脚本之家其它相关文章!

相关文章

  • Java深入分析与解决Top-K问题

    Java深入分析与解决Top-K问题

    TopK问题即在N个数中找出最大的前K个,这篇文章将详细讲解三种方法解决TopK问题,文中代码具有一定参考价值,快跟随小编一起学习一下吧
    2022-04-04
  • Java Structs框架原理案例详解

    Java Structs框架原理案例详解

    这篇文章主要介绍了Java Structs框架原理案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08
  • java如何用反射将一个对象复制给另一个对象

    java如何用反射将一个对象复制给另一个对象

    这篇文章主要介绍了java如何用反射将一个对象复制给另一个对象问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-09-09
  • 详解spring batch的使用和定时器Quart的使用

    详解spring batch的使用和定时器Quart的使用

    spring Batch是一个基于Spring的企业级批处理框架,它通过配合定时器Quartz来轻易实现大批量的数据读取或插入,并且全程自动化,无需人员管理
    2017-08-08
  • jedis的testWhileIdle用法源码解读

    jedis的testWhileIdle用法源码解读

    这篇文章主要为大家介绍了jedis的testWhileIdle用法源码解读,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • java字符串反转示例分享

    java字符串反转示例分享

    这篇文章主要介绍了将一个字符串进行反转或者字符串中指定部分进行反转的方法,大家参考使用吧
    2014-01-01
  • Java DirectByteBuffer堆外内存回收详解

    Java DirectByteBuffer堆外内存回收详解

    这篇文章主要为大家详细介绍了Java中发DirectByteBuffer堆外内存回收,文中的示例代码讲解详细,具有一定的借鉴价值,需要的可以参考一下
    2022-10-10
  • Java 8 开发的 Mybatis 注解代码生成工具

    Java 8 开发的 Mybatis 注解代码生成工具

    MybatisAnnotationTools 是基于 Java8 开发的一款可以用于自动化生成 MyBatis 注解类的工具,支持配置数据源、类路径,表名去前缀、指定类名前后缀等功能.这篇文章主要介绍了Java 8 开发的 Mybatis 注解代码生成工具 ,需要的朋友可以参考下
    2019-07-07
  • SpringBoot 使用WebSocket功能(实现步骤)

    SpringBoot 使用WebSocket功能(实现步骤)

    本文通过详细步骤介绍了SpringBoot 使用WebSocket功能,首先需要导入WebSocket坐标,编写WebSocket配置类,用于注册WebSocket的Bean,结合示例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧
    2024-02-02
  • Java实现特定范围的完数输出算法示例

    Java实现特定范围的完数输出算法示例

    这篇文章主要介绍了Java实现特定范围的完数输出算法,简单说明了完数的概念、计算原理并结合实例形式分析了java针对给定范围内的完数输出操作实现技巧,需要的朋友可以参考下
    2017-12-12

最新评论