前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring中优雅的处理全局异常

Spring中优雅的处理全局异常

作者头像
柏炎
发布2022-08-23 14:15:51
2.5K0
发布2022-08-23 14:15:51
举报
文章被收录于专栏:深入浅出java后端

一.前言

​ hello,everyone,周末愉快。双休日大家都去干嘛了?看MSI?看速9?刷剧?出去喝酒蹦迪?野炊春游。。。all right~假期总是过得很快,周末在家刷了绝命毒师,不愧是每一季豆瓣频分9+的神剧,全程无尿点,推荐大家观看。

​ 言归正传,玩归玩,闹归闹,不能拿bug开玩笑。日常工作编写代码的过程中,随手留下bug那是程序员再正常不过的事情了。程序出现了bug,总会有对应的日志信息产生,后端抛出的堆栈错误,不可能直接抛到前端。试想,用户搜索一件不存在的商品时,后端代码有bug【正常业务代码这里还是会去校验一下商品是否存在的】,报了空指针异常,这是不做任何错误包装,直接将空指针异常的堆栈信息返回给用户。这下好了,领导不请你喝杯茶说不过去了。

​ 那么我们该怎么来处理这些个抛异常的问题呢?本文就将给大家带来spring中如何优雅定制全局异常,如果本文写的有不对或者大家觉得有更好的方式,欢迎留言指正,salute!

二.异常

既然要谈一谈全局异常处理,那我们先要知道java中的异常体系。

2.png
2.png

说明

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

代码语言:javascript
复制
/**
 * 读取文件内容
 * @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就可以帮你节省这一步代码。

代码语言:javascript
复制
/**
 * 读取文件内容
 * @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)包裹处理异常。但是如果在微服务中,订单中心调用支付中心,支付中心异常了,支付中心自己把发生的异常捕获了,订单中心认为支付成功,将订单下单成功,这就凉凉了。。。

3.jpeg
3.jpeg

因此在支付中心必须将异常抛出,告知订单中心,我这里发生了异常了。订单中心接受到了异常,终止处理。终止处理总要给前端一个错误码,这个错误码怎么定义呢?try-catch吗?那这个还只是一个下订单的场景,如果每个业务场景我都要单独定一个错误码,我每个方法都定义一个try-catch块吗?显然这是不可能的,且不说大量的try-catch块会影响程序的运行效率,让你写着多异常处理我估计你都能烦死了。这时候我们就需要全局异常处理了。对于特定的业务异常,定义code码返回给全局异常处理,全局处理器解析code码映射业务异常返回标准输出给前端展示。

四.spring中处理全局异常

4.1.@ExceptionHandler

统一处理某一类异常,从而能够减少代码重复率和复杂度

1.未处理异常请求

代码语言:javascript
复制
@RestController
public class TestController {

    @RequestMapping("/test")
    public Object test(){
            //抛出java.lang.ArithmeticException: / by zero 异常
        int i = 1 / 0;
        return new Date();
    }
}

展示

image-20210523151242414.png
image-20210523151242414.png

2.处理异常请求

代码语言:javascript
复制
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();
    }
}

展示

image-20210523151426256.png
image-20210523151426256.png

4.2.HandlerExceptionResolver

异常集中处理:接口形式处理异常

1.未处理异常请求

与4.1一致

2.处理异常请求

代码语言:javascript
复制
@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();
    }

}
image-20210523151904317.png
image-20210523151904317.png

4.3@ControllerAdvice与@ExceptionHandler组合

异常集中处理:注解形式

1.未处理异常请求

与4.1一致

2.处理异常请求

代码语言:javascript
复制
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public Object handle(RuntimeException ex) {
        return ex.getMessage();
    }

}
image-20210523152345328.png
image-20210523152345328.png

好学的小伙伴可能想知道上面三种方式的原理,贴上一个源码解析链接:https://www.cnblogs.com/lvbinbin2yujie/p/10574812.html#type2

五.优雅异常返回

5.1.统一数据返回格式

ok,知道了异常的种类,统一捕获异常的方式,那么我们如何跟前端同事约定数据返回呢?总不能后端每个接口都告诉前端说我这个接口返回异常报文字符串,另一个接口正常数据返回是个List结构。那我估计前端兄弟一定要对你重拳出击了

那么定义一个统一的返回实体是很重要的,不废话直接上代码

代码语言:javascript
复制
//基础前后端交互实体,定义了前后端交互过程中,数据返回的标准格式
@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 = "下载成功";

}
代码语言:javascript
复制
@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);
    }

}
代码语言:javascript
复制
//如果是列表页,那必然要返回给前端数据总条数,不然前端不好计算你一共有几页
@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.异常处理

代码语言:javascript
复制
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public Object handle(RuntimeException ex) {
        return Result.error(errorCode.toString(), errorMessage.toString());
    }

}

现在我们来看一下,还是用上面的的demo代码,看一下异常返回是什么。

image-20210523153906814.png
image-20210523153906814.png

5.3.异常标准化处理

​ 看到这里小伙伴可能还觉得这个异常处理还是没什么东西,不还是吧代码里面的异常给抛了出来,前端是会直接展示message里面的信息用作用户操作结束的提示的。你报错了,返回了一个/by zero。用户鬼知道他的操作发生了什么。所以这里我们还需要针对不同的异常,需要有不同的业务异常提示映射机制。

​ 全局业务异常处理用映射规则,我们用什么比较好呢?跟我的异常能够匹配,返回的是我定制的业务提示?

国际化功能啊!!!

关于国际化功能,小伙伴如果有不了解的,可以参考这篇文章:https://blog.csdn.net/u012234419/article/details/49616527

我在国际化配置文件中定义code码,业务异常抛出对应的code码,全局异常中来映射不就好了?

ok,上代码【这里为了演示方便,仅提供中文版的国际化code对应】

5.3.1.定义messages.properties

写入内容

代码语言:javascript
复制
id.is.null=用户id不可为空

5.3.2.定义国际化配置类

代码语言:javascript
复制
@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.统一业务异常

所有本系统内的业务异常继承此异常,全局异常可通过捕获该异常来处理业务异常

代码语言:javascript
复制
//最高父类业务异常
@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;
    }

    //省略部分代码

}
代码语言:javascript
复制
//子类参数校验业务异常
@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.定义子类异常校验工具类

代码语言:javascript
复制
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.定义全局异常处理

代码语言:javascript
复制
@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项目结构如下

代码语言:javascript
复制
├── 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配置文件中添加

代码语言:javascript
复制
id.is.null=用户id不可为空
id.is.can.not.be.one=用户id不可以等于1
userName.is.blank=用户名不可为空

2.新建用户类

代码语言:javascript
复制
@Data
public class User {
        
        //定义用户id不可为空,否则报错
    @NotNull(message = "{id.is.null}")
    private Long id;

    @NotBlank(message = "{userName.is.blank}")
    private String userName;
}

3.测试方法

代码语言:javascript
复制
@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.参数校验

代码语言:javascript
复制
post中body参数
{
   
}

命中校验规则:1

控制台输出:
{
    "code": 500,
    "errorCode": "id.is.null,userName.is.blank",
    "message": "用户id不可为空,用户名不可为空",
    "traceId": null,
    "data": null
}

5.4.2.工具类校验

代码语言:javascript
复制
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.业务异常校验

代码语言:javascript
复制
post中body参数
{
    "id":2,
    "userName":"柏炎"
}

命中校验规则:3

控制台输出:
{
    "code": 500,
    "errorCode": null,
    "message": "用户id不可为2",
    "traceId": null,
    "data": null
}

5.4.4.非业务异常

代码语言:javascript
复制
post中body参数
{
    "id":3,
    "userName":"柏炎"
}

命中校验规则:4

控制台输出:
{
    "code": 500,
    "errorCode": null,
    "message": "系统错误",
    "traceId": null,
    "data": null
}

5.4.5.正常执行

代码语言:javascript
复制
post中body参数
{
    "id":4,
    "userName":"柏炎"
}

命中校验规则:无

控制台输出:
{
    "code": 200,
    "errorCode": null,
    "message": "新增成功",
    "traceId": null,
    "data": null
}

5.5.全局异常处理流程图

image-20210523164540066.png
image-20210523164540066.png

六.总结

本文详细介绍如何在spring优雅的使用全局异常的过程,现做以下总结及建议:

1.方法入参如果为body形式,使用spring校验规则进行参数预检查

2.减少if/else的逻辑异常抛出,使用逻辑校验工具类

3.内外部受检查的业务异常捕获返回包装后的信息抛出给前端

4.无法预测的异常在兜底的@ExceptionHandler(Throwable.class)最高异常捕获类中处理,严禁将未做包装的代码异常直接返回给前端

5.不做抛出的异常在自己捕获的地方做必要的日志打印,便于问题定位与跟踪

七.源码获取

本文核心内容已经收录至博主的github中,感兴趣的小伙伴可以自取:

https://github.com/louyanfeng25/common-frame/blob/master/common-interaction/src/main/java/com/baiyan/common/interaction/config/GlobalExceptionHandler.java

八.参考

https://blog.csdn.net/writebook2016/article/details/79291663

https://blog.csdn.net/michaelgo/article/details/82790253

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-05-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.前言
  • 二.异常
  • 三.异常处理的方式
  • 四.spring中处理全局异常
    • 4.1.@ExceptionHandler
      • 4.2.HandlerExceptionResolver
        • 4.3@ControllerAdvice与@ExceptionHandler组合
        • 五.优雅异常返回
          • 5.1.统一数据返回格式
            • 5.2.异常处理
              • 5.3.异常标准化处理
                • 5.3.1.定义messages.properties
                • 5.3.2.定义国际化配置类
                • 5.3.3.统一业务异常
                • 5.3.4.定义子类异常校验工具类
                • 5.3.5.定义全局异常处理
              • 5.4.演示
                • 5.4.1.参数校验
                • 5.4.2.工具类校验
                • 5.4.3.业务异常校验
                • 5.4.4.非业务异常
                • 5.4.5.正常执行
              • 5.5.全局异常处理流程图
              • 六.总结
              • 七.源码获取
              • 八.参考
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档