年輕人不講武德,竟然重構出這麼優雅後台 API 介面

Hello,早上好,我是樓下小黑哥~

最近偶然間在看到 Spring 官方文檔的時候,新學到一個註解 @ControllerAdvice,並且成功使用這個註解重構我們項目的對外 API 介面,去除繁瑣的重複程式碼,使其開發更加優雅。

展示具體重構程式碼之前,我們先來看下原先對外 API 介面是如何開發的。

這個 API 介面主要是用來與我們 APP 交互,這個過程我們統一定義一個交互協議,APP 端與後台 API 介面統一都使用 JSON 格式。

另外後台 API 介面對 APP 返回時,統一一些錯誤碼,APP 端需要根據相應錯誤碼,在頁面彈出一些提示。

下面展示一個查詢用戶資訊返回的介面數據:

{
    "code": "000000",
    "msg": "success",
    "result": {
        "id": "1",
        "name": "test"
    }
}

code代表對外的錯誤碼,msg代表錯誤資訊,result代表具體返回資訊。

前端 APP 獲取這個返回資訊,首先判斷介面返回 code是否為 000000,如果是代表查詢成功,然後獲取 result 資訊作出相應的展示。否則,直接彈出相應的錯誤資訊。

歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:studyidea.cn

重構之前

下面我們來看下,重構之前的,後台 API 層的如何編碼。

/**
 * V1 版本
 *
 * @return
 */
@RequestMapping("testv1")
public APIResult testv1() {
    try {
        User user = new User();
        user.setId("1");
        user.setName("test");
        return APIResult.success(user);
    } catch (APPException e) {
        log.error("內部異常", e);
        return APIResult.error(e.getCode(), e.getMsg());
    } catch (Exception e) {
        log.error("系統異常", e);
        return APIResult.error(RetCodeEnum.FAILED);
    }
}

上面的程式碼其實很簡單,內部統一封裝了一個工具類 APIResult,然後用其包裝具體的結果。

@Data
public class APIResult<T> implements Serializable {

    private static final long serialVersionUID = 4747774542107711845L;

    private String code;

    private String msg;

    private T result;


    public static <T> APIResult success(T result) {
        APIResult apiResult = new APIResult();
        apiResult.setResult(result);
        apiResult.setCode("000000");
        apiResult.setMsg("success");
        return apiResult;
    }

    public static APIResult error(String code, String msg) {
        APIResult apiResult = new APIResult();
        apiResult.setCode(code);
        apiResult.setMsg(msg);
        return apiResult;
    }

    public static APIResult error(RetCodeEnum codeEnum) {
        APIResult apiResult = new APIResult();
        apiResult.setCode(codeEnum.getCode());
        apiResult.setMsg(codeEnum.getMsg());
        return apiResult;
    }

除了這個以外,還定義一個異常對象 APPException,用來統一包裝內部的各種異常。

上面的程式碼很簡單,但是呢可以說比較繁瑣,重複程式碼也比較多,每個介面都需要使用 try...catch 包裝,然後使用 APIResult包括正常的返回資訊與錯誤資訊。

第二呢,介面對象只能返回 APIResult,真實業務對象只能隱藏在 APIResult中。這樣不太優雅,另外不能很直觀知道真實業務對象。

重構之後

下面我們開始重構上面的程式碼,主要目的是去除重複的那一坨try...catch 程式碼。

這次重構我們需要使用Spring 註解 @ControllerAdvice以及 ResponseBodyAdvice,我們先來看下重構的程式碼。

ps: ResponseBodyAdvice來自 Spring 4.2 API,如果各位同學需要使用這個的話,可能需要升級 Spring 版本。

改寫返回資訊

首先我們需要實現 ResponseBodyAdvice,實現我們自己的處理類。

@ControllerAdvice
public class CustomResponseAdvice implements ResponseBodyAdvice {
    /**
     * 是否需要處理返回結果
     * @param methodParameter
     * @param aClass
     * @return
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        System.out.println("In supports() method of " + getClass().getSimpleName());
        return 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 aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        System.out.println("In beforeBodyWrite() method of " + getClass().getSimpleName());
        if (body instanceof APIResult) {
            return body;
        }
        return APIResult.success(body);
    }
}

實現上面的介面,我們就可以在 beforeBodyWrite方法里,修改返回結果了。

上面程式碼中,只是簡單使用 APIResult包裝了返回結果,然後返回。其實我們還可以在此增加一些額外邏輯,比如說如介面返回資訊由加密的需求,我們可以在這一層統一加密。

另外,這裡判斷一下 body 是否 APIResult類,如果是就直接返回,不做修改。

這麼做一來兼容之前的老介面,這是因為默認情況下,我們自己實現的 CustomResponseAdvice類,將會對所有的 Controller 生效。

如果不做判斷,以前的老接返回就會被包裝了兩層 APIResul,影響 APP 解析。

除此之外,如果大家擔心這個修改對以前的老介面有影響的話,可以使用下面的方式,只對指定的方法生效。

首先自定義一個註解,比如說:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomResponse {
}

然後將其標註在需要改動的方法中,然後我們在 ResponseBodyAdvice#supports中判斷具體方法上有沒有自定義註解 CustomResponse,如果存在,返回 true,這就代表最後將會修改返回類。如果不存在,則返回 false,那麼就會跟以前流程一樣。

/**
 * 是否需要處理返回結果
 *
 * @param methodParameter
 * @param aClass
 * @return
 */
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
    System.out.println("In supports() method of " + getClass().getSimpleName());
    Method method = methodParameter.getMethod();
    return method.isAnnotationPresent(CustomResponse.class);
}

全局異常處理

上面的程式碼重構之後,將重複程式碼抽取了出來,整體的程式碼就剩下我們的業務邏輯,這樣就變得非常簡潔優雅。

不過,上面的重構的程式碼,還是存在問題,主要是異常的處理。

如果上面的業務程式碼拋出了異常,那麼介面將會返回堆棧錯誤資訊,而不是我們定義的錯誤資訊。所以下面我們這個,再次優化一下。

這次我們主要需要使用 @ExceptionHandler註解,這個註解需要與 @ControllerAdvice 一起使用。

@Slf4j
@ControllerAdvice
public class CustomExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public APIResult handleException(Exception e) {
        log.error("系統異常", e);
        return APIResult.error(RetCodeEnum.FAILED);
    }

    @ExceptionHandler(APPException.class)
    @ResponseBody
    public APIResult handleAPPException(APPException e) {
        log.error("內部異常", e);
        return APIResult.error(e.getCode(), e.getMsg());
    }

}

使用這個 @ExceptionHandler,將會攔截相應的異常,然後將會調用的相應方法處理異常。這裡我們就使用 APIResult包裝一些錯誤資訊返回。

總結

我們可以使用 @ControllerAdviceResponseBodyAdvice 攔截返回結果,統一做出一些修改。這樣就可以使用的業務程式碼非常簡潔,優雅。

另外,針對業務程式碼的中,我們可以使用 @ExceptionHandler註解,統一做一個全局異常處理,這樣就可以無縫的跟 ResponseBodyAdvice結合。

不過這裡需要一點,我們實現的 ResponseBodyAdvice 類,一定需要跟 @ControllerAdvice配合一起使用哦,至於具體原因,下篇文章小黑哥分析原來的時候,再具體解釋哦。敬請期待哦~

歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:studyidea.cn

Tags: