0


7.11 SpringBoot实战 全局异常处理 - 深入细节详解

CSDN成就一亿技术人

文章目录


前言

全局异常处理, 你真的学会了吗?

学完上文,你有思考和动手实践吗?

上文咱们主要讲的是全局异常处理机制,说句实在话,如果没有人带你,即使你掌握了机制,也未必能玩转异常处理!异常处理真的很重要,所以本文带大家在图书实战项目中落地!非常深入,非常细节,非常详细!你绝对没看过这么全的,最后有源码齐全可直接Copy!

我们的重点是利用全局异常处理

机制

来为我们好好服务,达到

异常为我、我爱异常

上文地址:7.10 SpringBoot实战 全局异常处理


一、异常分类

对于

@ExceptionHandler

,如果你只定义一个

@ExceptionHandler(Exception.class)

未免过于粗!

但是,如果你把所有异常都加一个

@ExceptionHandler

,又未免过于太细!没有必要!

所以,我们将需要【独立解析的异常】归为一类,统一处理!

1.1 业务异常

这里说的业务异常,不是JDK或第三方类库封装的异常类,而是由你自定义,并由你主动抛出的异常,可能是一个,也可能是N个,具体取决于你业务的复杂度!

本项目目前只需要先定义一个业务异常:

BizException

我们在业务逻辑校验不通过时,统一抛出该异常,并且统一在全局异常处理该异常!

这正是我对于【7.1】中如何优雅处理的答案!你懂了吗? 7.1「实战」图书录入和修改API --如何优雅处理校验逻辑?

在这里插入图片描述

因为

BizException

可能在项目中任意地方抛出,所以需要将此类定义在

common

注意, 业务异常是在运行时由我们主动抛出,属于运行时异常,所以继承自

RuntimeException

/**
 * 业务异常类
 *
 * @author 天罡gg
 * @date 2023/8/27
 **/publicclassBizExceptionextendsRuntimeException{privateString code;publicBizException(String message){this("400", message);}publicBizException(String message,Throwable cause){this("400", message, cause);}publicBizException(String code,String message){super(message);this.code = code;}publicBizException(String code,String message,Throwable cause){super(message, cause);this.code = code;}publicStringgetCode(){returnthis.code;}publicvoidsetCode(String code){this.code = code;}}

上面这些代码比较基础,message在父类已定义,所以主要定义了一个code,并实现了4个构造函数重载,以适用于不同的业务场景调用!

你可以根据你的业务定义不同的BizException,增加不同的参数!

然后,我们在

GlobalExceptionHandler

中通过

@ExceptionHandler(BizException.class)

通用处理!

@ExceptionHandler(BizException.class)publicTgResulthandleBizException(BizException e){
    log.warn("BizException", e);returnTgResult.fail(e.getCode(), e.getMessage());}

1.2 参数校验异常

除了业务异常,通常还有一类必须处理的异常:参数校验异常!

在springboot中,在controller层通常都是

基于注解

的参数校验!这部分目前我们还没有在项目中应用,这是不够健壮性的,所以在后面也会安排讲这部分!我们先处理校验失败抛出的异常!

校验失败会抛出:

BindException

MethodArgumentNotValidException

,至于为什么不做展开!

@ExceptionHandler(BindException.class)publicTgResulthandleBindException(BindException e){StringBuilder sb =newStringBuilder();
    e.getBindingResult().getAllErrors().forEach(error ->{
        sb.append(error.getDefaultMessage()).append("\r\n");});
    log.warn("BindException:{}", sb, e);returnTgResult.fail("400", sb.toString());}@ExceptionHandler(MethodArgumentNotValidException.class)publicTgResulthandleMethodArgumentNotValidException(MethodArgumentNotValidException e){StringBuilder sb =newStringBuilder();
    e.getBindingResult().getAllErrors().forEach(error ->{
        sb.append(error.getDefaultMessage()).append("\r\n");});
    log.warn("MethodArgumentNotValidException:{}", sb, e);returnTgResult.fail("400", sb.toString());}

1.3 通用异常兜底

这个兜底就是我们上文加过的@ExceptionHandler(Exception.class),所有异常通吃,所以用这个兜底!

本文以此3类抛转引玉,相信能解决大部分场景!如果超出处理范围,

原则

是当你发现通过@ExceptionHandler(Exception.class)无法解析出想要的信息时,就可以定义新的@ExceptionHandler(XXX.class)!


二、保留异常现场

解决BUG就像破案一样,通过异常反推,总有一些诡异的异常,绞尽脑汁,让你想破了天,可能依然摸不着头脑,但是如果测试人员能够复现,那么你解决起来就会水到渠成!认同的,点个赞 (≧▽≦)/

那么如何才能不依赖测试人员,只靠自己就能复现问题呢?

今天再教你实用一招,让你以后Happy的解决异常,那就是

保留好异常现场

,或者说是现场还原!

难的不会,会的不难,主要使用 HttpServletRequest 记录这一次Http请求的3大部分:请求地址、请求header、请求参数

实际上,在@RestControllerAdvice中,我们依然可以在@ExceptionHandler修饰的方法参数上加入

HttpServletRequest

,例如:

在这里插入图片描述

2.1 请求地址

  • 获取API的请求地址request.getRequestURI()
  • 获取API的请求方法通过:request.getMethod()

2.2 请求header

  • 获取指定header的值:request.getHeader规范的程序,我们在请求报文中定义的header都是固定的,所以只需要按header来获取值即可!例如本项目有个header叫tgCsrfToken,就这样获取:request.getHeader("tgCsrfToken")
  • 获取全部header:request.getHeaderNames``````Enumeration<String> headers = request.getHeaderNames();StringBuilder sbAllHeaders =newStringBuilder();sbAllHeaders.append("headers:\r\n");while(headers.hasMoreElements()){String headerKey = headers.nextElement();String headerValue = request.getHeader(headerKey); sbAllHeaders.append(headerKey+":"+headerValue+"\r\n");}

2.3 请求参数+body

  • 获取拼接地址上的参数:request.getParameterMap()
  • 获取body的参数:request.getReader()

不过此时使用getReader()会报异常:

getInputStream() has already been called for this request

原因是因为流总是向前的,只可以读取一次,所以要反复使用,需提前缓存body,以达到反复使用的目的。

解决方案是使用

Filter

,在doFilter时传入我们缓存的的

HttpServletRequestWrapper

,具体的实现:

CacheBodyFilter,优先级最高的过滤器、只执行一次,== 目的是将HttpServletRequest包装成CacheBodyHttpServletRequestWrapper ==

@Order(value =Ordered.HIGHEST_PRECEDENCE)@WebFilter(filterName ="CacheBodyFilter", urlPatterns ="/*")@ComponentpublicclassCacheBodyFilterextendsOncePerRequestFilter{@OverrideprotectedvoiddoFilterInternal(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,FilterChain filterChain)throwsServletException,IOException{CacheBodyHttpServletRequestWrapper servletRequest =newCacheBodyHttpServletRequestWrapper(httpServletRequest);
        filterChain.doFilter(servletRequest, httpServletResponse);}}

CacheBodyHttpServletRequestWrapper 缓存body

publicclassCacheBodyHttpServletRequestWrapperextendsHttpServletRequestWrapper{privatefinalbyte[] body;publicCacheBodyHttpServletRequestWrapper(HttpServletRequest request)throwsIOException{super(request);InputStream requestInputStream = request.getInputStream();this.body =StreamUtils.copyToByteArray(requestInputStream);}@OverridepublicServletInputStreamgetInputStream()throwsIOException{returnnewCacheBodyServletInputStream(this.body);}@OverridepublicBufferedReadergetReader()throwsIOException{ByteArrayInputStream byteArrayInputStream =newByteArrayInputStream(this.body);returnnewBufferedReader(newInputStreamReader(byteArrayInputStream));}publicbyte[]getBody(){return body;}publicstaticclassCacheBodyServletInputStreamextendsServletInputStream{privatefinalInputStream cacheBodyInputStream;publicCacheBodyServletInputStream(byte[] cachedBody){this.cacheBodyInputStream =newByteArrayInputStream(cachedBody);}@Overridepublicintread()throwsIOException{return cacheBodyInputStream.read();}@OverridepublicbooleanisFinished(){returnfalse;}@OverridepublicbooleanisReady(){returntrue;}@OverridepublicvoidsetReadListener(ReadListener readListener){}}}

2.4 构建异常上下文消息

如何获取都有了,那么我们加一个方法来构建消息吧~

/**
 * 构建异常上下文消息
 **/privateStringbuildContextMessage(HttpServletRequest request){// 请求地址String url = request.getRequestURI();String method = request.getMethod();// 获取指定header// String oneHeader = request.getHeader("tgCsrfToken");// 获取全部headerEnumeration<String> allHeaders = request.getHeaderNames();StringBuilder sbAllHeaders =newStringBuilder();while(allHeaders.hasMoreElements()){String headerKey = allHeaders.nextElement();String headerValue = request.getHeader(headerKey);
        sbAllHeaders.append(headerKey).append(":").append(headerValue).append("\r\n");}// 请求参数String parameterMap = request.getParameterMap().toString();// 获取bodyString body =null;if(request instanceofCacheBodyHttpServletRequestWrapper){CacheBodyHttpServletRequestWrapper wrapper =(CacheBodyHttpServletRequestWrapper) request;
        body =newString(wrapper.getBody());}returnString.format("url:%s, method:%s, headers:%s, parameterMap:%s, body:%s", url, method, sbAllHeaders.toString(), parameterMap, body);}

最终调用的完整代码如下:

// 业务异常 ===========================================
@ExceptionHandler(BizException.class)public TgResult handleBizException(HttpServletRequest request, BizException e){
    String contextMessage =buildContextMessage(request);
log.warn("BizException:code:{}, message:{}, contextMessage:{}", e.getCode(), e.getMessage(), contextMessage, e);return TgResult.fail(e.getCode(), e.getMessage());}// 参数校验异常 ===========================================
@ExceptionHandler(BindException.class)public TgResult handleBindException(HttpServletRequest request, BindException e){
    StringBuilder sb =newStringBuilder();
e.getBindingResult().getAllErrors().forEach(error ->{
    sb.append(error.getDefaultMessage()).append("\r\n");});
String contextMessage =buildContextMessage(request);
log.warn("BindException: message:{}, contextMessage:{}", sb, contextMessage, e);return TgResult.fail("400", sb.toString());}

@ExceptionHandler(MethodArgumentNotValidException.class)public TgResult handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e){
    StringBuilder sb =newStringBuilder();
e.getBindingResult().getAllErrors().forEach(error ->{
    sb.append(error.getDefaultMessage()).append("\r\n");});
String contextMessage =buildContextMessage(request);
log.warn("MethodArgumentNotValidException: message:{}, contextMessage:{}", sb, contextMessage, e);return TgResult.fail("400", sb.toString());}// 通用异常兜底 ===========================================
@ExceptionHandler(Exception.class)public TgResult handleException(HttpServletRequest request, Exception e){
    String contextMessage =buildContextMessage(request);
log.warn("Exception: message:{}, contextMessage:{}", e.getMessage(), contextMessage, e);return TgResult.fail("500","服务器内部错误");}

最后

看到这,觉得有帮助的,刷波666,投个票,感谢大家的支持~

想要看更多实战好文章,还是给大家推荐我的实战专栏–>《基于SpringBoot+SpringCloud+Vue前后端分离项目实战》,由我和 前端狗哥 合力打造的一款专栏,可以让你从0到1快速拥有企业级规范的项目实战经验!

具体的优势、规划、技术选型都可以在《开篇》试读!

订阅专栏后可以添加我的微信,我会为每一位用户进行针对性指导!

另外,别忘了关注我:天罡gg ,怕你找不到我,发布新文不容易错过: https://blog.csdn.net/scm_2008

标签: spring boot 后端 java

本文转载自: https://blog.csdn.net/scm_2008/article/details/132527038
版权归原作者 天罡gg 所有, 如有侵权,请联系我们删除。

“7.11 SpringBoot实战 全局异常处理 - 深入细节详解”的评论:

还没有评论