Spring Boot 無侵入式 實現RESTful API介面統一JSON格式返回

前言

現在我們做項目基本上中大型項目都是選擇前後端分離,前後端分離已經成了一個趨勢了,所以總這樣·我們就要和前端約定統一的api 介面返回json 格式,

這樣我們需要封裝一個統一通用全局 模版api返回格式,下次再寫項目時候直接拿來用就可以了

約定JSON格式

一般我們和前端約定json格式是這樣的

{
    "code": 200,
    "message": "成功",
    "data": {
 
    }
}
  • code: 返回狀態碼
  • message: 返回資訊的描述
  • data: 返回值

封裝java bean

定義狀態枚舉

package cn.soboys.core.ret;


import lombok.Data;
import lombok.Getter;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/6/17 15:35
 * 響應碼枚舉,對應HTTP狀態碼
 */
@Getter
public enum ResultCode {

    SUCCESS(200, "成功"),//成功
    //FAIL(400, "失敗"),//失敗
    BAD_REQUEST(400, "Bad Request"),
    UNAUTHORIZED(401, "認證失敗"),//未認證
    NOT_FOUND(404, "介面不存在"),//介面不存在
    INTERNAL_SERVER_ERROR(500, "系統繁忙"),//伺服器內部錯誤
    METHOD_NOT_ALLOWED(405,"方法不被允許"),

    /*參數錯誤:1001-1999*/
    PARAMS_IS_INVALID(1001, "參數無效"),
    PARAMS_IS_BLANK(1002, "參數為空");
    /*用戶錯誤2001-2999*/


    private Integer code;
    private String message;

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

定義返回狀態碼,和資訊一一對應,我們可以約定xxx~xxx 為什麼錯誤碼,防止後期錯誤碼重複,使用混亂不清楚,

定義返回體結果體

package cn.soboys.core.ret;

import lombok.Data;

import java.io.Serializable;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/6/17 15:47
 * 統一API響應結果格式封裝
 */
@Data
public class Result<T> implements Serializable {

    private static final long serialVersionUID = 6308315887056661996L;
    private Integer code;
    private String message;
    private T data;


    public Result setResult(ResultCode resultCode) {
        this.code = resultCode.getCode();
        this.message = resultCode.getMessage();
        return this;
    }

    public Result setResult(ResultCode resultCode,T data) {
        this.code = resultCode.getCode();
        this.message = resultCode.getMessage();
        this.setData(data);
        return this;
    }


}

code,和message都從定義的狀態枚舉中獲取

這裡有兩個需要注意地方我的數據類型T data返回的是泛型類型而不是object類型而且我的結果累實現了Serializable介面

我看到網上有很多返回object,最後返回泛型因為泛型效率要高於object,object需要強制類型轉換,還有最後實現了Serializable介面因為通過流bytes傳輸方式web傳輸,速率更塊

定義返回結果方法

一般業務返回要麼是 success成功,要麼就是failure失敗,所以我們需要單獨定義兩個返回實體對象方法,

package cn.soboys.core.ret;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/6/17 16:30
 * 響應結果返回封裝
 */
public class ResultResponse {
    private static final String DEFAULT_SUCCESS_MESSAGE = "SUCCESS";

    // 只返回狀態
    public static Result success() {
        return new Result()
                .setResult(ResultCode.SUCCESS);
    }

    // 成功返回數據
    public static Result success(Object data) {
        return new Result()
                .setResult(ResultCode.SUCCESS, data);


    }

    // 失敗
    public static Result failure(ResultCode resultCode) {
        return new Result()
                .setResult(resultCode);
    }

    // 失敗
    public static Result failure(ResultCode resultCode, Object data) {
        return new Result()
                .setResult(resultCode, data);
    }



}

注意這裡我定義的是靜態工具方法,因為使用構造方法進行創建對象調用太麻煩了, 我們使用靜態方法來就直接類調用很方便

這樣我們就可以在controller中很方便返回統一api格式了

 package cn.soboys.mallapi.controller;

import cn.soboys.core.ret.Result;
import cn.soboys.core.ret.ResultResponse;
import cn.soboys.mallapi.bean.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/7/2 20:28
 */  
@RestController   //默認全部返回json
@RequestMapping("/user")
public class UserController {
    @GetMapping("/list")
    public Result getUserInfo(){
        User u=new User();
        u.setUserId("21");
        u.setUsername("kenx");
        u.setPassword("224r2");
        return ResultResponse.success(u);
    }
}

返回結果符合我們預期json格式
但是這個程式碼還可以優化,不夠完善,比如,每次controller中所有的方法的返回必須都是要Result類型,我們想返回其他類型格式怎麼半,還有就是不夠語義化,其他開發人員看你方法根本就不知道具體返回什麼資訊

如果改成這個樣子就完美了如

 @GetMapping("/list")
    public User getUserInfo() {
        User u = new User();
        u.setUserId("21");
        u.setUsername("kenx");
        u.setPassword("224r2");
        return u;
    }

其他開發人員一看就知道具體是返回什麼數據。但這個格式要怎麼去統一出來?

其實我們可以這麼去優化,通過SpringBoot提供的ResponseBodyAdvice進行統一響應處理

  1. 自定義註解@ResponseResult來攔截有此controller註解類的代表需要統一返回json格式,沒有就安照原來返回
package cn.soboys.core.ret;

import java.lang.annotation.*;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/6/17 16:43
 * 統一包裝介面返回的值 Result
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseResult {
}

  1. 定義請求攔截器通過反射獲取到有此註解的HandlerMethod設置包裝攔截標誌
package cn.soboys.core.ret;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/6/17 17:10
 * 請求攔截
 */
public class ResponseResultInterceptor implements HandlerInterceptor {

    //標記名稱
    public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //請求方法
        if (handler instanceof HandlerMethod) {
            final HandlerMethod handlerMethod = (HandlerMethod) handler;
            final Class<?> clazz = handlerMethod.getBeanType();
            final Method method = handlerMethod.getMethod();
            //判斷是否在對象上加了註解
            if (clazz.isAnnotationPresent(ResponseResult.class)) {
                //設置此請求返回體需要包裝,往下傳遞,在ResponseBodyAdvice介面進行判斷
                request.setAttribute(RESPONSE_RESULT_ANN, clazz.getAnnotation(ResponseResult.class));
                //方法體上是否有註解
            } else if (method.isAnnotationPresent(ResponseResult.class)) {
                //設置此請求返回體需要包裝,往下傳遞,在ResponseBodyAdvice介面進行判斷
                request.setAttribute(RESPONSE_RESULT_ANN, clazz.getAnnotation(ResponseResult.class));
            }
        }
        return true;
    }
}

  1. 實現ResponseBodyAdvice<Object> 介面自定義json返回解析器根據包裝攔截標誌判斷是否需要自定義返回類型返回類型
package cn.soboys.core.ret;

import cn.soboys.core.utils.HttpContextUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;


import javax.servlet.http.HttpServletRequest;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/6/17 16:47
 * 全局統一響應返回體處理
 */
@Slf4j
@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {

    public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";

    /**
     * @param methodParameter
     * @param aClass
     * @return 此處如果返回false , 則不執行當前Advice的業務
     * 是否請求包含了包裝註解 標記,沒有直接返回不需要重寫返回體,
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        HttpServletRequest request = HttpContextUtil.getRequest();
        //判斷請求是否有包裝標誌
        ResponseResult responseResultAnn = (ResponseResult) request.getAttribute(RESPONSE_RESULT_ANN);
        return responseResultAnn == null ? false : true;
    }

    /**
     * @param body
     * @param methodParameter
     * @param mediaType
     * @param aClass
     * @param serverHttpRequest
     * @param serverHttpResponse
     * @return 處理響應的具體業務方法
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (body instanceof Result) {
            return body;
        } else if (body instanceof String) {
            return JSON.toJSONString(ResultResponse.success(body));
        } else {
            return ResultResponse.success(body);
        }
    }
}

注意這裡string類型返回要單獨json序列化返回一下,不然會報轉換異常

這樣我們就可以在controler中返回任意類型,了不用每次都必須返回 Result

package cn.soboys.mallapi.controller;

import cn.soboys.core.ret.ResponseResult;
import cn.soboys.core.ret.Result;
import cn.soboys.core.ret.ResultResponse;
import cn.soboys.mallapi.bean.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/7/2 20:28
 */
@RestController   //默認全部返回json
@RequestMapping("/user")
@ResponseResult
public class UserController {
    @GetMapping("/list")
    public User getUserInfo() {
        User u = new User();
        u.setUserId("21");
        u.setUsername("kenx");
        u.setPassword("224r2");
        return u;
    }

    @GetMapping("/test")
    public String test() {
        return "ok";
    }
    @GetMapping("/test2")
    public Result test1(){
        return ResultResponse.success();
    }

}

這裡還有一個問題?正常情況返回成功的話是統一json 格式,但是返回失敗,或者異常了,怎麼統一返回錯誤json 格式,sprinboot有自己的錯誤格式?

請參考我上一篇,SpringBoot優雅的全局異常處理

掃碼關注公眾號猿人生了解更多好文