Spring Boot 统一异常这样处理和剖析,安否?
话说异常
「欲渡黄河冰塞川,将登太行雪满天」,无论生活还是计算机世界难免发生异常,上一篇文章RESTful API 返回统一JSON数据格式 说明了统一返回的处理,这是请求一切正常的情形;这篇文章将说明如何统一处理异常,以及其背后的实现原理,老套路,先实现,后说明原理,有了上一篇文章的铺底,相信,理解这篇文章就驾轻就熟了

实现
新建业务异常
新建 BusinessException.class 类表示业务异常,注意这是一个 Runtime 异常
@Data
@AllArgsConstructor
public final class BusinessException extends RuntimeException {
private String errorCode;
private String errorMsg;
}
添加统一异常处理静态方法
在 CommonResult 类中添加静态方法 errorResult 用于接收异常码和异常消息:
public static <T> CommonResult<T> errorResult(String errorCode, String errorMsg){
CommonResult<T> commonResult = new CommonResult<>();
commonResult.errorCode = errorCode;
commonResult.errorMsg = errorMsg;
commonResult.status = -1;
return commonResult;
}
配置
同样要用到 @RestControllerAdvice 注解,将统一异常添加到配置中:
@RestControllerAdvice("com.example.unifiedreturn.api")
static class UnifiedExceptionHandler{
@ExceptionHandler(BusinessException.class)
public CommonResult<Void> handleBusinessException(BusinessException be){
return CommonResult.errorResult(be.getErrorCode(), be.getErrorMsg());
}
}
三部搞定,到这里无论是 Controller 还是 Service 中,只要抛出 BusinessException, 我们都会返回给前端一个统一数据格式
测试
将 UserController 中的方法进行改造,直接抛出异常:
@GetMapping("/{id}")
public UserVo getUserById(@PathVariable Long id){
throw new BusinessException("1001", "根据ID查询用户异常");
}
浏览器中输入: http://localhost:8080/users/1

在 Service 中抛出异常:
@Service
public class UserServiceImpl implements UserService {
/**
* 根据用户ID查询用户
*
* @param id
* @return
*/
@Override
public UserVo getUserById(Long id) {
throw new BusinessException("1001", "根据ID查询用户异常");
}
}
运行是得到同样的结果,所以我们尽可能的抛出异常吧 (作为一个程序猿这种心理很可拍)
解剖实现过程
解剖这个过程是相当纠结的,为了更好的说(yin)明(wei)问(wo)题(lan),我要说重中之重了,真心希望看该文章的童鞋自己去案发现场发现线索
还是在 WebMvcConfigurationSupport 类中实例化了 HandlerExceptionResolver Bean
@Bean
public HandlerExceptionResolver handlerExceptionResolver() {
List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
configureHandlerExceptionResolvers(exceptionResolvers);
if (exceptionResolvers.isEmpty()) {
addDefaultHandlerExceptionResolvers(exceptionResolvers);
}
extendHandlerExceptionResolvers(exceptionResolvers);
HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
composite.setOrder(0);
composite.setExceptionResolvers(exceptionResolvers);
return composite;
}
和上一篇文章一毛一样的套路,ExceptionHandlerExceptionResolver 实现了 InitializingBean 接口,重写了 afterPropertiesSet 方法:
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBodyAdvice beans
initExceptionHandlerAdviceCache();
...
}
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(adviceBeans);
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
// 重点看这个构造方法
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
}
重点看上面我用注释标记的构造方法,代码很好懂,仔细看看吧,其实就是筛选出我们用 @ExceptionHandler 注解标记的方法并放到集合当中,用于后续全局异常捕获的匹配
/**
* A constructor that finds {@link ExceptionHandler} methods in the given type.
* @param handlerType the type to introspect
*/
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
addExceptionMapping(exceptionType, method);
}
}
}
/**
* Extract exception mappings from the {@code @ExceptionHandler} annotation first,
* and then as a fallback from the method signature itself.
*/
@SuppressWarnings("unchecked")
private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
List<Class<? extends Throwable>> result = new ArrayList<>();
detectAnnotationExceptionMappings(method, result);
if (result.isEmpty()) {
for (Class<?> paramType : method.getParameterTypes()) {
if (Throwable.class.isAssignableFrom(paramType)) {
result.add((Class<? extends Throwable>) paramType);
}
}
}
if (result.isEmpty()) {
throw new IllegalStateException("No exception types mapped to " + method);
}
return result;
}
private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class);
Assert.state(ann != null, "No ExceptionHandler annotation");
result.addAll(Arrays.asList(ann.value()));
}
到这里,我们用 @RestControllerAdvice 和 @ExceptionHandler 注解就会被 Spring 扫描到上下文,供我们使用
让我们回到你最熟悉的调用的入口 DispatcherServlet 类的 doDispatch 方法:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
...
// 当请求发生异常,该方法会通过 catch 捕获异常
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 调用该方法分析捕获的异常
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
...
}
接下来,我们来看 processDispatchResult 方法,这里只要展示调用栈你就会眼前一亮了,又是为了返回统一格式数据:

总结
上一篇文章的返回统一数据格式是基础,当异常情况发生时,只不过需要将异常信息提取出来。本文主要为了说明问题,剖析原理,好多地方设计方式是不可取,比如我们最好将异常封装在一个 Enum 类,通过 enum 对象抛出异常等,如果你用到这些,去完善你的设计方案吧
回复 「demo」,打开链接,查看文件夹 「unifiedreturn」下内容,获取完整代码,更好阅读体验:
https://fraseryu.github.io/2019/08/09/ru-he-tong-yi-chu-li-yi-chang-bing-fan-hui-tong-yi-ge-shi/
附加说明
之前看到的一本书对异常的分类让我印象深刻,在此摘录一小段分享给大家:

结合出国旅行的例子说明异常分类:
- 机场地震,属于不可抗力,对应异常分类中的
Error,在制订出行计划时,根本不需要把这个部分的异常考虑进去- 堵车属于
checked异常,应对这种异常,我们可以提前出发,或者改签机票。而飞机延误异常,虽然也需要 check,但我们无能为力,只能持续关注航班动态- 没有带护照,明显属于可
提前预测的异常,只要出发前检查即可避免;去机场路上车子抛锚,这个异常是突发的,虽然难以预料,但是必须处理,属于需要捕捉的异常,可以通过更换交通工具;应对检票机器故障属于可透出异常,交由航空公司处理,我们无须关心
灵魂追问
- 这两篇文章,你学到了哪些设计模式?
- 你能熟练的使用反射吗?当看源码是会看到很多反射的应用
- 你了解 Spring CGLIB 吗?它的工作原理是什么?
提高效率工具

JSON-Viewer
JSON-Viewer 是 Chrome 浏览器的插件,用于快速解析及格式化 json 内容,在 Chrome omnibox(多功能输入框)输入json-viewer + TAB ,将 json 内容拷贝进去,然后输入回车键,将看到结构清晰的 json 数据,同时可以自定义主题

另外,前端人员打开开发者工具,双击请求链接,会自动将 response 中的 json 数据解析出来,非常方便
推荐阅读
- 只会用 git pull ?有时候你可以尝试更优雅的处理方式
- 双亲委派模型:大厂高频面试题,轻松搞定
- 面试还不知道BeanFactory和ApplicationContext的区别?
- 如何设计好的RESTful API
- 程序猿为什么要看源码?
欢迎持续关注公众号:「日拱一兵」
- 前沿 Java 技术干货分享
- 高效工具汇总 | 回复「工具」
- 面试问题分析与解答
- 技术资料领取 | 回复「资料」
以读侦探小说思维轻松趣味学习 Java 技术栈相关知识,本着将复杂问题简单化,抽象问题具体化和图形化原则逐步分解技术问题,技术持续更新,请持续关注......

Spring Boot 统一异常这样处理和剖析,安否?的更多相关文章
- spring boot统一异常页面
只需要创建一个类就可以了 package com.ulic.gis.securityManage.controller; import java.util.Map; import javax.serv ...
- Spring Boot 全局异常配置
Spring Boot 全局异常配置,处理异常控制器需要和发生异常的方法在一个类中.使用 ControllerAdvice 注解 package com.li.controller; import o ...
- spring boot 统一接口异常返回值
创建业务 Exception 一般在实际项目中,推荐创建自己的 Exception 类型,这样在后期会更容易处理,也比较方便统一,否则,可能每个人都抛出自己喜欢的异常类型,而造成代码混乱 Servic ...
- spring boot 拦截异常 统一处理
spring boot 默认情况下会映射到 /error 进行异常处理,提示不友好,需要自定义异常处理,提供友好展示 1.自定义异常类(spring 对于 RuntimeException 异常才会进 ...
- spring boot 统一异常处理
需求源自于任何一个业务的编写总会有各种各样的条件判断,需要时时手动抛出异常,又希望让接口返回友好的错误信息. spring boot提供的帮助是自动将异常重定向到路由为/error的控制器 但是我们又 ...
- Spring Boot统一异常处理实践
摘要: SpringBoot异常处理. 原文:Spring MVC/Boot 统一异常处理最佳实践 作者:赵俊 前言 在 Web 开发中, 我们经常会需要处理各种异常, 这是一件棘手的事情, 对于很多 ...
- Spring Boot 统一返回结果及异常处理
在 Spring Boot 构建电商基础秒杀项目 (三) 通用的返回对象 & 异常处理 基础上优化.调整 一.通用类 1.1 通用的返回对象 public class CommonReturn ...
- Spring Boot 处理异常返回json
spring boot 老版本处理异常 对于浏览器客户端,返回error数据 对于非浏览器返回json数据, 主要取决于你的请求head 是什么 但是当我们自定义了: 老版本无论请求什么都会返回j ...
- Spring Boot统一异常处理方案示例
一.异常处理的原则 1.调用方法的时候返回布尔值来代替返回null,这样可以 NullPointerException.由于空指针是java异常里最恶心的异常. 2. catch块里别不写代码.空ca ...
随机推荐
- Android之无限轮播图源代码
Android轮播广告图是大家经常用到的一个控件今天便撸了一把代码 实现步骤 使用Viewpager进行实现图片滑动 设置ViewPager的数据,让其无限切换 Activity代码 public c ...
- C#中面向对象编程中的函数式编程详解
介绍 使用函数式编程来丰富面向对象编程的想法是陈旧的.将函数编程功能添加到面向对象的语言中会带来面向对象编程设计的好处. 一些旧的和不太老的语言,具有函数式编程和面向对象的编程: 例如,Smallta ...
- 如何优雅关闭 Spring Boot 应用
## 前言 随着线上应用逐步采用 SpringBoot 构建,SpringBoot应用实例越来多,当线上某个应用需要升级部署时,常常简单粗暴地使用 kill 命令,这种停止应用的方式会让应用将所有处理 ...
- web-inf与meta-inf
/WEB-INF/web.xml Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则. /WEB-INF/classes/包含了站点所有用的 class 文件,包括 ser ...
- Jmeter脚本录制--HTTP代理服务器
Jmeter脚本录制功能依赖第三方工具Badboy,所以在安装了Jmeter之后,还需要再安装一个工具. Badboy本身自带浏览器,相关操作只能在Badboy上进行操作,偶尔可能会遇到浏览器兼容的问 ...
- .net core开发从未如此简单,比abp更接地气
在谈起java一家独大的时候,dotnet人员总是一边嘲笑大量滥竽充数的java从业者,一边羡慕人家的生态.以前是只能羡慕,现在dotnet core开源了,我们都可以为dotnet core的开原生 ...
- ubuntu搭建环境
1.终端输入 sudo apt- add-apt-repository ppa:ondrej/php sudo add-apt-repository ppa:ondrej/php sudo apt ...
- C++虚函数的工作原理
静态绑定与动态绑定 讨论静态绑定与动态绑定,首先需要理解的是绑定,何为绑定?函数调用与函数本身的关联,以及成员访问与变量内存地址间的关系,称为绑定. 理解了绑定后再理解静态与动态. 静态绑定:指在程序 ...
- 比特平面分层(一些基本的灰度变换函数)基本原理及Python实现
1. 基本原理 在灰度图中,像素值的范围为[0, 255],即共有256级灰度.在计算机中,我们使用8比特数来表示每一个像素值.因此可以提取出不同比特层面的灰度图.比特层面分层可用于图片压缩:只储存较 ...
- 最基础的 ant build 脚本
最基础的 ant build 脚本,根据项目,自行进行修改 <?xml version="1.0" encoding="UTF-8" ?> < ...