【SpringBoot web-3】web項目統一數據封裝與全局異常處理


在項目開發中,接口與接口之間、前後端之間的數據傳輸都使用 JSON 格式。

1 fastjson使用

阿里巴巴的 fastjson是目前應用最廣泛的JSON解析框架。本文也將使用fastjson。

1.1 引入依賴

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.35</version>
</dependency>

2 統一封裝返回數據

在web項目中,接口返回數據一般要包含狀態碼、信息、數據等,例如下面的接口示例:

import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author guozhengMu
 * @version 1.0
 * @date 2019/8/21 14:55
 * @description
 * @modify
 */
@RestController
@RequestMapping(value = "/test", method = RequestMethod.GET)
public class TestController {
    @RequestMapping("/json")
    public JSONObject test() {
        JSONObject result = new JSONObject();
        try {
            // 業務邏輯代碼
            result.put("code", 0);
            result.put("msg", "操作成功!");
            result.put("data", "測試數據");
        } catch (Exception e) {
            result.put("code", 500);
            result.put("msg", "系統異常,請聯繫管理員!");
        }
        return result;
    }
}

這樣的話,每個接口都這樣處理,非常麻煩,需要一種更優雅的實現方式。

2.1 定義統一的JSON結構

統一的 JSON 結構中屬性包括數據、狀態碼、提示信息,其他項可以自己根據需要添加。一般來說,應該有默認的返回結構,也應該有用戶指定的返回結構。由於返回數據類型無法確定,需要使用泛型,代碼如下:

public class ResponseInfo<T> {
    /**
     * 狀態碼
     */
    protected String code;
    /**
     * 響應信息
     */
    protected String msg;
    /**
     * 返回數據
     */
    private T data;

    /**
     * 若沒有數據返回,默認狀態碼爲 0,提示信息爲“操作成功!”
     */
    public ResponseInfo() {
        this.code = 0;
        this.msg = "操作成功!";
    }

    /**
     * 若沒有數據返回,可以人爲指定狀態碼和提示信息
     * @param code
     * @param msg
     */
    public ResponseInfo(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    /**
     * 有數據返回時,狀態碼爲 0,默認提示信息爲“操作成功!”
     * @param data
     */
    public ResponseInfo(T data) {
        this.data = data;
        this.code = 0;
        this.msg = "操作成功!";
    }

    /**
     * 有數據返回,狀態碼爲 0,人爲指定提示信息
     * @param data
     * @param msg
     */
    public ResponseInfo(T data, String msg) {
        this.data = data;
        this.code = 0;
        this.msg = msg;
    }
    // 省略 get 和 set 方法
}

2.2 使用統一的JSON結構

我們封裝了統一的返回數據結構後,在接口中就可以直接使用了。如下:

import com.example.demo.model.ResponseInfo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author guozhengMu
 * @version 1.0
 * @date 2019/8/21 14:55
 * @description
 * @modify
 */
@RestController
@RequestMapping(value = "/test", method = RequestMethod.GET)
public class TestController {
    @RequestMapping("/json")
    public ResponseInfo test() {
        try {
            // 模擬異常業務代碼
            int num = 1 / 0;
            return new ResponseInfo("測試數據");
        } catch (Exception e) {
            return new ResponseInfo(500, "系統異常,請聯繫管理員!");
        }
    }
}

如上,接口的返回數據處理便優雅了許多。針對上面接口做個測試,啓動項目,通過瀏覽器訪問:localhost:8096/test/json,得到響應結果:

{"code":500,"msg":"系統異常,請聯繫管理員!","data":null}

3 全局異常處理

3.1 系統定義異常處理

新建一個 ExceptionHandlerAdvice 全局異常處理類,然後加上 @RestControllerAdvice 註解即可攔截項目中拋出的異常,如下代碼中包含了幾個異常處理,如參數格式異常、參數缺失、系統異常等,見下例:

@RestControllerAdvice
@Slf4j
public class ExceptionHandlerAdvice {

    // 參數格式異常處理
    @ExceptionHandler({IllegalArgumentException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseInfo badRequestException(IllegalArgumentException exception) {
    	log.error("參數格式不合法:" + e.getMessage());
        return new ResponseInfo(HttpStatus.BAD_REQUEST.value() + "", "參數格式不符!");
    }

	// 權限不足異常處理
    @ExceptionHandler({AccessDeniedException.class})
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ResponseInfo badRequestException(AccessDeniedException exception) {
        return new ResponseInfo(HttpStatus.FORBIDDEN.value() + "", exception.getMessage());
    }

	// 參數缺失異常處理
    @ExceptionHandler({MissingServletRequestParameterException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseInfo badRequestException(Exception exception) {
        return new ResponseInfo(HttpStatus.BAD_REQUEST.value() + "", "缺少必填參數!");
    }

    // 空指針異常
    @ExceptionHandler(NullPointerException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseInfo handleTypeMismatchException(NullPointerException ex) {
        log.error("空指針異常,{}", ex.getMessage());
        return new JsonResult("500", "空指針異常");
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonResult handleUnexpectedServer(Exception ex) {
        log.error("系統異常:", ex);
        return new JsonResult("500", "系統發生異常,請聯繫管理員");
    }
    
    // 系統異常處理
    @ExceptionHandler(Throwable.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseInfo exception(Throwable throwable) {
        log.error("系統異常", throwable);
        return new ResponseInfo(HttpStatus.INTERNAL_SERVER_ERROR.value() + "系統異常,請聯繫管理員!");
    }
}
  1. @RestControllerAdvice 註解包含了 @Component 註解,說明在 Spring Boot 啓動時,也會把該類作爲組件交給 Spring 來管理。
  2. @RestControllerAdvice 註解包含了 @ResponseBody 註解,爲了異常處理完之後給調用方輸出一個 JSON 格式的封裝數據。
  3. @RestControllerAdvice 註解還有個 basePackages 屬性,該屬性用來攔截哪個包中的異常信息,一般我們不指定這個屬性,我們攔截項目工程中的所有異常。
  4. 在方法上通過 @ExceptionHandler 註解來指定具體的異常,然後在方法中處理該異常信息,最後將結果通過統一的 JSON 結構體返回給調用者。
  5. 但在項目中,我們一般都會比較詳細地去攔截一些常見異常,攔截 Exception 雖然可以一勞永逸,但是不利於我們去排查或者定位問題。實際項目中,可以把攔截 Exception 異常寫在 GlobalExceptionHandler 最下面,如果都沒有找到,最後再攔截一下 Exception 異常,保證輸出信息友好。

下面我們通過一個接口來進行測試:

@RestController
@RequestMapping(value = "/test", method = RequestMethod.POST)
public class TestController {
    @RequestMapping("/json")
    public ResponseInfo test(@RequestParam String userName, @RequestParam String password) {
        try {
            String data = "登錄用戶:" + userName + ",密碼:" + password;
            return new ResponseInfo("0", "操作成功!", data);
        } catch (Exception e) {
            return new ResponseInfo("500", "系統異常,請聯繫管理員!");
        }
    }
}

接口調用,password這項故意空缺:
在這裏插入圖片描述

3.2 自定義異常攔截

在實際項目中,除了攔截一些系統異常外,在某些業務上,我們需要自定義一些業務異常,要處理一個服務的調用時,那麼可能會調用失敗或者調用超時等等,此時我們需要自定義一個異常,當調用失敗時拋出該異常,讓 ExceptionHandlerAdvice 去捕獲。

3.2.1 定義異常信息

由於在業務中,有很多異常,上面的系統定義異常遠遠不能覆蓋,爲了方便項目異常信息管理,我們一般會定義一個異常信息枚舉類。比如:

public enum BusinessMsgEnum {
    /**
     * 參數異常
     */
    PARMETER_EXCEPTION("101", "參數異常!"),
    /**
     * 等待超時
     */
    SERVICE_TIME_OUT("102", "服務超時!"),
    /**
     * 參數過大
     */
    PARMETER_BIG_EXCEPTION("903", "內容不能超過200字,請重試!"),
    /**
     * 數據庫操作失敗
     */
    DATABASE_EXCEPTION("509", "數據庫操作異常,請聯繫管理員!"),
    /**
     * 500 : 一勞永逸的提示也可以在這定義
     */
    UNEXPECTED_EXCEPTION("500", "系統發生異常,請聯繫管理員!");
    // 還可以定義更多的業務異常

    /**
     * 消息碼
     */
    private String code;
    /**
     * 消息內容
     */
    private String msg;

    private BusinessMsgEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    // set get方法
}

3.2.2 攔截自定義異常

我們可以定義一個業務異常,當出現業務異常時,我們就拋出這個自定義的業務異常即可。比如我們定義一個 BusinessErrorException 異常,如下:

public class BusinessErrorException extends RuntimeException {

    private static final long serialVersionUID = -7480022450501760611L;

    /**
     * 異常碼
     */
    private String code;
    /**
     * 異常提示信息
     */
    private String msg;

    public BusinessErrorException(BusinessMsgEnum businessMsgEnum) {
        this.code = businessMsgEnum.code();
        this.msg = businessMsgEnum.msg();
    }
    // get set方法
}

在構造方法中,傳入我們上面自定義的異常枚舉類,在項目中,如果有新的異常信息需要添加,我們直接在枚舉類中添加即可,很方便,做到統一維護,在攔截該異常時獲取即可。

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 攔截業務異常,返回業務異常信息
     * @param ex
     * @return
     */
    @ExceptionHandler(BusinessErrorException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseInfo handleBusinessError(BusinessErrorException ex) {
        String code = ex.getCode();
        String message = ex.getMessage();
        return new ResponseInfo(code, message);
    }
}

在接口層,模擬異常場景,如下:

@RestController
@RequestMapping("/test")
public class ExceptionController {

    private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);

    @GetMapping("/exception")
    public ResponseInfo testException() {
        try {
            int i = 1 / 0;
        } catch (Exception e) {
            throw new BusinessErrorException(BusinessMsgEnum.UNEXPECTED_EXCEPTION);
        }
        return new ResponseInfo();
    }
}

啓動項目,請求該接口:
在這裏插入圖片描述

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