Springboot全局异常处理
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Springboot全局异常处理相关的知识,希望对你有一定的参考价值。
参考技术A 从 spring 3.2 开始,新增了 @ControllerAdvice 注解,可以用于定义@ExceptionHandler,并应用到配置了@RequestMapping 的控制器中。如果这接口是给第三方调用那是不行的,至此大致能了解到为啥需要对异常进行全局捕获了。
原理也很简单,Spring Boot 默认提供了程序出错的结果映射路径/error。这个/error请求会在BasicErrorController中处理,其内部是通过判断请求头中的Accept的内容是否为text/html来区分请求是来自客户端浏览器(浏览器通常默认自动发送请求头内容Accept:text/html)还是客户端接口的调用,以此来决定返回页面视图还是 JSON 消息内容。
浏览器端访问的话,任何错误Spring Boot返回的都是一个Whitelabel Error Page的错误页面,这个很不友好,所以我们可以自定义下错误页面。
这样运行的时候,请求一个不存在的页面或服务端处理发生异常时,展示的自定义错误界面。
Spring Boot提供的ErrorController是一种全局性的容错机制。此外,你还可以用@ControllerAdvice注解和@ExceptionHandler注解实现对指定异常的特殊处理。
这里介绍两种情况:
局部异常主要用到的是@ExceptionHandler注解,此注解注解到类的方法上,当此注解里定义的异常抛出时,此方法会被执行。如果@ExceptionHandler所在的类是@Controller,则此方法只作用在此类。如果@ExceptionHandler所在的类带有@ControllerAdvice注解,则此方法会作用在全局。
在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中。
简单的说,进入Controller层的错误才会由@ControllerAdvice处理,拦截器抛出的错误以及访问错误地址的情况@ControllerAdvice处理不了,由SpringBoot默认的异常处理机制处理。
我们实际开发中,如果是要实现RESTful API,那么默认的JSON错误信息就不是我们想要的,这时候就需要统一一下JSON格式,所以需要封装一下。
此类在common的项目,要暴露出去给依赖的项目使用,在文件src\main\resources\META-INF\spring.factories中添加最后一行
可以被全局异常捕捉并处理成json
访问接口,如果无数据,则输出异常信息
"data":"package id为:BZ-20200107000005 的indexpackage无记录","flag":false,"code":null,"msg":"未查到数据"
全局异常类可以用 @RestControllerAdvice ,替代 @ControllerAdvice ,因为这里返回的主要是json格式,这样可以少写一个 @ResponseBody 。
重学SpringBoot系列之统一全局异常处理
重学SpringBoot系列之统一全局异常处理
设计一个优秀的异常处理机制
异常处理的乱象例举
乱象一:捕获异常后只输出到控制台
前端js-ajax代码
$.ajax(
type: "GET",
url: "/user/add",
dataType: "json",
success: function(data)
alert("添加成功");
);
后端业务代码
try
// do something
catch (XyyyyException e)
e.printStackTrace();
问题:
- 后端直接将异常捕获,而且只做了日志打印。用户体验非常差,一旦后台出错,用户没有任何感知,页面无状态。
- 后端只给出前端异常结果,没有给出异常的原因的描述。用户不知道是自己操作输入错误,还是系统bug。用户无法判断自己需要等一下再操作?还是继续下一步?
- 如果没有人去经常关注服务端日志,不会有人发现系统出现异常。
乱象二:混乱的返回方式
前端代码
$.ajax(
type: "GET",
url: "/goods/add",
dataType: "json",
success: function(data)
if (data.flag)
alert("添加成功");
else
alert(data.message);
,
error: function(data)
alert("添加失败");
);
后端代码
@RequestMapping("/goods/add")
@ResponseBody
public Map add(Goods goods)
Map map = new HashMap();
try
// do something
map.put(flag, true);
catch (Exception e)
e.printStackTrace();
map.put("flag", false);
map.put("message", e.getMessage());
reutrn map;
问题:
- 每个人返回的数据有每个人自己的规范,你叫flag他叫isOK,你的成功code是0,它的成功code是0000。这样导致后端书写了大量的异常返回逻辑代码,前端也随之每一个请求一套异常处理逻辑。很多重复代码。
- 如果是前端后端一个人开发还勉强能用,如果前后端分离,这就是系统灾难。
该如何设计异常处理
面向相关方友好
- 后端开发人员职责单一,只需要将异常捕获并转换为自定义异常一直对外抛出。不需要去想页面跳转404,以及异常响应的数据结构的设计。
- 面向前端人员友好,后端返回给前端的数据应该有统一的数据结构,统一的规范。不能一个人一个响应的数据结构。而在此过程中不需要后端开发人员做更多的工作,交给全局异常处理器去处理“异常”到“响应数据结构”的转换。
- 面向用户友好,用户能够清楚的知道异常产生的原因。这就要求自定义异常,全局统一处理,ajax接口请求响应统一的异常数据结构,页面模板请求统一跳转到404页面
- 面向运维友好,将异常信息合理规范的持久化,以日志的形式存储起来,以便查询。
为什么要将系统运行时异常捕获,转换为自定义异常抛出?
答:因为用户不认识ConnectionTimeOutException类似这种异常是什么东西,但是转换为自定义异常就要求程序员对运行时异常进行一个翻译,比如:自定义异常里面应该有message字段,后端程序员应该明确的在message字段里面用面向用户的友好语言,说明服务端发生了什么。
开发规范
- Controller、Service、DAO层拦截异常转换为自定义异常,不允许将异常私自截留。必须对外抛出。
- 统一数据响应代码,使用http状态码,不要自定义。自定义不方便记忆,HTTP状态码程序员都知道。但是太多了程序员也记不住,在项目组规定范围内使用几个就可以。比如:200请求成功,400用户输入错误导致的异常,500系统内部异常,999未知异常。
- 自定义异常里面有message属性,用对用户友好的语言描述异常的发生情况,并赋值给message.
- 不允许对父类Exception统一catch,要分小类catch,这样能够清楚地将异常转换为自定义异常传递给前端。
自定义异常和相关数据结构
该如何设计数据结构
- CustomException 自定义异常。核心要素包含异常错误编码(400,500)、异常错误信息message。
- ExceptionTypeEnum 枚举异常分类,将异常分类固化下来,防止开发人员思维发散。
- AjaxResponse 用于响应HTTP 请求的统一数据结构。
枚举异常的类型
为了防止开发人员大脑发散,每个开发人员都不断的发明自己的异常类型,我们需要规定好异常的类型(枚举)。比如:系统异常、用户(输入)操作导致的异常、其他异常等。
public enum CustomExceptionType
USER_INPUT_ERROR(400,"您输入的数据错误或您没有权限访问资源!"),
SYSTEM_ERROR (500,"系统出现异常,请您稍后再试或联系管理员!"),
OTHER_ERROR(999,"系统出现未知异常,请联系管理员!");
CustomExceptionType(int code, String desc)
this.code = code;
this.desc = desc;
private String desc;//异常类型中文描述
private int code; //code
public String getDesc()
return desc;
public int getCode()
return code;
- 以笔者的经验,最好不要超过5个,否则开发人员将会记不住,也不愿意去记。对于我来说上面的三种异常类型就足够了。
- 这里的code表示异常类型的唯一编码,为了方便大家记忆,就使用Http状态码400、500
- 这里的desc是通用的异常描述,在创建自定义异常的时候,为了给用户更友好的回复,通常异常信息描述应该更具体更友好。
自定义异常
- 自定义异常有两个核心内容,一个是code。使用CustomExceptionType 来限定范围。
- 另外一个是message,这个message信息是要最后返回给前端的,所以需要用友好的提示来表达异常发生的原因或内容
public class CustomException extends RuntimeException
//异常错误编码
private int code ;
//异常信息
private String message;
private CustomException()
public CustomException(CustomExceptionType exceptionTypeEnum)
this.code = exceptionTypeEnum.getCode();
this.message = exceptionTypeEnum.getDesc();
public CustomException(CustomExceptionType exceptionTypeEnum,
String message)
this.code = exceptionTypeEnum.getCode();
this.message = message;
public int getCode()
return code;
@Override
public String getMessage()
return message;
请求接口统一响应数据结构
为了解决不同的开发人员使用不同的结构来响应给前端,导致规范不统一,开发混乱的问题。我们使用如下代码定义统一数据响应结构
- isok表示该请求是否处理成功(即是否发生异常)。true表示请求处理成功,false表示处理失败。
- code对响应结果进一步细化,200表示请求成功,400表示用户操作导致的异常,500表示系统异常,999表示其他异常。与CustomExceptionType枚举一致。
- message:友好的提示信息,或者请求结果提示信息。如果请求成功这个信息通常没什么用,如果请求失败,该信息需要展示给用户。data:通常用于查询数据请求,成功之后将查询数据响应给前端。
/**
* 接口数据请求统一响应数据结构
*/
@Data
public class AjaxResponse
private boolean isok; //请求是否处理成功
private int code; //请求响应状态码
private String message; //请求结果描述信息
private Object data; //请求结果数据(通常用于查询操作)
private AjaxResponse()
//请求出现异常时的响应数据封装
public static AjaxResponse error(CustomException e)
AjaxResponse resultBean = new AjaxResponse();
resultBean.setIsok(false);
resultBean.setCode(e.getCode());
resultBean.setMessage(e.getMessage());
return resultBean;
//请求出现异常时的响应数据封装
public static AjaxResponse error(CustomExceptionType customExceptionType,
String errorMessage)
AjaxResponse resultBean = new AjaxResponse();
resultBean.setIsok(false);
resultBean.setCode(customExceptionType.getCode());
resultBean.setMessage(errorMessage);
return resultBean;
//请求成功的响应,不带查询数据(用于删除、修改、新增接口)
public static AjaxResponse success()
AjaxResponse ajaxResponse = new AjaxResponse();
ajaxResponse.setIsok(true);
ajaxResponse.setCode(200);
ajaxResponse.setMessage("请求响应成功!");
return ajaxResponse;
//请求成功的响应,带有查询数据(用于数据查询接口)
public static AjaxResponse success(Object obj)
AjaxResponse ajaxResponse = new AjaxResponse();
ajaxResponse.setIsok(true);
ajaxResponse.setCode(200);
ajaxResponse.setMessage("请求响应成功!");
ajaxResponse.setData(obj);
return ajaxResponse;
//请求成功的响应,带有查询数据(用于数据查询接口)
public static AjaxResponse success(Object obj,String message)
AjaxResponse ajaxResponse = new AjaxResponse();
ajaxResponse.setIsok(true);
ajaxResponse.setCode(200);
ajaxResponse.setMessage(message);
ajaxResponse.setData(obj);
return ajaxResponse;
对于不同的场景,提供了四种构建AjaxResponse 的方法。
- 当请求成功的情况下,可以使用AjaxResponse.success()构建返回结果给前端。
- 当查询请求等需要返回业务数据,请求成功的情况下,可以使用AjaxResponse.success(data)构建返回结果给前端。携带结果数据。
- 当请求处理过程中发生异常,需要将异常转换为CustomException ,然后在控制层使用AjaxResponse .error(CustomException)构建返回结果给前端。
- 在某些情况下,没有任何异常产生,我们判断某些条件也认为请求失败。这种使用AjaxResponse.error(customExceptionType,errorMessage)构建响应结果。
使用示例如下
例如:更新操作,Controller无需返回额外的数据
return AjaxResponse.success();
例如:查询接口,Controller需返回结果数据(data可以是任何类型数据)
return AjaxResponse.success(data);
通用全局异常处理逻辑
通用异常处理逻辑
程序员的异常处理逻辑要十分的单一:无论在Controller层、Service层还是什么其他位置,程序员只负责一件事:那就是捕获异常,并将异常转换为自定义异常。使用用户友好的信息去填充CustomException的message,并将CustomException抛出去。
@Service
public class ExceptionService
//服务层,模拟系统异常
public void systemBizError()
try
Class.forName("com.mysql.jdbc.xxxx.Driver");
catch (ClassNotFoundException e)
throw new CustomException(
CustomExceptionType.SYSTEM_ERROR,
"在XXX业务,myBiz()方法内,出现ClassNotFoundException,请将该信息告知管理员");
//服务层,模拟用户输入数据导致的校验异常
public void userBizError(int input)
if(input < 0) //模拟业务校验失败逻辑
throw new CustomException(
CustomExceptionType.USER_INPUT_ERROR,
"您输入的数据不符合业务逻辑,请确认后重新输入!");
//…… 其他的业务
全局异常处理器
通过团队内的编码规范的要求,我们已经知道了:不允许程序员截留处理Exception,必须把异常转换为自定义异常CustomException全都抛出去。那么程序员把异常跑出去之后由谁来处理?那就是ControllerAdvice。
ControllerAdvice注解的作用就是监听所有的Controller,一旦Controller抛出CustomException,就会在@ExceptionHandler(CustomException.class)注解的方法里面对该异常进行处理。
处理方法很简单就是使用AjaxResponse.error(e)包装为通用的接口数据结构返回给前端。
@ControllerAdvice
public class WebExceptionHandler
//处理程序员主动转换的自定义异常
@ExceptionHandler(CustomException.class)
@ResponseBody
public AjaxResponse customerException(CustomException e)
if(e.getCode() == CustomExceptionType.SYSTEM_ERROR.getCode())
//400异常不需要持久化,将异常信息以友好的方式告知用户就可以
//TODO 将500异常信息持久化处理,方便运维人员处理
return AjaxResponse.error(e);
//处理程序员在程序中未能捕获(遗漏的)异常
@ExceptionHandler(Exception.class)
@ResponseBody
public AjaxResponse exception(Exception e)
//TODO 将异常信息持久化处理,方便运维人员处理
return AjaxResponse.error(new CustomException(
CustomExceptionType.OTHER_ERROR));
业务状态与HTTP协议状态一致
不知道大家有没有注意到一个问题(看上图)?这个问题就是我们的AjaxResponse的code是400,但是真正的HTTP协议状态码是200?
通说的说,目前
- AjaxResponse的code是400代表的是业务状态,也就是说用户的请求业务失败了
- 但是HTTP请求是成功的,也就是说数据是正常返回的。
在很多的公司开发RESTful服务时,要求HTTP状态码能够体现业务的最终执行状态,所以说:我们有必要让业务状态与HTTP协议Response状态码一致。
@Component
@ControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice
@Override
public boolean supports(MethodParameter returnType, Class converterType)
//return returnType.hasMethodAnnotation(ResponseBody.class);
return true;
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response)
//如果响应结果是JSON数据类型
if(selectedContentType.equalsTypeAndSubtype(
MediaType.APPLICATION_JSON))
//为HTTP响应结果设置状态码,状态码就是AjaxResponse的code,二者达到统一
response.setStatusCode(
HttpStatus.valueOf(((AjaxResponse) body).getCode())
);
return body;
return body;
- 实现ResponseBodyAdvice 接口的作用是:在将数据返回给用户之前,做最后一步的处理。也就是说,ResponseBodyAdvice 的处理过程在全局异常处理的后面。
进一步优化
我们已经知道了,ResponseBodyAdvice 接口的作用是:在将数据返回给用户之前,做最后一步的处理。将上文的GlobalResponseAdvice 中beforeBodyWrite方法代码优化如下。
- 如果Controller或全局异常处理响应的结果body是AjaxResponse,就直接return给前端。
- 如果Controller或全局异常处理响应的结果body不是AjaxResponse,就将body封装为AjaxResponse之后再return给前端。
所以,我们之前的代码是这样写的,比如:某个controller方法返回值
return AjaxResponse.success(objList);
现在就可以这样写了,因为在GlobalResponseAdvice 里面会统一再封装为AjaxResponse。
return objList;
最终代码如下:
@Component
@ControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice
@Override
public boolean supports(MethodParameter methodParameter, Class aClass)
return true;
@Override
public Object beforeBodyWrite(Object body,
MethodParameter methodParameter,
MediaType mediaType,
Class aClass,
ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse)
//如果响应结果是JSON数据类型
if(mediaType.equalsTypeAndSubtype(
MediaType.APPLICATION_JSON))
if(body instanceof AjaxResponse)
AjaxResponse ajaxResponse = (AjaxResponse)body;
if(ajaxResponse.getCode() != 999) //999 不是标准的HTTP状态码,特殊处理
serverHttpResponse.setStatusCode(HttpStatus.valueOf(
ajaxResponse.getCode()
));
return body;
else
serverHttpResponse.setStatusCode(HttpStatus.OK);
return AjaxResponse.success(body);
return body;
服务端数据校验异常处理逻辑
异常校验的规范及常用注解
在web开发时,对于请求参数,一般上都需要进行参数合法性校验的,原先的写法时一个个字段一个个去判断,这种方式太不通用了,所以java的JSR 303: Bean Validation规范就是解决这个问题的。
JSR 303只是个规范,并没有具体的实现,目前通常都是才有hibernate-validator进行统一参数校验。
JSR303定义的校验类
Hibernate Validator 附加的 constraint
用法:把以上注解加在ArticleVO的属性字段上,然后在参数校验的方法上加@Valid注解 如:
当用户输入参数不符合注解给出的校验规则的时候,会抛出BindException或MethodArgumentNotValidException。
Assert断言与IllegalArgumentException
之前给大家讲通用异常处理的时候,用户输入异常判断是这样处理的。这种方法也是可以用的,但是我们学了这么多的知识,可以优化一下
//服务层,模拟用户输入数据导致的校验异常
public void userBizError(int input)
if(input < 0) //模拟业务校验失败逻辑
throw new CustomException(
CustomExceptionType.USER_INPUT_ERROR,
"您输入的数据不符合业务逻辑,请确认后重新输入!");
//…… 其他的业务
更好的写法是下面这样的,使用org.springframework.util.Assert断言input >= 0,如果不满足条件就抛出IllegalArgumentException,参数不合法的异常。
//服务层,模拟用户输入数据导致的校验异常
public void userBizError(int input)
Assert.isTrue重学SpringBoot系列之统一全局异常处理