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行出现问题。

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

实例

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

1

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

接口

    /**
     * 更新作业分数
     * @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方式。

SpringMVC空指针异常NullPointerException的原因和解决方法_第1张图片

对于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();

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

2

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

接口

    /**
     * 更新作业分数
     * @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 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 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:  but was: 
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失效了,于是导致之前本来能通过的单元测试报错了。

SpringMVC空指针异常NullPointerException的原因和解决方法_第2张图片

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

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

解决方法:

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

总结

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

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

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

你可能感兴趣的:(java,单元测试,spring,后端)