Spring Boot 2.X REST 風格全局異常處理


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 拋出自定義異常

展示層:

拋出自定義異常

請求日誌:

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 拋出其他異常

展示層:

拋出其他異常

請求日誌:

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 錯誤的請求方式

展示層:

錯誤的請求方式

請求日誌:

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 向不接受文件參數的接口上傳文件

展示層:

向不接受文件參數的接口上傳文件

請求日誌:

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 參數字段類型對不上

展示層:

參數字段類型對不上

請求日誌:

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 上傳文件爲空

展示層:

上傳文件爲空

請求日誌:

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 缺少請求參數

展示層:

缺少請求參數

請求日誌:

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,分享半個互聯網人的技術與思考,感興趣的可以關注.
404Code

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章