SpringMVC空指針異常NullPointerException解決及原理解析
前言
在寫單元測試的過程中,出現(xiàn)過許多次java.lang.NullPointerException,而這些空指針的錯誤又是不同原因造成的,本文從實(shí)際代碼出發(fā),研究一下空指針的產(chǎn)生原因。
一句話概括:空指針異常,是在程序在調(diào)用某個對象的某個方法時,由于該對象為null產(chǎn)生的。
所以如果出現(xiàn)此異常,大多數(shù)情況要判斷測試中的對象是否被成功的注入,以及Mock方法是否生效。
基礎(chǔ)
出現(xiàn)空指針異常的錯誤信息如下:
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)這實(shí)際上是方法棧,就是在WorkServiceImplTest.java測試類的137行調(diào)用WorkServiceImpl.java被測試類的178行出現(xiàn)問題。
下面從兩個實(shí)例來具體分析。
實(shí)例
(代碼僅為了報錯時方便分析,請勿仔細(xì)閱讀,避免浪費(fèi)時間)
目的:測試服務(wù)層的一個用于更新作業(yè)的功能。
接口
/**
* 更新作業(yè)分?jǐn)?shù)
* @param id
* @param score
* @return
*/
Work updateScore(Long id, int score);接口實(shí)現(xiàn):
@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 + "的作業(yè)"));
if (!this.isTeacher()) {
throw new AccessDeniedException("無權(quán)判定作業(yè)");
}
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());
}運(yùn)行測試,出現(xiàn)空指針: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行,可以看到報錯的代碼根本不是要測試的方法,而是被調(diào)用的方法。
再看測試代碼的203行,測試時的本來目的是為了Mock掉這個方法,但使用的是when().thenReturn方式。

對于Mock對象(完全假的對象),使用when().thenReturn和doReturn().when的效果是一樣的,都可以制造一個假的返回值。
但是對于Spy對象(半真半假的對象)就不一樣了,when().thenReturn會去執(zhí)行真正的方法,再返回假的返回值,在這個執(zhí)行真正方法的過程中,就可能出現(xiàn)空指針錯誤。
而doReturn().when會直接返回假的數(shù)據(jù),而根本不執(zhí)行真正的方法。
參考鏈接:https://sangsoonam.github.io/...
所以把測試代碼的改成:
- Mockito.when(this.workService.isTeacher()).thenReturn(true); + Mockito.doReturn(true).when(workService).isTeacher();
再次運(yùn)行,就能通過測試。
目的:還是測試之前的方法,只不過新增了功能。
接口
/**
* 更新作業(yè)分?jǐn)?shù)
* @param id
* @param score
* @return
*/
Work updateScore(Long id, int score);接口實(shí)現(xiàn)(在原有的儲存學(xué)生成績方法上新增了計算總分的功能)
@Override
public Work updateScore(Long id, int score) {
Work work = this.workRepository.findById(id)
.orElseThrow(() -> new ObjectNotFoundException("未找到ID為" + id + "的作業(yè)"));
if (!this.isTeacher()) {
throw new AccessDeniedException("無權(quán)判定作業(yè)");
}
work.setScore(score);
work.setReviewed(true);
logger.info(String.valueOf(work.getScore()));
+ //取出此學(xué)生的所有作業(yè)
+ List<Work> currentStudentWorks = this.workRepository.findAllByStudent(work.getStudent());
+ //取出此學(xué)生
+ 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);
}由于出現(xiàn)了對學(xué)生倉庫studentRepository的調(diào)用,需要注入:
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
一個斷言錯誤,兩個空指針錯誤。
可是這些三個功能我根本就沒有改,而且是之前已經(jīng)通過測試的功能,為什么會出錯呢?
拿出一個具體的錯誤,從本地跑一下測試:
測試代碼
@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, "更新的作業(yè)實(shí)體不能為null");
Work oldWork = this.workRepository.findById(id)
.orElseThrow(() -> new ObjectNotFoundException("未找到ID為" + id + "的作業(yè)"));
178 if (!oldWork.getStudent().getId().equals(this.studentService.getCurrentStudent().getId())) {
throw new AccessDeniedException("無權(quán)更新其它學(xué)生的作業(yè)");
}
if (!oldWork.getItem().getActive()) {
throw new ValidationException("禁止提交已關(guān)閉的實(shí)驗作業(yè)");
}
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)
根據(jù)報錯信息來看,是測試類在調(diào)用功能代碼178行時,出現(xiàn)了空指針,
經(jīng)過分析,在執(zhí)行this.studentService.getCurrentStudent().getId()時出現(xiàn)的。
然后就來判斷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));
}問題就出在這里,由于測試類執(zhí)行了繼承,父類已經(jīng)Mock了一個studentService并且成功的設(shè)定了Moockito的返回值,但測試類又進(jìn)行了一次賦值,這就使得父類的Mock失效了,于是導(dǎo)致之前本來能通過的單元測試報錯了。

所以本實(shí)例的根本問題是,重復(fù)注入了對象。
這導(dǎo)致了原有的mock方法被覆蓋,以至于執(zhí)行了真實(shí)的studentService中的方法,返回了空的學(xué)生。
解決方法:
- 在測試類WorkServiceImplTest中刪除studentService的注入,使用父類。
- 使用子類的studentService,并在所有的報錯位置,加入對應(yīng)的mock方法
總結(jié)
java.lang.NullPointerException直接翻譯過來是空指針,但根本原因卻不是空對象,一定是由于某種錯誤的操作(錯誤的注入),導(dǎo)致了空對象。
最常見的情況,就是在測試時執(zhí)行了真正的方法,而不是mock方法。
此時的解決方案,就是檢查所有的依賴注入和Mock是否完全正確,如果正確,就不會出現(xiàn)空指針異常了。
最根本的辦法,還是去分析,找到誰是那個空對象,問題就迎刃而解。
以上就是SpringMVC空指針異常NullPointerException解決及原理解析的詳細(xì)內(nèi)容,更多關(guān)于SpringMVC空指針異常解決的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何在SpringBoot項目中集成SpringSecurity進(jìn)行權(quán)限管理
在本文中,我們將討論如何在Spring?Boot項目中集成權(quán)限管理,我們將使用Spring?Security框架,這是一個專門用于實(shí)現(xiàn)安全性功能的框架,包括認(rèn)證和授權(quán),需要的朋友可以參考下2023-07-07
OpenTelemetry初識及調(diào)用鏈Trace詳解
這篇文章主要為為大家介紹了OpenTelemetry初識及調(diào)用鏈Trace詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
mybatis實(shí)現(xiàn)對數(shù)據(jù)的增刪查改實(shí)例詳解
這篇文章主要介紹了mybatis實(shí)現(xiàn)對數(shù)據(jù)的增刪查改實(shí)例詳解的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-07-07
MybatisPlus公共字段自動注入方法實(shí)現(xiàn)
本文介紹了MybatisPlus公共字段自動注入方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-08-08
Java中兩個大數(shù)之間的相關(guān)運(yùn)算及BigInteger代碼示例
這篇文章主要介紹了Java中兩個大數(shù)之間的相關(guān)運(yùn)算及BigInteger代碼示例,通過biginteger類實(shí)現(xiàn)大數(shù)的運(yùn)算代碼,具有一定參考價值,需要的朋友可以了解下。2017-11-11

