26. 从零开始学springboot-全局异常处理

前言

无论什么项目,异常处理和数据校验都显得尤其重要。作为一个开发,我们不应该不对数据检验就直接入库,我们也不应该傻乎乎的把乱糟糟的报错信息直接返回给用户。本章,我们就讲讲sprinboot的异常和数据校验处理。

异常处理流程

  • 自定义异常类型
  • 自定义错误代码及错误信息
  • 对于可预知的异常由程序员在代码中主动抛出,有springboot统一捕获,可预知异常是程序员在代码中手动抛出本系统定义的特定业务异常类型,由于是程序员抛出的异常,通常异常信息比较齐全,程序员在抛出时会指定错误代码及错误信息,获取异常信息也比较方便
  • 对于不可预知的异常(运行时异常)由springboot统一捕获Exception类型的异常,不可预知的异常通常是由于系统出现bug、或一些不可抗拒的错误(比如网络中断、服务器宕机等),异常类型为RuntimeException类型(运行时异常)
  • 可预知异常及不可预知异常最终都会采用统一的信息格式(错误代码+错误信息)来表示,最终也会随请求响应给客户端

默认异常处理

默认情况下,sprinboot为两种情况提供了不同的响应方式。

  • 浏览器异常处理
    浏览器客户端请求一个不存在的页面或服务端处理发生异常时,一般情况下浏览器默认发送的请求头中Accept: text/html,sprinboot默认会响应一个html文档内容,称作“Whitelabel Error Page”。


    1.png
  • 非浏览器异常处理
    如postman等调试工具发送请求一个不存在的url或服务端处理发生异常时,sprinboot会返回类似如下的Json格式字符串信息


    2.png

默认异常处理原理简介

sprinboot默认提供了程序出错的结果映射路径“/error”。这个“/error”请求会在BasicErrorController类中处理
我们找到该类,简单看些核心实现


3.png

不难看出其内部是通过判断请求头中的Accept的内容是否为text/html来区分请求是来自浏览器,还是其它接口的调用,以此来决定返回页面视图还是 json 消息内容。

全局异常处理

以上介绍了springboot默认的异常处理机制,那么,问题来了,我们实际业务中肯定是需要统一的消息体的。所以,我们就需要实现ErrorController这个接口,来统一返回的消息体。
(PS:也可以直接继承BasicErrorController,该中已经有一个默认处理text/html的方法。如果你想添加一个新内容类型(如JSON)的处理程序,你需要添加一个具有@RequestMapping属性的方法,然后返回你自定义的实体类)。

package com.mrcoder.sbexceptionvalidator.controller;
import com.mrcoder.sbexceptionvalidator.common.model.ResponseInfo;
import com.mrcoder.sbexceptionvalidator.model.Person;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
@RestController
public class ExceptionValidatorController implements ErrorController {
    @RequestMapping("/error")
    public ResponseInfo handleError(HttpServletRequest request) {
        //获取statusCode:401,404,500
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        return ResponseInfo.fail(statusCode);
    }
    @Override
    public String getErrorPath() {
        return "/error";
    }
}

此时,再访问不存在的url你会发现返回的消息体已经是我们定义的了。
浏览器:


5.png

非浏览器:


4.png

自定义全局异常处理

上面我们提到ErrorController可对全局错误进行处理,但是有如下几点问题:

  • 获取不到异常的具体信息
  • 无法根据异常类型进行不同的响应,例如对自定义异常的处理。(PS:实际开发中我们常常会定义各种业务的自定义异常)。

实际上,当出现错误,如获取值为空或出现异常时,我们并不希望用户看到异常的具体信息,而是希望对对应的错误和异常做相应提示
在MVC框架中很多时候会出现执行异常,那我们就需要加try/catch进行捕获,如果service层和controller层都加上,那就会造成代码冗余。

而@ControllerAdvice可对全局异常进行捕获,包括自定义异常(也就是我们自定定义的业务异常)
需要注意的是,@ControllerAdvice是应用于对springmvc中的控制器抛出的异常进行处理,而对于404/500这类的异常不会进入控制器处理的异常不起作用,所以404这类的还是要依靠实现ErrorController接口来处理。

我们定义一个错误码枚举类

package com.mrcoder.sbexceptionvalidator.common.status;
import com.mrcoder.sbexceptionvalidator.common.model.ResponseInfo;
/**
 * @Description: 系统错误类型枚举类
 */
public enum ErrorCodeEnum {
    SYSTEM_ERROR(ResponseInfo.FAIL_CODE, "系统异常,请稍后重试"),
    NO_AUTHORITY(ResponseInfo.FAIL_CODE, "无权访问"),
    PARAM_ERROR(ResponseInfo.FAIL_CODE, "参数非法"),
    NOT_LOG(ResponseInfo.FAIL_CODE, "当前用户未登录");
    private Integer errCode;
    private String errMsg;
    private ErrorCodeEnum(Integer errCode, String errMsg) {
        this.errCode = errCode;
        this.errMsg = errMsg;
    }
    public Integer getErrCode() {
        return errCode;
    }
    public String getErrMsg() {
        return errMsg;
    }
}

再定义一个业务异常类型

package com.mrcoder.sbexceptionvalidator.common.exception;


import com.mrcoder.sbexceptionvalidator.common.model.ResponseInfo;
import com.mrcoder.sbexceptionvalidator.common.status.ErrorCodeEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * @Description: 全局业务异常类型
 */
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class BusinessException extends RuntimeException {
    private static final long serialVersionUID = -6842004487143726249L;
    private Integer errCode;
    private String errMsg;
    public BusinessException(String errMsg) {
        super();
        this.errCode = ResponseInfo.FAIL_CODE;
        this.errMsg = errMsg;
    }
    public BusinessException(ErrorCodeEnum errorCodeEnum) {
        super();
        this.errCode = errorCodeEnum.getErrCode();
        this.errMsg = errorCodeEnum.getErrMsg();
    }
}

最后全局异常处理器

package com.mrcoder.sbexceptionvalidator.common.exception;


import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;

import com.mrcoder.sbexceptionvalidator.common.model.ResponseInfo;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;


/**
 * @Description: 全局异常捕获处理器
 */
@ControllerAdvice
public class GlobalExpectionHandler {
    /**
     * 异常捕获处理
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ResponseInfo expectionHandler(HttpServletRequest request, Exception e) {
        if (e instanceof MethodArgumentNotValidException) { // JavaBean参数校验异常
            MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
            List<ObjectError> allErrors = exception.getBindingResult().getAllErrors(); // 取出错误信息
            ObjectError error = allErrors.get(0); // 只返回第一个错误信息即可
            return ResponseInfo.fail(error.getDefaultMessage());
        } else if (e instanceof ConstraintViolationException) { // Controller方法参数校验异常
            // 错误异常
            String message = ((ConstraintViolationException) e).getConstraintViolations().iterator().next().getMessage();
            return ResponseInfo.fail(message);
        } else if (e instanceof BusinessException) {
            // 自定义业务异常
            BusinessException exception = (BusinessException) e;
            return ResponseInfo.fail(exception.getErrMsg());
        } else {
            // 系统异常,打印错误日志
            e.printStackTrace();
            return ResponseInfo.fail("系统异常,请稍后重试");
        }
    }
}

项目地址

https://github.com/MrCoderStack/SpringBootDemo/tree/master/sb-exception-validator

请关注我的订阅号

订阅号.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,367评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,959评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,750评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,226评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,252评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,975评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,592评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,497评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,027评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,147评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,274评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,953评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,623评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,143评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,260评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,607评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,271评论 2 358