Springboot统一异常处理

当服务端发生异常(Exception)时,我们往往不希望程序异常终止,也不希望暴露过多的异常信息给客户端,这时候就需要一个统一的异常处理机制,来保证客户端能够收到友好的提示。

异常(exception) 是程序运行过程中发生的事件, 该事件可以中断程序指令的正常执行流程.
1)当Java程序运行时出现问题时,系统能够自动检测到,并立即生成一个与该问题对应的异常对象;
2)然后把该异常对象提交给Java虚拟机;
3)Java虚拟机会自动寻找相应的处理代码来处理这个异常,如果没有找到,则程序终止;
4)为了避免程序异常终止,我们可以编写代码来捕获和处理可能出现的异常。

实现方法

当前比较流行的统一异常处理方式为:
1) 自定义异常类,用于收敛已知异常;
2)使用@ControllerAdvice和@ExceptionHandler编写异常处理Handler,实现处理Interceptor内部(包括controller层、Service层、Dao层以及Spring系统内定义的部分)异常。Springboot执行流程参见:《Springboot工作原理》
3)返回统一封装好的更加友好的提示信息给客户端。

自定义异常类
@Getter
public class ResponseException extends RuntimeException {
    private ResponseStatus responseStatus;
    private String request;
    
    public ResponseException(ResponseStatus responseStatus) {
        super(responseStatus.getEc() + "," + responseStatus.getEm());
        this.responseStatus = responseStatus;
    }
    
    public ResponseException(ResponseStatus responseStatus, String request) {
        this(responseStatus);
        this.request = request;
    }
    
    public ResponseException(ResponseStatus responseStatus, Throwable cause) {
        super(responseStatus.getEc() + "," + responseStatus.getEm(), cause);
        this.responseStatus = responseStatus;
    }
    
    public ResponseException(ResponseStatus responseStatus, Throwable cause, String request) {
        this(responseStatus, cause);
        this.request = request;
    }
}

Response状态码协议

@Getter
public enum ResponseStatus {
    /**
     * 成功
     */
    SUCCESS(0, "OK"),

    /**
     * 客户端请求的语法错误,服务器无法理解
     */
    BAD_REQUEST(400, "Bad Request"),

    /**
     * 服务器内部错误
     */
    INTERNAL_SERVICE_ERROR(500, "Internal Server Error"),
    
    private int ec;
    private String em;

    ResponseStatus(int ec, String em) {
        this.ec = ec;
        this.em = em;
    }

    public static ResponseStatus getResponseStatus(int ec) {
        for (ResponseStatus status : ResponseStatus.values()) {
            if (status.getEc() == ec) {
                return status;
            }
        }
        return null;
    }

    @Override
    public String toString() {
        return "[" + ec + "]" + "[" + em + "]";
    }
}
编写异常处理Handler

异常处理Handler一般干三件事:异常监控统计、记录异常日志、返回统一封装好的更加友好的提示信息给客户端。

1)Controller层、service层、dao层抛出的异常,捕获后可以直接return封装好的提示信息(ResponseDto)给客户端;
2)但是,进入Controller之前发生的异常(例如 Filter异常),这种异常的返回需要操作I/O流来返回。
  例如,某个Controller有一个Post方法,接收一个json对象,如果实际传输的json对象格式和需要的json对象不一致;
  消息进入Controller之前就回抛出序列化异常(HttpMessageNotReadableException),这个异常Controller层是捕获不到的;
  但是我们希望捕获该异常,并且返回自己定义的ResponseDto对象,这时便可以使用ServletOutputStream的write方法返回。
@Slf4j
@ControllerAdvice
public class ResponseExceptionHandler {
    /**
     * 客户端全局异常捕捉处理
     * 捕获自定义异常 ResponseException
     */
    @ResponseBody
    @ExceptionHandler(value = {ResponseException.class})
    public ResponseDto<Map<String, Object>> handleException(ResponseException e, HttpServletRequest request, ServletResponse response) {
        if (ResponseStatus.BAD_REQUEST == e.getResponseStatus()) {
            // 异常计数
            AppStateMonitor.BAD_REQUEST_COUNTER.inc(); 
            // 打印异常日志
            LogUtil.error(log, e, "{0}[{1}][{2}]", ResponseStatus.BAD_REQUEST.toString(), request.getRequestURI(), e.getRequest()); 
            // 返回统一封装好的提示给客户端
            return new ResponseDto(ResponseStatus.BAD_REQUEST.getEc(), ResponseStatus.BAD_REQUEST.getEm(), null);
        } else if (ResponseStatus.INTERNAL_SERVICE_ERROR == e.getResponseStatus()) {
            AppStateMonitor.PROC_FAIL_COUNTER.inc();
            LogUtil.error(log, e, "{0}[{1}][{2}]", ResponseStatus.INTERNAL_SERVICE_ERROR.toString(), request.getRequestURI(), e.getRequest());
            return new ResponseDto(ResponseStatus.INTERNAL_SERVICE_ERROR.getEc(), ResponseStatus.INTERNAL_SERVICE_ERROR.getEm(), null);
        } 
        return null;
    }

    /**
     * 客户端全局异常捕捉处理
     * 捕获 Controller 外层、Interceptor 内层 异常,下面异常是 SpringFramework 中定义的异常
     * HttpMessageNotReadableException
     * MissingServletRequestParameterException
     * MaxUploadSizeExceededException
     * HttpRequestMethodNotSupportedException
     */
    @ExceptionHandler(value = {HttpMessageNotReadableException.class, MissingServletRequestParameterException.class, MaxUploadSizeExceededException.class, HttpRequestMethodNotSupportedException.class})
    public void springFrameworkException(Exception e, HttpServletRequest request, ServletResponse response) throws Exception {
        AppStateMonitor.PROC_FAIL_COUNTER.inc();
        AppStateMonitor.ERR_CLI_COUNTER.inc();
        LogUtil.error(log, "{0}[{1}][{2}]", ResponseStatus.BAD_REQUEST.toString(), request.getRequestURI(), e.getMessage());
        if (e instanceof MissingServletRequestParameterException) {
            AppStateMonitor.PARAM_ERR_COUNTER.inc();
        } else if (e instanceof HttpMessageNotReadableException) {
            AppStateMonitor.SERIAL_ERR_COUNTER.inc();
        } else if (e instanceof MaxUploadSizeExceededException) {
            AppStateMonitor.FILE_SIZE_LIMIT_COUNTER.inc();
        } else if (e instanceof HttpRequestMethodNotSupportedException) {
            AppStateMonitor.PARAM_ERR_COUNTER.inc();
        }

        // 使用ServletOutputStream的write方法返回异常处理信息
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream servletOutputStream = response.getOutputStream();
        servletOutputStream.write(new ServletFilterDto(ResponseStatus.BAD_REQUEST.getEc(), ResponseStatus.BAD_REQUEST.getEm()).toString().getBytes());
    }
}
定义传输层数据传输对象
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BaseDto {
    /**
     * 状态码
     */
    private int ec;
    /**
     * 提示信息
     */
    private String em;

    public BaseDto(int ec, String em) {
        this.ec = ec;
        this.em = em;
    }

    @Override
    public String toString() {
        return JsonUtil.toJSON(this);
    }
}
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ServletFilterDto extends BaseDto {
    /**
     * 时间戳
     */
    protected long timesec;

    public ServletFilterDto(int ec, String em) {
        super(ec, em);
        this.timesec = System.currentTimeMillis();
    }

    @Override
    public String toString() {
        return JsonUtil.toJSON(this);
    }
}
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseDto<T> extends ServletFilterDto {
    /**
     * 返回数据
     */
    private T data;

    public ResponseDto(int ec, String em) {
        super(ec, em);
    }

    public ResponseDto(int ec, String em, T data) {
        this(ec, em);
        this.data = data;
    }

    @Override
    public String toString() {
        return JsonUtil.toJSON(this);
    }
}
调用示例
@Slf4j
@RestController
@RequestMapping("/api/exception")
public class ExceptionController extends BaseController {
    @PostMapping("/controller")
    public ResponseDto<String> controller() {
        String requestBody = getRequestBody(request);
        if (StringUtils.isEmpty(requestBody)) {
            throw new ResponseException(ResponseStatus.BAD_REQUEST, ":empty request body:" + requestBody);
        }

        return succuss();
    }

    @PostMapping("/filter")
    public ResponseDto<String> filter(@RequestBody Person person) {
        return succuss();
    }
}

Controller基类

@Slf4j
public abstract class BaseController {
    /**
     * 该成员变量是线程安全的
     * 1.在controller中注入的request是jdk动态代理对象,ObjectFactoryDelegatingInvocationHandler的实例.
     * 当我们调用成员域request的方法的时候其实是调用了objectFactory的getObject()对象的相关方法.这里的objectFactory是RequestObjectFactory.
     * 2.RequestObjectFactory的getObject其实是从RequestContextHolder的threadlocal中去取值的.
     * 3.请求刚进入springmvc的dispatcherServlet的时候会把request相关对象设置到RequestContextHolder的threadlocal中去.
     */
    @Autowired
    protected HttpServletRequest request;

    /**
     * 获取消息体
     */
    protected String getRequestBody() {
        return getRequestBody(this.request);
    }
    
    protected String getRequestBody(HttpServletRequest request) {
        try {
            return HttpServletHelper.getRequestBody(request);
        } catch (UnsupportedEncodingException e) {
            throw new ResponseException(ResponseStatus.BAD_REQUEST, e, "unsupport encoding exception");
        } catch (IOException e) {
            throw new ResponseException(ResponseStatus.BAD_REQUEST, e, "io exception");
        } catch (Exception e) {
            throw new ResponseException(ResponseStatus.BAD_REQUEST, e, "exception");
        }
    }

    /**
     * 返回成功
     */
    protected <T> ResponseDto<T> succuss() {
        return succuss(null);
    }
    
    protected <T> ResponseDto<T> succuss(T t) {
        return new ResponseDto<>(ResponseStatus.SUCCESS.getEc(), ResponseStatus.SUCCESS.getEm(), t);
    }
}
测试结果
Contorller层异常处理

在这里插入图片描述
异常日志

2019-09-09 12:44:06.501 ERROR 91853 --- [  XNIO-2 task-7] c.p.r.w.e.h.ResponseExceptionHandler
 : [400][Bad Request][/api/exception/controller][:empty request body:] 
com.pesen.roc.web.exception.ResponseException: 400,Bad Request
	at com.pesen.roc.web.connectorhttp.web.controller.api.ExceptionController.controller(ExceptionController.java:28) ~[classes/:na] 
	...


Controller层外部Interceptor层内部异常处理

在这里插入图片描述
异常日志

2019-09-09 12:37:22.967 ERROR 91853 --- [  XNIO-2 task-3] c.p.r.w.e.h.ResponseExceptionHandler 
: [400][Bad Request][/api/exception/filter][JSON parse error: Unexpected character ('ï' (code 239)):
 was expecting comma to separate Object entries; nested exception is com.fasterxml.jackson.core.JsonParseException
: Unexpected character ('ï' (code 239)): was expecting comma to separate Object entries
 at [Source: (PushbackInputStream); line: 2, column: 18]]


在这里插入图片描述
异常日志

2019-09-09 12:40:15.101 ERROR 91853 --- [  XNIO-2 task-5] c.p.r.w.e.h.ResponseExceptionHandler     
: [400][Bad Request][/api/exception/filter][Required request body is missing
: public com.pesen.roc.core.dto.ResponseDto<java.lang.String> com.pesen.roc.web.connectorhttp.web.controller.api.ExceptionController.filter(com.pesen.roc.web.connectorhttp.web.controller.api.Person)]


参考文献
Error Handling for REST with Spring

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值