文章目录
前言
全局异常处理, 你真的学会了吗?
学完上文,你有思考和动手实践吗?
上文咱们主要讲的是全局异常处理机制,说句实在话,如果没有人带你,即使你掌握了机制,也未必能玩转异常处理!异常处理真的很重要,所以本文带大家在图书实战项目中落地!非常深入,非常细节,非常详细!你绝对没看过这么全的,最后有源码齐全可直接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
版权归原作者 天罡gg 所有, 如有侵权,请联系我们删除。