年輕人不講武德,竟然重構(gòu)出這么優(yōu)雅后臺(tái) API 接口
本文轉(zhuǎn)載自微信公眾號(hào)「Java極客技術(shù)」,作者鴨血粉絲 。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java極客技術(shù)公眾號(hào)。
Hello,早上好,我是阿粉~
最近偶然間在看到 Spring 官方文檔的時(shí)候,新學(xué)到一個(gè)注解 @ControllerAdvice,并且成功使用這個(gè)注解重構(gòu)我們項(xiàng)目的對(duì)外 API 接口,去除繁瑣的重復(fù)代碼,使其開(kāi)發(fā)更加優(yōu)雅。
展示具體重構(gòu)代碼之前,我們先來(lái)看下原先對(duì)外 API 接口是如何開(kāi)發(fā)的。
這個(gè) API 接口主要是用來(lái)與我們 APP 交互,這個(gè)過(guò)程我們統(tǒng)一定義一個(gè)交互協(xié)議,APP 端與后臺(tái) API 接口統(tǒng)一都使用 JSON 格式。
另外后臺(tái) API 接口對(duì) APP 返回時(shí),統(tǒng)一一些錯(cuò)誤碼,APP 端需要根據(jù)相應(yīng)錯(cuò)誤碼,在頁(yè)面彈出一些提示。
下面展示一個(gè)查詢(xún)用戶(hù)信息返回的接口數(shù)據(jù):
- {
- "code": "000000",
- "msg": "success",
- "result": {
- "id": "1",
- "name": "test"
- }
- }
code代表對(duì)外的錯(cuò)誤碼,msg代表錯(cuò)誤信息,result代表具體返回信息。
前端 APP 獲取這個(gè)返回信息,首先判斷接口返回 code是否為 「000000」,如果是代表查詢(xún)成功,然后獲取 result 信息作出相應(yīng)的展示。否則,直接彈出相應(yīng)的錯(cuò)誤信息。
重構(gòu)之前
下面我們來(lái)看下,重構(gòu)之前的,后臺(tái) 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("內(nèi)部異常", e);
- return APIResult.error(e.getCode(), e.getMsg());
- } catch (Exception e) {
- log.error("系統(tǒng)異常", e);
- return APIResult.error(RetCodeEnum.FAILED);
- }
- }
上面的代碼其實(shí)很簡(jiǎn)單,內(nèi)部統(tǒng)一封裝了一個(gè)工具類(lèi) APIResult,然后用其包裝具體的結(jié)果。
- @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;
- }
除了這個(gè)以外,還定義一個(gè)異常對(duì)象 APPException,用來(lái)統(tǒng)一包裝內(nèi)部的各種異常。
上面的代碼很簡(jiǎn)單,但是呢可以說(shuō)比較繁瑣,重復(fù)代碼也比較多,每個(gè)接口都需要使用 try...catch 包裝,然后使用 APIResult包括正常的返回信息與錯(cuò)誤信息。
第二呢,接口對(duì)象只能返回 APIResult,真實(shí)業(yè)務(wù)對(duì)象只能隱藏在 APIResult中。這樣不太優(yōu)雅,另外不能很直觀知道真實(shí)業(yè)務(wù)對(duì)象。
重構(gòu)之后
下面我們開(kāi)始重構(gòu)上面的代碼,主要目的是去除重復(fù)的那一坨try...catch 代碼。
這次重構(gòu)我們需要使用Spring 注解 @ControllerAdvice以及 ResponseBodyAdvice,我們先來(lái)看下重構(gòu)的代碼。
ps: ResponseBodyAdvice來(lái)自 Spring 4.2 API,如果各位同學(xué)需要使用這個(gè)的話(huà),可能需要升級(jí) Spring 版本。
改寫(xiě)返回信息
首先我們需要實(shí)現(xiàn) ResponseBodyAdvice,實(shí)現(xiàn)我們自己的處理類(lèi)。
- @ControllerAdvice
- public class CustomResponseAdvice implements ResponseBodyAdvice {
- /**
- * 是否需要處理返回結(jié)果
- * @param methodParameter
- * @param aClass
- * @return
- */
- @Override
- public boolean supports(MethodParameter methodParameter, Class aClass) {
- System.out.println("In supports() method of " + getClass().getSimpleName());
- return true;
- }
- /**
- * 處理返回結(jié)果
- * @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);
- }
- }
實(shí)現(xiàn)上面的接口,我們就可以在 beforeBodyWrite方法里,修改返回結(jié)果了。
上面代碼中,只是簡(jiǎn)單使用 APIResult包裝了返回結(jié)果,然后返回。其實(shí)我們還可以在此增加一些額外邏輯,比如說(shuō)如接口返回信息由加密的需求,我們可以在這一層統(tǒng)一加密。
另外,這里判斷一下 body 是否 APIResult類(lèi),如果是就直接返回,不做修改。
這么做一來(lái)兼容之前的老接口,這是因?yàn)槟J(rèn)情況下,我們自己實(shí)現(xiàn)的 CustomResponseAdvice類(lèi),將會(huì)對(duì)所有的 Controller 生效。
如果不做判斷,以前的老接返回就會(huì)被包裝了兩層 APIResul,影響 APP 解析。
除此之外,如果大家擔(dān)心這個(gè)修改對(duì)以前的老接口有影響的話(huà),可以使用下面的方式,只對(duì)指定的方法生效。
首先自定義一個(gè)注解,比如說(shuō):
- @Target({ElementType.TYPE, ElementType.METHOD})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface CustomResponse {
- }
然后將其標(biāo)注在需要改動(dòng)的方法中,然后我們?cè)?ResponseBodyAdvice#supports中判斷具體方法上有沒(méi)有自定義注解 CustomResponse,如果存在,返回 true,這就代表最后將會(huì)修改返回類(lèi)。如果不存在,則返回 false,那么就會(huì)跟以前流程一樣。
- /**
- * 是否需要處理返回結(jié)果
- *
- * @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);
- }
全局異常處理
上面的代碼重構(gòu)之后,將重復(fù)代碼抽取了出來(lái),整體的代碼就剩下我們的業(yè)務(wù)邏輯,這樣就變得非常簡(jiǎn)潔優(yōu)雅。
不過(guò),上面的重構(gòu)的代碼,還是存在問(wèn)題,主要是異常的處理。
如果上面的業(yè)務(wù)代碼拋出了異常,那么接口將會(huì)返回堆棧錯(cuò)誤信息,而不是我們定義的錯(cuò)誤信息。所以下面我們這個(gè),再次優(yōu)化一下。
這次我們主要需要使用 @ExceptionHandler注解,這個(gè)注解需要與 @ControllerAdvice 一起使用。
- @Slf4j
- @ControllerAdvice
- public class CustomExceptionHandler {
- @ExceptionHandler(Exception.class)
- @ResponseBody
- public APIResult handleException(Exception e) {
- log.error("系統(tǒng)異常", e);
- return APIResult.error(RetCodeEnum.FAILED);
- }
- @ExceptionHandler(APPException.class)
- @ResponseBody
- public APIResult handleAPPException(APPException e) {
- log.error("內(nèi)部異常", e);
- return APIResult.error(e.getCode(), e.getMsg());
- }
- }
使用這個(gè) @ExceptionHandler,將會(huì)攔截相應(yīng)的異常,然后將會(huì)調(diào)用的相應(yīng)方法處理異常。這里我們就使用 APIResult包裝一些錯(cuò)誤信息返回。
總結(jié)
我們可以使用 @ControllerAdvice加 ResponseBodyAdvice 攔截返回結(jié)果,統(tǒng)一做出一些修改。這樣就可以使用的業(yè)務(wù)代碼非常簡(jiǎn)潔,優(yōu)雅。
另外,針對(duì)業(yè)務(wù)代碼的中,我們可以使用 @ExceptionHandler注解,統(tǒng)一做一個(gè)全局異常處理,這樣就可以無(wú)縫的跟 ResponseBodyAdvice結(jié)合。
不過(guò)這里需要一點(diǎn),我們實(shí)現(xiàn)的 ResponseBodyAdvice 類(lèi),一定需要跟 @ControllerAdvice配合一起使用哦,至于具體原因,下篇文章阿粉分析原來(lái)的時(shí)候,再具體解釋哦。敬請(qǐng)期待哦~