当前位置: 首页 > 科技观察

用枚举简单封装一个优雅的SpringBoot全局异常处理!

时间:2023-03-17 13:19:08 科技观察

这篇文章已经鸽了好久了,我在这篇文章中提到要分享《用好 Java 中的枚举,真的没有那么简单!》。昨天有读者提醒,我才发现我没有把这篇文章发到公众号。说到这里,我发现自己一个很大的问题,就是有时候在文章里说了要更新的东西,后来忘记了。很多时候,不是我没写,而是各种事情混在一起,忘记发了。尝试在将来更正此问题!在上一篇《SpringBoot 处理异常的几种常见姿势》中,我介绍了:使用@ControllerAdvice和@ExceptionHandler来处理全局异常@ExceptionHandler来处理Controller级别的异常ResponseStatusException通过这篇文章,你可以了解如何在SpringBoot中使用它来做异常处理。然而,仅仅知道如何使用它是不够的。我们还需要考虑如何更优雅地编写异常处理代码。下面说下我在工作中学习到的实际项目中的异常处理方式,说一下我认为稍微优雅一点的异常处理方案。以下仅代表我个人观点。如果读者有更好的解决方案或者认为本文提出的方案还有优化的空间,欢迎在评论区留言。最终效果如下图。先展示一下完成后的效果吧。当我们定义的异常被系统捕获后,返回给客户端的信息如下:效果展示返回的信息包括异常的以下5个部分:唯一表示异常的代码HTTPHTTPStatuscode错误路径wrongtimestamperrorspecificinformation这样返回异常信息,更有利于我们前端根据异常信息做出相应的表现。异常处理核心代码ErrorCode.java(该枚举类包含异常的唯一标识、HTTP状态码和错误信息)该类主要作用是统一管理系统可能发生的异常,相对清除。但是,可能出现的问题是,当系统过于复杂,异常情况过多时,这个类就会比较大。有一个解决方案:将多个类似的异常统一为一个,比如将用户找不到异常和没有找到订单信息的异常统一为“找不到资源”的异常,然后前端会查看相应的情况做详细的处理(个人处理方式,不敢保证是更好的方式)。exportorg.springframework.http.httpstatus;publicEnumErrorCode{resource_not_found(1001,httpstatus.not_found,“未未找到资源”),request_validation_failed(1002,htttpptpstatus.badrint.bad_requltrintintrint.badrintrintrint.badrintrint.badrint.badrint.badrintrintrintrytriTriTriThtriftrint.(intcode,HttpStatusstatus,Stringmessage){this.code=code;this.status=status;this.message=message;}publicintgetCode(){returncode;}publicHttpStatusgetStatus(){returnstatus;}publicStringgetMessage(){returnmessage;}@OverridepublicStringtoString(){return"ErrorCode{"+"code="+code+",status="+status+",message='"+message+'\''+'}';}}ErrorReponse.java(返回客户端具体Exceptionobject)这个类作为异常信息返回给客户端,它包含了异常发生时我们要返回给客户端的所有信息。importorg.springframework.util.ObjectUtils;importjava.time.Instant;importjava.util.HashMap;importjava.util.Map;publicclassErrorReponse{privateintcode;privateintstatus;privateStringmessage;privateStringpath;privateInstanttimestamp;privateHashMapdata=newHashMap();publicErrorReponse(){}publicErrorReponse(BaseExceptionex,Stringpath){this(ex.getError().getCode(),ex.getError().getStatus().value(),ex.getError().getMessage(),path,ex.getData());}publicErrorReponse(intcode,intstatus,Stringmessage,Stringpath,Mapdata){this.code=code;this.status=status;this.message=message;this.path=path;this.timestamp=Instant.now();if(!ObjectUtils.isEmpty(data)){this.data.putAll(data);}}//省略getter/setter方法@OverridepublicStringtoString(){return"ErrorReponse{"+"code="+code+",status="+status+",message='"+message+'\''+",path='"+path+'\''+",timestamp="+时间amp+",data="+data+'}';}}BaseException.java(继承自RuntimeException的抽象类,可以看作是系统中其他异常类的父类)系统中所有的异常类都必须继承自这个班publicabstractclassBaseExceptionextendsRuntimeException{privatefinalErrorCodeerror;privatefinalHashMapdata=newHashMap<>();publicBaseException(ErrorCodeerror,Mapdata){super(error.getMessage());this.error=error;if(!ObjectUtils.isEmpty(data)){this.data.putAll(data);}}protectedBaseException(ErrorCodeerror,Mapdata,Throwablecause){super(error.getMessage(),cause);this.error=error;if(!ObjectUtils.isEmpty(data)){this.data.putAll(data);}}publicErrorCodegetError(){returnerror;}publicMapgetData(){returndata;}}ResourceNotFoundException.java(自定义异常)可见,通过继承BaseException类,我们的自定义异常就会变得非常简单!导入java.util.Map;publicclassResourceNotFoundExceptionextendsBaseException{publicResourceNotFoundException(Mapdata){super(ErrorCode.RESOURCE_NOT_FOUND,data);}}GlobalExceptionHandler。Java(全局异常捕获)我们定义了两种异常捕获方法。这里再说明一下,其实这个类只需要handleAppException()方法即可,因为它是本系统所有异常的父类。只要抛出继承BaseException类的异常,都会在这里处理。importcom.twuc.webApp.web.ExceptionController;importorg.springframework.http.HttpHeaders;importorg.springframework.http.HttpStatus;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.ControllerAdvice;importorg.springframework。web.bind.annotation.ExceptionHandler;importorg.springframework.web.bind.annotation.ResponseBody;importjavax.servlet.http.HttpServletRequest;@ControllerAdvice(assignableTypes={ExceptionController.class})@ResponseBodypublicclassGlobalExceptionHandler{//也可以将BaseException换为RuntimeException//因为RuntimeException是BaseException的父类@ExceptionHandler(BaseException.class)publicResponseEntityhandleAppException(BaseExceptionex,HttpServletRequestrequest){ErrorReponserepresentation=newErrorReponse(ex,request.getRequestURI());returnnewResponseEntity<>(representation,newHttpHeaders(),ex.getError().getStatus());}@ExceptionHandler(value=ResourceNotFoundException.class)publicResponseEntity<ErrorReponse>handleResourceNotFoundException(ResourceNotFoundExceptionex,HttpServletRequestrequest){ErrorReponseerrorReponse=newErrorReponse(ex,request.getRequestURI());returnResponseEntity.status(HttpStatus.extendsAD_REQUESter);或}重要位)Repon(er添加了一个多余的异常捕获方法handleResourceNotFoundException()主要是测试大家,当我们抛出ResourceNotFoundException异常的时候,下面的哪些方法会被捕获?答:会被handleResourceNotFoundException()方法捕获,因为@ExceptionHandler捕获异常过程其中,最匹配的会先找到。下面简单分析下源码:ExceptionHandlerMethodResolver.java中的getMappedMethod判断处理哪个方法@NullableprivateMethodgetMappedMethod(ClassexceptionType){List>matches=newArrayList<>();//查找所有异常可以处理的信息。MappedMethods存储了异常与异常处理方法的对应关系不为空,表示有处理异常的方法if(!matches.isEmpty()){//排序matches.sort(newExceptionDepthComparator(exceptionType));//返回处理异常的方法returnthis.mappedMethods.get(matches.get(0));}else{returnnull;}}从源码可以看出:getMappedMethod()会先找到所有能匹配异常处理的方法信息,然后从小到大排序大,最后取最小匹配法(即匹配度最高的)。写一个抛出异常的类来测试Person.javapublicclassPerson{privateLongid;privateStringname;//省略getter/setter方法}ExceptionController.java(throwsaclass)@RestController@RequestMapping("/api")publicclassExceptionController{@GetMapping("/resourceNotFound")publicvoidthrowException(){Personp=newPerson(1L,"SnailClimb");thrownewResourceNotFoundException(ImmutableMap.of("personid:",p.getId()));}}源码地址:https://github。com/Snailclimb/springboot-guide/tree/master/source-code/basis/springboot-handle-exception-improved