前後端分離後項目中統一響應返回的思考

一、算是個演進?

1.1 傳統JSP + ajax

jsp這種傳統的javaweb項目肯定是不需要考慮太多的。將數據寫入到request的上下文中 通過EL表達式可以直接渲染到頁面上。
當然爲了增加用戶體驗 達到局部刷新的效果。可以通過ajax來動態渲染頁面
這時候就需要 後端開放只返回數據的api接口 而不是將整個jsp頁面在後端渲染後 返回到前端。
這時候假設我要請求一個 文章列表。這時候後端就需要拼接一個vo來對應給前端解析渲染。
假設我們返回的數據爲

{
	status:0
	msg:"獲取成功",
	data:[
		{
			"id":1,
			"title":"文章1",
			"content":"文章內容"
		},
		{
			"id":2,
			"title":"文章2",
			"content":"文章內容"
		}
	]
}

ajax在獲取到數據後 就可以動態渲染DOM節點。達到局部刷新的作用
可以通過判斷 status 是否爲0 來判斷請求業務是否是執行成功。

1.2 後來越來越多的ajax請求 了

依舊延續這之前的統一返回格式並封裝成了一個實體類

@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
public class ServerResponse<T> implements Serializable {

    @ApiModelProperty(value = "請求結果狀態0成功 其他是失敗")
    private int status;
    @ApiModelProperty(value = "附加消息")
    private String msg;
    private T data;
    public ServerResponse(int status){
        this.status = status;
    }
    public ServerResponse(int status,T data){
        this.status = status;
        this.data = data;
    }

    public ServerResponse(int status,String msg,T data){
        this.status = status;
        this.msg = msg;
        this.data = data;
    }

    public ServerResponse(int status,String msg){
        this.status = status;
        this.msg = msg;
    }
    public ServerResponse(){
        super();
    }
    @JsonIgnore
    //使之不在json序列化結果當中
    public boolean isSuccess(){
        return this.status == ResponseCode.SUCCESS.getCode();
    }
    public int getStatus(){
        return status;
    }
    public T getData(){
        return data;
    }
    public String getMsg(){
        return msg;
    }
}

並使用自定義的業務代碼來判斷錯誤的類型

public enum ResponseCode {

    SUCCESS(0,"SUCCESS"),
    ERROR(1,"ERROR"),
    PARAM_ERROR(2,"PARAM_ERROR"),
    NEED_LOGIN(3,"NEED_LOGIN")

    ;

    private  final  int code;
    private final  String desc;

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

    public int getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}

此時 前端依然是判斷status是否爲0

1.3 前後端分離時代來了

我依然使用者之前的統一返回 以不變應萬變。
其實還是挺好用的 至少 我已經馴服了前端同事 他們都已經瞭解了我的返回風格。
並且配合swagger的接口文檔 我可以方便的給返回的泛型VO數據添加註解。
省區了很多不必要的交流。

二、自定義ServerResponse

2.1 統一異常處理

前端同事和我都習慣了以前的自定義ServerResponse統一返回格式。

在我的項目中 總是有個CommonException 和CommonExceptionAdvice

/**
 * 自定義異常處理
 */
@Slf4j
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CommonException extends Exception {

    private Exception error;

    private ServerResponse serverResponse;

    public static CommonException create(ServerResponse serverResponse) {
        CommonException exception = new CommonException();
        exception.serverResponse = serverResponse;
        return exception;
    }

    public static CommonException create(Exception error, ServerResponse serverResponse) {
        CommonException exception = new CommonException();
        exception.serverResponse = serverResponse;
        exception.error = error;
        return exception;
    }
}


@Slf4j
@ControllerAdvice
public class CommonExceptionAdvice {
    @ResponseBody
    @ExceptionHandler(value = {CommonException.class})
    public ServerResponse myError(HttpServletRequest request, CommonException e) {
        // 如果有err 則打印堆棧
        if (e.getError() != null) {
            log.error(CommonMethod.getTrace(e.getError()));
        }
        log.error(e.getServerResponse().getMsg());
        return e.getServerResponse();
    }
    @ResponseBody
    @ExceptionHandler(value = {Exception.class})
    public ServerResponse globalError(HttpServletRequest request, Exception e) {
        log.error(CommonMethod.getTrace(e));
        // 在這裏 可以寫對e的判斷一些常常初心的錯誤 可以友好返回
        return ServerResponse.createByErrorMessage("抱歉!未知錯誤");
    }
}

也就是說 我會在Controller層拋出CommonException便是我可以自己判斷到的錯誤,可以給與友好的提示 並在拋出是 攜帶一個我創建好的ServerResponse對象。前端同事 可以直接拿到統一的異常處理結果。

香嗎?前後端 都習慣了 蠻香的!

2.2 有些不妥的現象

因爲自定義了ServerResponse。所以 要將錯誤簡單的分爲幾類。

  1. 前端請求不合法:參數格式錯誤?缺少必選?
  2. 後端可以catch的錯誤: 數據庫操作失敗?
  3. java runtime異常:沒有catch的錯誤。。統一返回了 抱歉!未知錯誤!
  4. …當然還可以拓展

從以上的簡單分類來看 ServerResponse似乎是可以涵蓋所有的異常問題 。
不知道大家能不能感覺一絲絲不妥。

在SpringMVC的框架下。有些錯誤沒有到我們業務層就被。直接就通過統一異常處理 返回了抱歉!未知錯誤
例如以下Controller

@Slf4j
@RestController
@Api(description = "test接口")
@RequestMapping("/test")
public class TestController {

    @PostMapping("/post")
    public ServerResponse testPOST() throws Exception {
        return ServerResponse.createBySuccessMessage("POST");
    }

    @GetMapping("/get")
    public ServerResponse testGET() throws Exception {
        return ServerResponse.createBySuccessMessage("GET");
    }

	@GetMapping("/value/{value}")
    public ServerResponse testValue(@PathVariable(value = "value") String value) throws Exception {
        return ServerResponse.createBySuccessMessage(value);
    }

    @PostMapping("/body")
    public ServerResponse testBody(@RequestBody Student student) throws Exception {
        return ServerResponse.createBySuccess(student);
    }

}
其中的Student

@Getter
@Setter
public class Student {

    @ApiModelProperty(value = "id")
    private Integer id;

    @ApiModelProperty(value = "名稱")
    private String name;

}

嘗試以下錯誤:
1.用get請求去請求 /test/post 接口
2.發送post請求到 /test/body 但是數據類型寫錯 {id:“abc”,name:“xxx”}
3.發送get請求 /test/value
如果在有CommonExceptionAdvice的情況下分別報錯

1.{    "status": 1,    "msg": "抱歉!未知錯誤"}
2.{    "status": 1,    "msg": "抱歉!未知錯誤"} 
3. There was an unexpected error (type=Not Found, status=404).

在沒有CommonExceptionAdvice的情況下

1.There was an unexpected error (type=Method Not Allowed, status=405).
2.{"timestamp": "2020-03-23T05:13:28.317+0000",  "status": 400,  "error": "Bad Request"}
3. There was an unexpected error (type=Not Found, status=404).

三、總結下問題

出現了我們常常看見的現象
1.只要是我們沒有catch的錯誤,反饋到前端都是未知錯誤,前端同事不知道是什麼錯誤。
2.前端會詢問後端,我調用 xxx 接口 報了個未知錯誤,你幫我看下吧。
3.後端也無從下手 只能翻看日誌,假設日誌沒有很詳細的打印,就需要前端從新發請求。配合調試了。
如此 循環往復。。。後端和前端會增加許多交流成本。。。怪不得前端同事脾氣都大了。哎。。。有問題了 我都不知道啥問題 ,後端大哥你查不到 就要我配合重發請求。

四、 如何解決

4.1 在ServerResponse基礎上解決

再看ResponseCode枚舉

public enum ResponseCode {

    SUCCESS(0,"SUCCESS"),
    ERROR(1,"ERROR"),
    PARAM_ERROR(4,"PARAM_ERROR")
    ;

    private  final  int code;
    private final  String desc;

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

    public int getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}

咋辦 增加code,發現一種錯誤 我增加一種code。並且在Advice中的globalError方法中 對常見的error進行判定 分別返回異常信息,前端拿到錯誤至少是有個code的。

後端知道是哪個code後 就知道是什麼錯誤了 可以有方向的去查。

這樣解決有啥問題呢?
就是 這個code的定義要費腦子了。怎麼可以即容易識別 又能按錯誤類型劃分。。
一定避免設計不好 就出現啥 9999 110092 之類的 不查枚舉 後端都不知道是啥意思了。

但是是可以解決問題的。

4.1 使用SpringFramework中的ResponseEntity

ServerResponse的長期使用 好像讓我忘記了http請求的初心。
Http 的返回碼大家肯定都瞭解,倒背如流
200 表示成功返回。
404 資源沒找到。
500 服務器內部錯誤。
那問題來了 其他的你還知道嗎
401 ? 403?405?406?408? 429?

其實 http協議已經幫我們想好了 各種各樣的錯誤類型
1xx的信息已經接受 請繼續
2xx的請求成功
3xx的請求重定向
4xx的請求錯誤
5xx的服務器錯誤

SpringFramework中的ResponseEntity 其實就是Spring幫我們封裝的ServerResponse。
並且 支持所有的響應嗎

示例CommonException和CommonExceptionAdvice

@Slf4j
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CommonException extends Exception {

    private Exception error;

    private ResponseEntity responseEntity;

    public static CommonException create(ResponseEntity responseEntity) {
        CommonException exception = new CommonException();
        exception.responseEntity = responseEntity;
        return exception;
    }

    public static CommonException create(Exception error, ResponseEntity responseEntity) {
        CommonException exception = new CommonException();
        exception.responseEntity = responseEntity;
        exception.error = error;
        return exception;
    }
}

@Slf4j
@ControllerAdvice
@AllArgsConstructor
@NoArgsConstructor
public class CommonExceptionAdvice {

    @ResponseBody
    @ExceptionHandler(value = {CommonException.class})
    public ResponseEntity myError(HttpServletRequest request, CommonException e) {
        // 如果有err 則打印堆棧
        if (e.getError() != null) {
            log.error(CommonMethod.getTrace(e.getError()));
        }
        log.error(JSONObject.toJSONString(e.getResponseEntity().getBody()));
        return e.getResponseEntity();
    }
    
    @ResponseBody
    @ExceptionHandler(value = {Exception.class})
    public ResponseEntity globalError(HttpServletRequest request, Exception e) {
        log.error(CommonMethod.getTrace(e));
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ResponseEntityBody("抱歉!未知錯誤"));
    }
}

這樣的話 我們就可以使用所有的標準的http響應嗎來描述錯誤
當然 依然要在Advice中的globalError方法中 對常見的error進行判定 分別返回異常信息 和responseCode
這樣我們可以省去自己維護ResponseCode的麻煩。
並且 當前前端同事碰到4xx之後 再也不用請 後端同事幫忙了 節省很多溝通成本

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