1. 程式人生 > >年輕人不講武德,竟然重構出這麼優雅後臺 API 介面

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

Hello,早上好,我是樓下小黑哥~ 最近偶然間在看到 Spring 官方文件的時候,新學到一個註解 `@ControllerAdvice`,並且成功使用這個註解重構我們專案的對外 API 介面,去除繁瑣的重複程式碼,使其開發更加優雅。 展示具體重構程式碼之前,我們先來看下原先對外 API 介面是如何開發的。 這個 API 介面主要是用來與我們 APP 互動,這個過程我們統一定義一個互動協議,APP 端與後臺 API 介面統一都使用 JSON 格式。 另外後臺 API 介面對 APP 返回時,統一一些錯誤碼,APP 端需要根據相應錯誤碼,在頁面彈出一些提示。 下面展示一個查詢使用者資訊返回的介面資料: ```json { "code": "000000", "msg": "success", "result": { "id": "1", "name": "test" } } ``` `code`代表對外的錯誤碼,`msg`代表錯誤資訊,`result`代表具體返回資訊。 前端 APP 獲取這個返回資訊,首先判斷介面返回 `code`是否為 **000000**,如果是代表查詢成功,然後獲取 `result` 資訊作出相應的展示。否則,直接彈出相應的錯誤資訊。 > 歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:[studyidea.cn](https://studyidea.cn) ## 重構之前 下面我們來看下,重構之前的,後臺 API 層的如何編碼。 ```java /** * 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`,然後用其包裝具體的結果。 ```java @Data public class APIResult implements Serializable { private static final long serialVersionUID = 4747774542107711845L; private String code; private String msg; private T result; public static 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`,實現我們自己的處理類。 ```java @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 解析。 除此之外,如果大家擔心這個修改對以前的老介面有影響的話,可以使用下面的方式,只對指定的方法生效。 首先自定義一個註解,比如說: ```java @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CustomResponse { } ``` 然後將其標註在需要改動的方法中,然後我們在 `ResponseBodyAdvice#supports`中判斷具體方法上有沒有自定義註解 `CustomResponse`,如果存在,返回 `true`,這就代表最後將會修改返回類。如果不存在,則返回 `false`,那麼就會跟以前流程一樣。 ```java /** * 是否需要處理返回結果 * * @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` 一起使用。 ```java @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`包裝一些錯誤資訊返回。 ## 總結 我們可以使用 `@ControllerAdvice`加 `ResponseBodyAdvice` 攔截返回結果,統一做出一些修改。這樣就可以使用的業務程式碼非常簡潔,優雅。 另外,針對業務程式碼的中,我們可以使用 `@ExceptionHandler`註解,統一做一個全域性異常處理,這樣就可以無縫的跟 `ResponseBodyAdvice`結合。 不過這裡需要一點,我們實現的 `ResponseBodyAdvice` 類,一定需要跟 `@ControllerAdvice`配合一起使用哦,至於具體原因,下篇文章小黑哥分析原來的時候,再具體解釋哦。敬請期待哦~ > 歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:[studyidea.cn](https://studyi