欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring Boot 2.X REST 风格全局异常处理

程序员文章站 2022-05-22 09:42:15
...


1 摘要

异常是程序的一部分,在项目运行时可能由于各种问题而抛出。一份规范、简洁的程序代码需要有一套合理的异常处理机制,在同一个项目中使用统一的异常处理,能够极大地方便问题的排查、接口的对接以及提升用户体验。本文将介绍一种在 Spring Boot 项目符合 REST 风格的全局异常处理解决方案。

2 核心依赖

            <!-- web,mvc -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${springboot.version}</version>
            </dependency>
        <!-- Servlet -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>${servlet-api.version}</version>
        </dependency>

其中 ${springboot.version} 的版本为 2.0.6.RELEASE , ${servlet-api.version} 的版本为 3.1.0

3 核心代码

3.1 接口返回码封装类

../demo-common/src/main/java/com/ljq/demo/springboot/common/api/ResponseCode.java
package com.ljq.demo.springboot.common.api;

import lombok.Getter;
import lombok.ToString;

/**
 * @Description: 返回码枚举
 * @Author yemiaoxin
 * @Date 2018/5/22
 */
@Getter
@ToString
public enum ResponseCode {

    /**
     * 成功与失败
     */
    SUCCESS(200, "成功"),
    FAIL(-1, "失败"),

    /**
     * 公共参数
     */
    PARAM_ERROR(1001, "参数错误"),
    PARAM_NOT_NULL(1002, "参数不能为空"),
    SIGN_ERROR(1003,"签名错误"),
    REQUEST_METHOD_ERROR(1004, "请求方式错误"),
    MEDIA_TYPE_NOT_SUPPORT_ERROR(1005, "参数(文件)格式不支持"),
    PARAM_BIND_ERROR(1006, "参数格式错误,数据绑定失败"),
    NOT_FOUND_ERROR(1007, "请求资源(接口)不存在"),
    MISS_REQUEST_PART_ERROR(1008, "缺少请求体(未上传文件)"),
    MISS_REQUEST_PARAM_ERROR(1009, "缺少请求参数"),

    /**
     * 用户模块
     */
    ACCOUNT_ERROR(2001, "账号错误"),
    PASSWORD_ERROR(2002,"密码错误"),
    ACCOUNT_NOT_EXIST(2003,"账号不存在"),

    /**
     * 其他
     */
    UNKNOWN_ERROR(-1000,"未知异常");

    /**
     * 返回码
     */
    private int code;

    /**
     * 返回信息
     */
    private String msg;

    private ResponseCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

}

3.2 接口返回结果封装类

../demo-common/src/main/java/com/ljq/demo/springboot/common/api/ApiResult.java
package com.ljq.demo.springboot.common.api;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;
import java.util.Map;

/**
 * @Description: 接口请求返回结果
 * @Author: junqiang.lu
 * @Date: 2018/10/9
 */
@Data
@ApiModel(value = "接口返回结果")
public class ApiResult<T> implements Serializable {

    private static final long serialVersionUID = -2953545018812382877L;

    /**
     * 返回码,200 正常
     */
    @ApiModelProperty(value = "返回码,200 正常", name = "code")
    private int code = 200;

    /**
     * 返回信息
     */
    @ApiModelProperty(value = "返回信息", name = "msg")
    private String msg = "成功";

    /**
     * 返回数据
     */
    @ApiModelProperty(value = "返回数据对象", name = "data")
    private T data;

    /**
     * 附加数据
     */
    @ApiModelProperty(value = "附加数据", name = "extraData")
    private Map<String, Object> extraData;

    /**
     * 系统当前时间
     */
    @ApiModelProperty(value = "服务器系统时间,时间戳(精确到毫秒)", name = "timestamp")
    private Long timestamp = System.currentTimeMillis();

    /**
     * 获取成功状态结果
     *
     * @return
     */
    public static ApiResult success() {
        return success(null, null);
    }

    /**
     * 获取成功状态结果
     *
     * @param data 返回数据
     * @return
     */
    public static ApiResult success(Object data) {
        return success(data, null);
    }

    /**
     * 获取成功状态结果
     *
     * @param data 返回数据
     * @param extraData 附加数据
     * @return
     */
    public static ApiResult success(Object data, Map<String, Object> extraData) {
        ApiResult apiResult = new ApiResult();
        apiResult.setCode(ResponseCode.SUCCESS.getCode());
        apiResult.setMsg(ResponseCode.SUCCESS.getMsg());
        apiResult.setData(data);
        apiResult.setExtraData(extraData);
        return apiResult;
    }

    /**
     * 获取失败状态结果
     *
     * @return
     */
    public static ApiResult failure() {
        return failure(ResponseCode.FAIL.getCode(), ResponseCode.FAIL.getMsg(), null);
    }

    /**
     * 获取失败状态结果
     *
     * @param msg (自定义)失败消息
     * @return
     */
    public static ApiResult failure(String msg) {
        return failure(ResponseCode.FAIL.getCode(), msg, null);
    }

    /**
     * 获取失败状态结果
     *
     * @param responseCode 返回状态码
     * @return
     */
    public static ApiResult failure(ResponseCode responseCode) {
        return failure(responseCode.getCode(), responseCode.getMsg(), null);
    }

    /**
     * 获取失败状态结果
     *
     * @param responseCode 返回状态码
     * @param data         返回数据
     * @return
     */
    public static ApiResult failure(ResponseCode responseCode, Object data) {
        return failure(responseCode.getCode(), responseCode.getMsg(), data);
    }

    /**
     * 获取失败返回结果
     *
     * @param code 错误码
     * @param msg  错误信息
     * @param data 返回结果
     * @return
     */
    public static ApiResult failure(int code, String msg, Object data) {
        ApiResult result = new ApiResult();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }


}

3.3 自义定异常类

../demo-common/src/main/java/com/ljq/demo/springboot/common/exception/ParamsCheckException.java
package com.ljq.demo.springboot.common.exception;

import com.ljq.demo.springboot.common.api.ResponseCode;
import com.ljq.demo.springboot.common.api.ResponseCodeI18n;
import lombok.Data;

/**
 * @Description: 自定义参数校验异常
 * @Author: junqiang.lu
 * @Date: 2019/1/24
 */
@Data
public class ParamsCheckException extends Exception{

    private static final long serialVersionUID = 2684099760669375847L;

    /**
     * 异常编码
     */
    private int code;

    /**
     * 异常信息
     */
    private String message;

    public ParamsCheckException(){
        super();
    }

    public ParamsCheckException(int code, String message){
        this.code = code;
        this.message = message;
    }

    public ParamsCheckException(String message){
        this.message = message;
    }

    public ParamsCheckException(ResponseCode responseCode){
        this.code = responseCode.getCode();
        this.message = responseCode.getMsg();
    }


}

3.4 全局异常处理类

../demo-common/src/main/java/com/ljq/demo/springboot/common/interceptor/GlobalExceptionHandler.java
package com.ljq.demo.springboot.common.interceptor;

import com.ljq.demo.springboot.common.api.ApiResult;
import com.ljq.demo.springboot.common.api.ResponseCode;
import com.ljq.demo.springboot.common.exception.ParamsCheckException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;

/**
 * @Description: 全局异常处理
 * @Author: junqiang.lu
 * @Date: 2019/12/2
 */
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 全局异常处理
     *
     * @param e
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = {ParamsCheckException.class, HttpRequestMethodNotSupportedException.class,
            HttpMediaTypeNotSupportedException.class, BindException.class, NoHandlerFoundException.class,
            MissingServletRequestPartException.class, MissingServletRequestParameterException.class,
            Exception.class})
    public ResponseEntity exceptionHandler(Exception e) {
        log.warn("class: {}, message: {}",e.getClass().getName(), e.getMessage());
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
        // 自定义异常
        if (ParamsCheckException.class.isAssignableFrom(e.getClass())) {
            return new ResponseEntity(ApiResult.failure(((ParamsCheckException) e).getCode(),e.getMessage(), null),headers, HttpStatus.BAD_REQUEST);
        }
        // 请求方式错误异常
        if (HttpRequestMethodNotSupportedException.class.isAssignableFrom(e.getClass())) {
            return new ResponseEntity(ApiResult.failure(ResponseCode.REQUEST_METHOD_ERROR), headers, HttpStatus.BAD_REQUEST);
        }
        // 参数格式不支持
        if (HttpMediaTypeNotSupportedException.class.isAssignableFrom(e.getClass())) {
            return new ResponseEntity(ApiResult.failure(ResponseCode.MEDIA_TYPE_NOT_SUPPORT_ERROR), headers, HttpStatus.BAD_REQUEST);
        }
        // 参数格式错误,数据绑定失败
        if (BindException.class.isAssignableFrom(e.getClass())) {
            return new ResponseEntity(ApiResult.failure(ResponseCode.PARAM_BIND_ERROR), headers, HttpStatus.BAD_REQUEST);
        }
        // 404
        if (NoHandlerFoundException.class.isAssignableFrom(e.getClass())) {
            return new ResponseEntity(ApiResult.failure(ResponseCode.NOT_FOUND_ERROR), headers, HttpStatus.BAD_REQUEST);
        }
        // 缺少请求体(未上传文件)
        if (MissingServletRequestPartException.class.isAssignableFrom(e.getClass())) {
            return new ResponseEntity(ApiResult.failure(ResponseCode.MISS_REQUEST_PART_ERROR), headers, HttpStatus.BAD_REQUEST);
        }
        // 缺少请求参数
        if (MissingServletRequestParameterException.class.isAssignableFrom(e.getClass())) {
            return new ResponseEntity(ApiResult.failure(ResponseCode.MISS_REQUEST_PARAM_ERROR), headers, HttpStatus.BAD_REQUEST);
        }

        /**
         * 根据情况添加异常类型(如IO,线程,DB 相关等)
         */

        // 其他
        return new ResponseEntity(ApiResult.failure(ResponseCode.UNKNOWN_ERROR), headers, HttpStatus.BAD_REQUEST);
    }

}

说明:

与网络请求相关的常见异常可参考:

Handling Standard Spring MVC Exceptions

部分异常与出现的场景归纳:

异常名称 出现情况
HttpRequestMethodNotSupportedException 请求方式不是 Controller 中指定的,
eg: Controller 指定 POST 请求,实际用 GET 请求
HttpMediaTypeNotSupportedException Controller 中指定接收参数格式为文本,但实际请求
包含文件
BindException 接收参数为 int 类型, 实际传参为 String 类型
NoHandlerFoundException 请求的地址不存在
MissingServletRequestPartException Controller 指明需要传文件,但实际上没有上传文件
MissingServletRequestParameterException 在使用 @RequestParam 注解时,没有传指定的参数
(这里建议使用封装的 Bean 来接收参数,不要使用 @RequestParam)

3.5 配置文件

../demo-web/src/main/resources/application.yml
  ## 异常处理
spring:  
  mvc:
    throw-exception-if-no-handler-found: true
  resources:
    add-mappings: false

若不添加此配置,则无法手动拦截 NoHandlerFoundException 异常,此时系统会调用 SpringBoot默认的拦截器,返回信息也是 REST 风格,但不统一,具体示例如下:

{
    "timestamp": "2019-12-02T07:34:40.485+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/api/rest/user/info/dede"
}

4 测试

4.1 抛出自定义异常

展示层:

Spring Boot 2.X REST 风格全局异常处理

请求日志:

2019-12-02 15:41:18 | INFO  | http-nio-8088-exec-5 | com.ljq.demo.springboot.web.acpect.LogAspectLogAspect.java 68| [AOP-LOG-START]
	requestMark: cf0c3e90-39fc-4df9-9fe8-86fcaf3c37a8
	requestIP: 127.0.0.1
	contentType:application/x-www-form-urlencoded
	requestUrl: http://127.0.0.1:8088/api/rest/user/info
	requestMethod: GET
	requestParams: id = [1];
	targetClassAndMethod: com.ljq.demo.springboot.web.controller.RestUserController#info
2019-12-02 15:41:18 | WARN  | http-nio-8088-exec-5 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: com.ljq.demo.springboot.common.exception.ParamsCheckException, message: 失败
2019-12-02 15:41:18 | WARN  | http-nio-8088-exec-5 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: ParamsCheckException(code=-1, message=失败)

4.2 抛出其他异常

展示层:

Spring Boot 2.X REST 风格全局异常处理

请求日志:

2019-12-02 15:41:07 | INFO  | http-nio-8088-exec-4 | com.ljq.demo.springboot.web.acpect.LogAspectLogAspect.java 68| [AOP-LOG-START]
	requestMark: 51e727ac-28fb-4fee-a20f-3e2cd9ccc5f2
	requestIP: 127.0.0.1
	contentType:application/x-www-form-urlencoded
	requestUrl: http://127.0.0.1:8088/api/rest/user/info
	requestMethod: GET
	requestParams: id = [2];
	targetClassAndMethod: com.ljq.demo.springboot.web.controller.RestUserController#info
2019-12-02 15:41:07 | WARN  | http-nio-8088-exec-4 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: java.lang.Exception, message: 未知异常
2019-12-02 15:41:07 | WARN  | http-nio-8088-exec-4 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: java.lang.Exception: 未知异常

4.3 错误的请求方式

展示层:

Spring Boot 2.X REST 风格全局异常处理

请求日志:

2019-12-02 15:49:08 | WARN  | http-nio-8088-exec-6 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: org.springframework.web.HttpRequestMethodNotSupportedException, message: Request method 'POST' not supported
2019-12-02 15:49:08 | WARN  | http-nio-8088-exec-6 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported

4.4 向不接受文件参数的接口上传文件

展示层:

Spring Boot 2.X REST 风格全局异常处理

请求日志:

2019-12-02 15:54:46 | WARN  | http-nio-8088-exec-8 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: org.springframework.web.HttpMediaTypeNotSupportedException, message: Content type 'multipart/form-data;boundary=--------------------------099318382469038507746221;charset=UTF-8' not supported
2019-12-02 15:54:46 | WARN  | http-nio-8088-exec-8 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'multipart/form-data;boundary=--------------------------099318382469038507746221;charset=UTF-8' not supported

4.5 参数字段类型对不上

展示层:

Spring Boot 2.X REST 风格全局异常处理

请求日志:

2019-12-02 15:57:50 | WARN  | http-nio-8088-exec-1 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: org.springframework.validation.BindException, message: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'restUserInfoParam' on field 'id': rejected value [aaa]; codes [typeMismatch.restUserInfoParam.id,typeMismatch.id,typeMismatch.java.lang.Long,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [restUserInfoParam.id,id]; arguments []; default message [id]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.lang.Long' for property 'id'; nested exception is java.lang.NumberFormatException: For input string: "aaa"]
2019-12-02 15:57:50 | WARN  | http-nio-8088-exec-1 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'restUserInfoParam' on field 'id': rejected value [aaa]; codes [typeMismatch.restUserInfoParam.id,typeMismatch.id,typeMismatch.java.lang.Long,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [restUserInfoParam.id,id]; arguments []; default message [id]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.lang.Long' for property 'id'; nested exception is java.lang.NumberFormatException: For input string: "aaa"]

4.6 上传文件为空

展示层:

Spring Boot 2.X REST 风格全局异常处理

请求日志:

2019-12-02 15:59:57 | WARN  | http-nio-8088-exec-2 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: org.springframework.web.multipart.support.MissingServletRequestPartException, message: Required request part 'file' is not present
2019-12-02 15:59:57 | WARN  | http-nio-8088-exec-2 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present

4.7 缺少请求参数

展示层:

Spring Boot 2.X REST 风格全局异常处理

请求日志:

2019-12-02 16:02:16 | WARN  | http-nio-8088-exec-6 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: org.springframework.web.bind.MissingServletRequestParameterException, message: Required String parameter 'passcode' is not present
2019-12-02 16:02:16 | WARN  | http-nio-8088-exec-6 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: org.springframework.web.bind.MissingServletRequestParameterException: Required String parameter 'passcode' is not present

5 参考资料推荐

Error Handling for REST with Spring

Handling Standard Spring MVC Exceptions

解决spring boot中rest接口404,500等错误返回统一的json格式

6 Github 源码

Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo

个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
Spring Boot 2.X REST 风格全局异常处理