一.前言
hello,everyone,周末愉快。双休日大家都去干嘛了?看MSI?看速9?刷剧?出去喝酒蹦迪?野炊春游。。。all right~假期总是过得很快,周末在家刷了绝命毒师,不愧是每一季豆瓣频分9+的神剧,全程无尿点,推荐大家观看。
言归正传,玩归玩,闹归闹,不能拿bug开玩笑。日常工作编写代码的过程中,随手留下bug那是程序员再正常不过的事情了。程序出现了bug,总会有对应的日志信息产生,后端抛出的堆栈错误,不可能直接抛到前端。试想,用户搜索一件不存在的商品时,后端代码有bug【正常业务代码这里还是会去校验一下商品是否存在的】,报了空指针异常,这是不做任何错误包装,直接将空指针异常的堆栈信息返回给用户。这下好了,领导不请你喝杯茶说不过去了。
那么我们该怎么来处理这些个抛异常的问题呢?本文就将给大家带来spring中如何优雅定制全局异常,如果本文写的有不对或者大家觉得有更好的方式,欢迎留言指正,salute!
二.异常
既然要谈一谈全局异常处理,那我们先要知道java中的异常体系。
说明
1.Throwable
所有的异常都是Throwable的直接或者间接子类。Throwable有两个直接子类,Error和Exception。
2.Error
Error是错误,对于所有的编译时期的错误以及系统错误都是通过Error抛出的。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
3.Exception
它规定的异常是程序本身可以处理的异常。异常和错误的区别是,异常是可以被处理的,而错误是没法处理的。
4.Checked Exception【受检异常】
可检查的异常,这是编码时非常常用的,所有checked exception都是需要在代码中处理的。它们的发生是可以预测的,正常的一种情况,可以合理的处理。例如IOException。
5.Unchecked Exception【非受检异常】
RuntimeException及其子类都是unchecked exception。比如NPE空指针异常,除数为0的算数异常ArithmeticException等等,这种异常是运行时发生,无法预先捕捉处理的。Error也是unchecked exception,也是无法预先处理的。
三.异常处理的方式
1.try-catch-finally
这种方式是单体业务方法中最常见的处理方式,对于try块内的业务逻辑预知可能会产生异常做处理。
例如读取文件会强制要求你处理受检查异常 IOException
/**
* 读取文件内容
* @param fileName
* @return
*/
public String readFileContent(String fileName) {
File file = new File(fileName);
BufferedReader reader = null;
StringBuffer sbf = new StringBuffer();
try {
reader = new BufferedReader(new FileReader(file));
String tempStr;
while ((tempStr = reader.readLine()) != null) {
sbf.append(tempStr).append("\n");
}
reader.close();
return sbf.toString();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
//doSomething
}
return sbf.toString();
}
复制代码
2.try-with-resource-finally
try-with-resources 是JDK 7中一个新的异常处理机制,它能够很容易(优雅)地关闭在 try-catch 语句块中使用的资源。在第一种处理的过程中,finally中还要去手动关闭流。使用try-with-resource-finally就可以帮你节省这一步代码。
/**
* 读取文件内容
* @param fileName
* @return
*/
public String readFileContent(String fileName) {
File file = new File(fileName);
StringBuffer sbf = new StringBuffer();
try ( BufferedReader reader = new BufferedReader(new FileReader(file))){
String tempStr;
while ((tempStr = reader.readLine()) != null) {
sbf.append(tempStr).append("\n");
}
reader.close();
return sbf.toString();
} catch (IOException e) {
e.printStackTrace();
} finally {
//doSomething
}
return sbf.toString();
}
复制代码
3.全局异常处理
上面两种方法是在方法内部处理了可以预见的异常,那如果发生了不可预知的异常呢?有的朋友会说,我直接用catch(Exception ex)包裹处理异常。但是如果在微服务中,订单中心调用支付中心,支付中心异常了,支付中心自己把发生的异常捕获了,订单中心认为支付成功,将订单下单成功,这就凉凉了。。。
因此在支付中心必须将异常抛出,告知订单中心,我这里发生了异常了。订单中心接受到了异常,终止处理。终止处理总要给前端一个错误码,这个错误码怎么定义呢?try-catch吗?那这个还只是一个下订单的场景,如果每个业务场景我都要单独定一个错误码,我每个方法都定义一个try-catch块吗?显然这是不可能的,且不说大量的try-catch块会影响程序的运行效率,让你写着多异常处理我估计你都能烦死了。这时候我们就需要全局异常处理了。对于特定的业务异常,定义code码返回给全局异常处理,全局处理器解析code码映射业务异常返回标准输出给前端展示。
四.spring中处理全局异常
4.1.@ExceptionHandler
统一处理某一类异常,从而能够减少代码重复率和复杂度
1.未处理异常请求
@RestController
public class TestController {
@RequestMapping("/test")
public Object test(){
//抛出java.lang.ArithmeticException: / by zero 异常
int i = 1 / 0;
return new Date();
}
}
复制代码
展示
2.处理异常请求
public class TestController {
@RequestMapping("/test")
public Object test(){
int i = 1 / 0;
return new Date();
}
@ExceptionHandler({RuntimeException.class})
public Object catchException(Exception ex){
return ex.getMessage();
}
}
复制代码
展示
4.2.HandlerExceptionResolver
异常集中处理:接口形式处理异常
1.未处理异常请求
与4.1一致
2.处理异常请求
@Component
public class GlobalExceptionHandler implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelMap mmp=new ModelMap();
mmp.addAttribute("ex",ex.getMessage());
response.addHeader("Content-Type","application/json;charset=UTF-8");
try {
new ObjectMapper().writeValue(response.getWriter(),ex.getMessage());
response.getWriter().flush();
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView();
}
}
复制代码
4.3@ControllerAdvice与@ExceptionHandler组合
异常集中处理:注解形式
1.未处理异常请求
与4.1一致
2.处理异常请求
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public Object handle(RuntimeException ex) {
return ex.getMessage();
}
}
复制代码
好学的小伙伴可能想知道上面三种方式的原理,贴上一个源码解析链接:www.cnblogs.com/lvbinbin2yu…
五.优雅异常返回
5.1.统一数据返回格式
ok,知道了异常的种类,统一捕获异常的方式,那么我们如何跟前端同事约定数据返回呢?总不能后端每个接口都告诉前端说我这个接口返回异常报文字符串,另一个接口正常数据返回是个List结构。那我估计前端兄弟一定要对你重拳出击了
那么定义一个统一的返回实体是很重要的,不废话直接上代码
//基础前后端交互实体,定义了前后端交互过程中,数据返回的标准格式
@Data
public class BaseResult {
/**
* httpCode
*/
private Integer code;
/**
* 业务code
*/
private String errorCode;
/**
* 业务信息
*/
private String message;
/**
* 链路id【微服务请求调用链路跟踪,不了解此概念的,可以看一下我的另一篇博客:https://juejin.cn/post/6923004276335869960】
*/
private String traceId;
public BaseResult() {
}
public BaseResult(Integer code, String message) {
this.code = code;
this.message = message;
}
public BaseResult(Integer code, String errorCode, String message) {
this.code = code;
this.errorCode = errorCode;
this.message = message;
}
/**
* 通用业务请求状态码
*/
public static final Integer CODE_SUCCESS = 200;
public static final Integer CODE_SYSTEM_ERROR = 500;
/**
* 通用请求信息
*/
public static final String SYSTEM_ERROR = "系统错误";
public static final String MESSAGE_SUCCESS = "请求成功";
public static final String QUERY_SUCCESS = "查询成功";
public static final String INSERT_SUCCESS = "新增成功";
public static final String UPDATE_SUCCESS = "更新成功";
public static final String DELETE_SUCCESS = "删除成功";
public static final String IMPORT_SUCCESS = "导入成功";
public static final String EXPORT_SUCCESS = "导出成功";
public static final String DOWNLOAD_SUCCESS = "下载成功";
}
复制代码
@Data
@EqualsAndHashCode(callSuper = true)
public class Result<T> extends BaseResult {
//业务数据返回放置
private T data;
public Result() {
}
public Result(Integer code, String message, T data) {
super(code, message);
this.data = data;
}
public Result(Integer code, String errorCode, String message, T data) {
super(code, errorCode, message);
this.data = data;
}
public boolean success() {
return CODE_SUCCESS.equals(getCode());
}
public boolean systemFail() {
return CODE_SYSTEM_ERROR.equals(getCode());
}
public static Result<Object> ok() {
return new Result<>(CODE_SUCCESS, "", null);
}
public static Result<Object> ok(String message) {
return new Result<>(CODE_SUCCESS, message, null);
}
public static <T> Result<T> success(T data) {
return new Result<>(CODE_SUCCESS, MESSAGE_SUCCESS, data);
}
public static <T> Result<T> success(T data, String message) {
return new Result<>(CODE_SUCCESS, message, data);
}
public static Result<Object> error(String message) {
return Result.error(CODE_SYSTEM_ERROR, null, message, null);
}
public static Result<Object> error(String errorCode, String message) {
return Result.error(CODE_SYSTEM_ERROR, errorCode, message, null);
}
public static Result<Object> error(Integer code, String errorCode, String message, Object data) {
return new Result<>(code, errorCode, message, data);
}
}
复制代码
//如果是列表页,那必然要返回给前端数据总条数,不然前端不好计算你一共有几页
@Data
@EqualsAndHashCode(callSuper = true)
public class PageResult<T> extends BaseResult {
private Long total;
private List<T> data;
public PageResult() {
}
public static <T> PageResult<T> ok(Page<T> result) {
PageResult<T> pageResult = new PageResult<>();
pageResult.setCode(CODE_SUCCESS);
pageResult.setMessage(QUERY_SUCCESS);
pageResult.setTotal(result.getTotal());
pageResult.setData(result.getRecords());
return pageResult;
}
}
复制代码
5.2.异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public Object handle(RuntimeException ex) {
return Result.error(errorCode.toString(), errorMessage.toString());
}
}
复制代码
现在我们来看一下,还是用上面的的demo代码,看一下异常返回是什么。
5.3.异常标准化处理
看到这里小伙伴可能还觉得这个异常处理还是没什么东西,不还是吧代码里面的异常给抛了出来,前端是会直接展示message里面的信息用作用户操作结束的提示的。你报错了,返回了一个/by zero。用户鬼知道他的操作发生了什么。所以这里我们还需要针对不同的异常,需要有不同的业务异常提示映射机制。
全局业务异常处理用映射规则,我们用什么比较好呢?跟我的异常能够匹配,返回的是我定制的业务提示?
国际化功能啊!!!
关于国际化功能,小伙伴如果有不了解的,可以参考这篇文章:blog.csdn.net/u012234419/…
我在国际化配置文件中定义code码,业务异常抛出对应的code码,全局异常中来映射不就好了?
ok,上代码【这里为了演示方便,仅提供中文版的国际化code对应】
5.3.1.定义messages.properties
写入内容
id.is.null=用户id不可为空
复制代码
5.3.2.定义国际化配置类
@Component
public class SpringMessageSourceErrorMessageSource {
@Autowired
private MessageSource messageSource;
@Override
public String getMessage(String code, Object... params) {
return messageSource.getMessage(code, params, LocaleContextHolder.getLocale());
}
@Override
public String getMessage(String code, String defaultMessage, Object... params) {
return messageSource.getMessage(code, params, defaultMessage, LocaleContextHolder.getLocale());
}
}
复制代码
5.3.3.统一业务异常
所有本系统内的业务异常继承此异常,全局异常可通过捕获该异常来处理业务异常
//最高父类业务异常
@Data
@EqualsAndHashCode(callSuper = true)
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = 430933593095358673L;
private String errorMessage;
private String errorCode;
/**
* 构造新实例。
*/
public ServiceException() {
super();
}
/**
* 用给定的异常信息构造新实例。
* @param errorMessage 异常信息。
*/
public ServiceException(String errorMessage) {
super((String)null);
this.errorMessage = errorMessage;
}
//省略部分代码
}
复制代码
//子类参数校验业务异常
@EqualsAndHashCode(callSuper = true)
public class ValidationException extends ServiceException {
@Getter
private Object[] params;
public ValidationException(String message) {
super(message);
}
public ValidationException(String message, Object[] params) {
super(message);
this.params = params;
}
public ValidationException(String code, String message, Object[] params) {
super(code, message);
this.params = params;
}
public static ValidationException of(String code, Object[] params) {
return new ValidationException(code, null, params);
}
}
复制代码
5.3.4.定义子类异常校验工具类
public class ValidationUtil {
public static void isTrue(boolean expect, String code, Object... params) {
if (!expect) {
throw ValidationException.of(code, params);
}
}
public static void isFalse(boolean expect, String code, Object... params) {
isTrue(!expect, code, params);
}
//省略部分代码
}
复制代码
5.3.5.定义全局异常处理
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@Autowired
private SpringMessageSourceErrorMessageSource messageSource;
@ExceptionHandler(ConstraintViolationException.class)
public Object handle(ConstraintViolationException ex) {
StringBuilder errorCode = new StringBuilder();
StringBuilder errorMessage = new StringBuilder();
ex.getConstraintViolations()
.stream()
.forEach(error -> {
if (StrUtil.isNotBlank(errorCode.toString())) {
errorCode.append(",");
}
errorCode.append(error.getMessageTemplate());
if (StrUtil.isNotBlank(errorMessage.toString())) {
errorMessage.append(",");
}
errorMessage.append(error.getMessage());
});
return Result.error(errorCode.toString(), errorMessage.toString());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Object handle(MethodArgumentNotValidException ex) {
StringBuilder errorCode = new StringBuilder();
StringBuilder errorMessage = new StringBuilder();
buildBindingResult(errorCode, errorMessage, ex.getBindingResult());
return Result.error(errorCode.toString(), errorMessage.toString());
}
@ExceptionHandler(ValidationException.class)
public Object handle(ValidationException ex) {
String errorMessage = messageSource.getMessage(ex.getErrorCode(), ex.getMessage(), ex.getParams());
return Result.error(ex.getErrorCode(), errorMessage);
}
@ExceptionHandler(ServiceException.class)
public Object handle(ServiceException ex) {
return Result.error(ex.getErrorCode(), ex.getMessage());
}
@ExceptionHandler(Throwable.class)
public Object handle(Throwable ex) {
log.error("全局异常", ex);
return Result.error(BaseResult.SYSTEM_ERROR);
}
/**
* 获取国际化数据
* @param messageTemplate 消息模板
* @return
*/
private String getFromMessageTemplate(String messageTemplate) {
if(StrUtil.isBlank(messageTemplate)){
return null;
}
if (messageTemplate.length() < 2) {
return null;
}
return messageTemplate.substring(1, messageTemplate.length() - 1);
}
/**
* 构建并绑定返回结果
* @param errorCode 错误code
* @param errorMessage 国际化错误信息
* @param bindingResult 需要处理的错误信息
*/
private void buildBindingResult(StringBuilder errorCode, StringBuilder errorMessage, BindingResult bindingResult) {
List<ObjectError> errors = bindingResult.getAllErrors();
errors
.stream()
.forEach(error -> {
if (error.contains(ConstraintViolation.class)) {
ConstraintViolation constraintViolation = error.unwrap(ConstraintViolation.class);
if (errorCode.length() > 0) {
errorCode.append(",");
}
errorCode.append(getFromMessageTemplate(constraintViolation.getMessageTemplate()));
}
if (errorMessage.length() > 0) {
errorMessage.append(",");
}
String errorInfo = messageSource.getMessage(getFromMessageTemplate(error.getDefaultMessage()), null, (Object) null);
errorMessage.append(errorInfo);
});
}
}
复制代码
5.4.演示
通过以上配置后,demo项目结构如下
├── demo.iml
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── examp
│ ├── DemoApplication.java
│ ├── config
│ │ └── SpringMessageSourceErrorMessageSource.java
│ ├── controller
│ │ └── TestController.java
│ ├── exception
│ │ ├── ServiceException.java
│ │ └── ValidationException.java
│ ├── handler
│ │ └── GlobalExceptionHandler.java
│ ├── model
│ │ ├── BaseResult.java
│ │ ├── Result.java
│ │ └── User.java
│ └── util
│ └── ValidationUtil.java
└── resources
├── application.yml
├── logback-spring.xml
└── messages.properties
复制代码
演示一下异常处理的效果
1.messages.properties配置文件中添加
id.is.null=用户id不可为空
id.is.can.not.be.one=用户id不可以等于1
userName.is.blank=用户名不可为空
复制代码
2.新建用户类
@Data
public class User {
//定义用户id不可为空,否则报错
@NotNull(message = "{id.is.null}")
private Long id;
@NotBlank(message = "{userName.is.blank}")
private String userName;
}
复制代码
3.测试方法
@RestController
public class TestController {
@PostMapping
//1.参数校验命中
public Object add(@RequestBody @Valid User user) throws Exception{
//2.工具类校验命中
ValidationUtil.isFalse(Objects.equals(user.getId(),1L),"id.is.can.not.be.one");
//3.业务异常校验命中
if(Objects.equals(user.getId(),2L)){
throw new ServiceException("用户id不可为2");
}
//4.非业务异常命中
if(Objects.equals(user.getId(),3L)){
throw new Exception("用户id不可为3");
}
//5.正确逻辑执行
System.out.println(user.toString());
return Result.ok(BaseResult.INSERT_SUCCESS);
}
}
复制代码
5.4.1.参数校验
post中body参数
{
}
命中校验规则:1
控制台输出:
{
"code": 500,
"errorCode": "id.is.null,userName.is.blank",
"message": "用户id不可为空,用户名不可为空",
"traceId": null,
"data": null
}
复制代码
5.4.2.工具类校验
post中body参数
{
"id":1,
"userName":"柏炎"
}
命中校验规则:2
控制台输出:
{
"code": 500,
"errorCode": "id.is.can.not.be.one",
"message": "用户id不可以等于1",
"traceId": null,
"data": null
}
复制代码
5.4.3.业务异常校验
post中body参数
{
"id":2,
"userName":"柏炎"
}
命中校验规则:3
控制台输出:
{
"code": 500,
"errorCode": null,
"message": "用户id不可为2",
"traceId": null,
"data": null
}
复制代码
5.4.4.非业务异常
post中body参数
{
"id":3,
"userName":"柏炎"
}
命中校验规则:4
控制台输出:
{
"code": 500,
"errorCode": null,
"message": "系统错误",
"traceId": null,
"data": null
}
复制代码
5.4.5.正常执行
post中body参数
{
"id":4,
"userName":"柏炎"
}
命中校验规则:无
控制台输出:
{
"code": 200,
"errorCode": null,
"message": "新增成功",
"traceId": null,
"data": null
}
复制代码
5.5.全局异常处理流程图
六.总结
本文详细介绍如何在spring优雅的使用全局异常的过程,现做以下总结及建议:
1.方法入参如果为body形式,使用spring校验规则进行参数预检查
2.减少if/else的逻辑异常抛出,使用逻辑校验工具类
3.内外部受检查的业务异常捕获返回包装后的信息抛出给前端
4.无法预测的异常在兜底的@ExceptionHandler(Throwable.class)最高异常捕获类中处理,严禁将未做包装的代码异常直接返回给前端
5.不做抛出的异常在自己捕获的地方做必要的日志打印,便于问题定位与跟踪
七.源码获取
本文核心内容已经收录至博主的github中,感兴趣的小伙伴可以自取:
八.参考
九.联系我
如果你觉得文章写得不错,能够点赞评论+关注三连,么么哒~
钉钉:louyanfeng25
微信:baiyan_lou