文章目录
- 前言
- 一、异常分类
- 1.1 业务异常
- 1.2 参数校验异常
- 1.3 通用异常兜底
- 二、保留异常现场
- 2.1 请求地址
- 2.2 请求header
- 2.3 请求参数+body
- 2.4 构建异常上下文消息
- 最后
前言
全局异常处理, 你真的学会了吗?
学完上文,你有思考和动手实践吗?
上文咱们主要讲的是全局异常处理机制,说句实在话,如果没有人带你,即使你掌握了机制,也未必能玩转异常处理!异常处理真的很重要,所以本文带大家在图书实战项目中落地!非常深入,非常细节,非常详细!你绝对没看过这么全的,最后有源码齐全可直接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
**/
public class BizException extends RuntimeException {
private String code;
public BizException(String message) {
this("400", message);
}
public BizException(String message, Throwable cause) {
this("400", message, cause);
}
public BizException(String code, String message) {
super(message);
this.code = code;
}
public BizException(String code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
public String getCode() {
return this.code;
}
public void setCode(String code) {
this.code = code;
}
}
上面这些代码比较基础,message在父类已定义,所以主要定义了一个code,并实现了4个构造函数重载,以适用于不同的业务场景调用!
你可以根据你的业务定义不同的BizException,增加不同的参数!
然后,我们在GlobalExceptionHandler
中通过@ExceptionHandler(BizException.class)
通用处理!
@ExceptionHandler(BizException.class)
public TgResult handleBizException(BizException e) {
log.warn("BizException", e);
return TgResult.fail(e.getCode(), e.getMessage());
}
1.2 参数校验异常
除了业务异常,通常还有一类必须处理的异常:参数校验异常!
在springboot中,在controller层通常都是基于注解
的参数校验!这部分目前我们还没有在项目中应用,这是不够健壮性的,所以在后面也会安排讲这部分!我们先处理校验失败抛出的异常!
校验失败会抛出:BindException
或MethodArgumentNotValidException
,至于为什么不做展开!
@ExceptionHandler(BindException.class)
public TgResult handleBindException(BindException e) {
StringBuilder sb = new StringBuilder();
e.getBindingResult().getAllErrors().forEach(error -> {
sb.append(error.getDefaultMessage()).append("\r\n");
});
log.warn("BindException:{}", sb, e);
return TgResult.fail("400", sb.toString());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public TgResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
StringBuilder sb = new StringBuilder();
e.getBindingResult().getAllErrors().forEach(error -> {
sb.append(error.getDefaultMessage()).append("\r\n");
});
log.warn("MethodArgumentNotValidException:{}", sb, e);
return TgResult.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 = new StringBuilder(); 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 = "/*")
@Component
public class CacheBodyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
CacheBodyHttpServletRequestWrapper servletRequest = new CacheBodyHttpServletRequestWrapper(httpServletRequest);
filterChain.doFilter(servletRequest, httpServletResponse);
}
}
CacheBodyHttpServletRequestWrapper 缓存body
public class CacheBodyHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public CacheBodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
InputStream requestInputStream = request.getInputStream();
this.body = StreamUtils.copyToByteArray(requestInputStream);
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CacheBodyServletInputStream(this.body);
}
@Override
public BufferedReader getReader() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.body);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
public byte[] getBody() {
return body;
}
public static class CacheBodyServletInputStream extends ServletInputStream {
private final InputStream cacheBodyInputStream;
public CacheBodyServletInputStream(byte[] cachedBody) {
this.cacheBodyInputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public int read() throws IOException {
return cacheBodyInputStream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
}
}
2.4 构建异常上下文消息
如何获取都有了,那么我们加一个方法来构建消息吧~
/**
* 构建异常上下文消息
**/
private String buildContextMessage(HttpServletRequest request) {
// 请求地址
String url = request.getRequestURI();
String method = request.getMethod();
// 获取指定header
// String oneHeader = request.getHeader("tgCsrfToken");
// 获取全部header
Enumeration<String> allHeaders = request.getHeaderNames();
StringBuilder sbAllHeaders = new StringBuilder();
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();
// 获取body
String body = null;
if (request instanceof CacheBodyHttpServletRequestWrapper) {
CacheBodyHttpServletRequestWrapper wrapper = (CacheBodyHttpServletRequestWrapper) request;
body = new String(wrapper.getBody());
}
return String.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 = new StringBuilder();
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 = new StringBuilder();
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