SpringMVC空指針異常NullPointerException解決及原理解析
前言
在寫單元測(cè)試的過(guò)程中,出現(xiàn)過(guò)許多次java.lang.NullPointerException,而這些空指針的錯(cuò)誤又是不同原因造成的,本文從實(shí)際代碼出發(fā),研究一下空指針的產(chǎn)生原因。
一句話概括:空指針異常,是在程序在調(diào)用某個(gè)對(duì)象的某個(gè)方法時(shí),由于該對(duì)象為null產(chǎn)生的。
所以如果出現(xiàn)此異常,大多數(shù)情況要判斷測(cè)試中的對(duì)象是否被成功的注入,以及Mock方法是否生效。
基礎(chǔ)
出現(xiàn)空指針異常的錯(cuò)誤信息如下:
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
測(cè)試類的137行調(diào)用WorkServiceImpl.java
被測(cè)試類的178行出現(xiàn)問(wèn)題。
下面從兩個(gè)實(shí)例來(lái)具體分析。
實(shí)例
(代碼僅為了報(bào)錯(cuò)時(shí)方便分析,請(qǐng)勿仔細(xì)閱讀,避免浪費(fèi)時(shí)間)
目的:測(cè)試服務(wù)層的一個(gè)用于更新作業(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("無(wú)權(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; }
測(cè)試:
@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)行測(cè)試,出現(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)
問(wèn)題出在功能代碼的第130行,可以看到報(bào)錯(cuò)的代碼根本不是要測(cè)試的方法,而是被調(diào)用的方法。
再看測(cè)試代碼的203行,測(cè)試時(shí)的本來(lái)目的是為了Mock掉這個(gè)方法,但使用的是when().thenReturn方式。
對(duì)于Mock對(duì)象(完全假的對(duì)象),使用when().thenReturn和doReturn().when的效果是一樣的,都可以制造一個(gè)假的返回值。
但是對(duì)于Spy對(duì)象(半真半假的對(duì)象)就不一樣了,when().thenReturn會(huì)去執(zhí)行真正的方法,再返回假的返回值,在這個(gè)執(zhí)行真正方法的過(guò)程中,就可能出現(xiàn)空指針錯(cuò)誤。
而doReturn().when會(huì)直接返回假的數(shù)據(jù),而根本不執(zhí)行真正的方法。
參考鏈接:https://sangsoonam.github.io/...
所以把測(cè)試代碼的改成:
- Mockito.when(this.workService.isTeacher()).thenReturn(true); + Mockito.doReturn(true).when(workService).isTeacher();
再次運(yùn)行,就能通過(guò)測(cè)試。
目的:還是測(cè)試之前的方法,只不過(guò)新增了功能。
接口
/** * 更新作業(yè)分?jǐn)?shù) * @param id * @param score * @return */ Work updateScore(Long id, int score);
接口實(shí)現(xiàn)(在原有的儲(chǔ)存學(xué)生成績(jī)方法上新增了計(jì)算總分的功能)
@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("無(wú)權(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++; + //計(jì)算總成績(jī) + currentStudent.setTotalScore(currentStudent.getTotalScore()+awork.getScore()); + //計(jì)算平均成績(jī) + currentStudent.setAverageScore(currentStudent.getTotalScore()/viewed); + } + } + + studentRepository.save(currentStudent); return this.save(work); }
由于出現(xiàn)了對(duì)學(xué)生倉(cāng)庫(kù)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; }
然后是測(cè)試代碼
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); } ... }
順利通過(guò)測(cè)試,看似沒(méi)什么問(wèn)題,可是一跑全局單元測(cè)試,就崩了。
[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
一個(gè)斷言錯(cuò)誤,兩個(gè)空指針錯(cuò)誤。
可是這些三個(gè)功能我根本就沒(méi)有改,而且是之前已經(jīng)通過(guò)測(cè)試的功能,為什么會(huì)出錯(cuò)呢?
拿出一個(gè)具體的錯(cuò)誤,從本地跑一下測(cè)試:
測(cè)試代碼
@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("無(wú)權(quán)更新其它學(xué)生的作業(yè)"); } if (!oldWork.getItem().getActive()) { throw new ValidationException("禁止提交已關(guān)閉的實(shí)驗(yàn)作業(yè)"); } oldWork.setContent(work.getContent()); oldWork.setAttachments(work.getAttachments()); return this.workRepository.save(oldWork); }
報(bào)錯(cuò)信息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ù)報(bào)錯(cuò)信息來(lái)看,是測(cè)試類在調(diào)用功能代碼178行時(shí),出現(xiàn)了空指針,
經(jīng)過(guò)分析,在執(zhí)行this.studentService.getCurrentStudent().getId()
時(shí)出現(xiàn)的。
然后就來(lái)判斷studentService的注入情況,
//父類的BeforeEach public void beforeEach() { this.studentService = Mockito.mock(StudentService.class); this.currentStudent.setId(this.random.nextLong()); Mockito.doReturn(currentStudent) .when(this.studentService) .getCurrentStudent(); }
//測(cè)試類的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)); }
問(wèn)題就出在這里,由于測(cè)試類執(zhí)行了繼承,父類已經(jīng)Mock了一個(gè)studentService并且成功的設(shè)定了Moockito的返回值,但測(cè)試類又進(jìn)行了一次賦值,這就使得父類的Mock失效了,于是導(dǎo)致之前本來(lái)能通過(guò)的單元測(cè)試報(bào)錯(cuò)了。
所以本實(shí)例的根本問(wèn)題是,重復(fù)注入了對(duì)象。
這導(dǎo)致了原有的mock方法被覆蓋,以至于執(zhí)行了真實(shí)的studentService中的方法,返回了空的學(xué)生。
解決方法:
- 在測(cè)試類WorkServiceImplTest中刪除studentService的注入,使用父類。
- 使用子類的studentService,并在所有的報(bào)錯(cuò)位置,加入對(duì)應(yīng)的mock方法
總結(jié)
java.lang.NullPointerException直接翻譯過(guò)來(lái)是空指針,但根本原因卻不是空對(duì)象,一定是由于某種錯(cuò)誤的操作(錯(cuò)誤的注入),導(dǎo)致了空對(duì)象。
最常見(jiàn)的情況,就是在測(cè)試時(shí)執(zhí)行了真正的方法,而不是mock方法。
此時(shí)的解決方案,就是檢查所有的依賴注入和Mock是否完全正確,如果正確,就不會(huì)出現(xiàn)空指針異常了。
最根本的辦法,還是去分析,找到誰(shuí)是那個(gè)空對(duì)象,問(wèn)題就迎刃而解。
以上就是SpringMVC空指針異常NullPointerException解決及原理解析的詳細(xì)內(nèi)容,更多關(guān)于SpringMVC空指針異常解決的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java實(shí)時(shí)監(jiān)控日志文件并輸出的方法詳解
這篇文章主要給大家介紹了關(guān)于Java實(shí)時(shí)監(jiān)控日志文件并輸出的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編一起來(lái)學(xué)習(xí)學(xué)習(xí)吧。2017-06-06idea設(shè)置@Author文件頭注釋的實(shí)現(xiàn)步驟
本文主要介紹了idea設(shè)置@Author文件頭注釋的實(shí)現(xiàn)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07java連接數(shù)據(jù)庫(kù)增、刪、改、查工具類
這篇文章主要介紹了java連接數(shù)據(jù)庫(kù)增、刪、改、查工具類,需要的朋友可以參考下2014-05-05JavaWeb 使用Session實(shí)現(xiàn)一次性驗(yàn)證碼功能
這篇文章主要介紹了JavaWeb 使用Session實(shí)現(xiàn)一次性驗(yàn)證碼功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-08-08簡(jiǎn)單了解java類型轉(zhuǎn)換常見(jiàn)的錯(cuò)誤
這篇文章主要介紹了簡(jiǎn)單了解java類型轉(zhuǎn)換常見(jiàn)的錯(cuò)誤,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04Java網(wǎng)絡(luò)編程之簡(jiǎn)單的服務(wù)端客戶端應(yīng)用實(shí)例
這篇文章主要介紹了Java網(wǎng)絡(luò)編程之簡(jiǎn)單的服務(wù)端客戶端應(yīng)用,以實(shí)例形式較為詳細(xì)的分析了java網(wǎng)絡(luò)編程的原理與服務(wù)器端客戶端的實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-04-04