開發規約(一)介面統一返回值格式
上篇在介紹 Spring Boot 整合 Dubbo 時,埋下了有關返回值格式的一個小小伏筆。本篇將主要介紹一種常用的返回值格式以及通過什麼手段去達成這個目的。
二、Dubbo 統一返回值格式
我們在應用中經常會涉及到 server 和 client 的互動,目前比較流行的是基於 json 格式的資料互動。但是 json 只是訊息的格式,其中的內容還需要我們自行設計。不管是 HTTP 介面還是 RPC 介面保持返回值格式統一很重要,這將大大降低 client 的開發成本。
2.1 定義返回值四要素
- boolean success ;是否成功。
- T data ;成功時具體返回值,失敗時為 null 。
- Integer code ;成功時返回 0 ,失敗時返回具體錯誤碼。
- String message ;成功時返回 null ,失敗時返回具體錯誤訊息。
2.2 定義錯誤碼
為了相容多種型別的錯誤碼,可以通過宣告介面的方式解決,再由具體的業務錯誤碼類實現該介面。
① 首先在 demo-common 層的 com.example.demo.common 包中新增 error 目錄並新建 ServiceErrors 錯誤碼介面類。package com.example.demo.common.error; /** * @author linjian * @date 2019/3/14 */ public interface ServiceErrors { /** * 獲取錯誤碼 * * @return Integer */ Integer getCode(); /** * 獲取錯誤資訊 * * @return String */ String getMessage(); }
② 其次再定義一個業務錯誤碼列舉類實現上述介面類
package com.example.demo.common.error; /** * @author linjian * @date 2019/3/14 */ public enum DemoErrors implements ServiceErrors { /** * 錯誤碼 */ SYSTEM_ERROR(10000, "系統錯誤"), PARAM_ERROR(10001, "引數錯誤"), ; private Integer code; private String message; DemoErrors(Integer code, String message) { this.code = code; this.message = message; } @Override public Integer getCode() { return code; } @Override public String getMessage() { return message; } }
2.3 定義 Result 返回包裝類
繼續在 demo-common 層的 com.example.demo.common 包中新增 entity 目錄並新建 Result 返回包裝類。其中提供了 wrapSuccessfulResult 及 wrapErrorResult 方法用於介面呼叫成功或失敗。
package com.example.demo.common.entity; import com.example.demo.common.error.ServiceErrors; import java.io.Serializable; /** * @author linjian * @date 2019/3/14 */ public class Result<T> implements Serializable { private T data; private boolean success; private Integer code; private String message; public Result() { } public static <T> Result<T> wrapSuccessfulResult(T data) { Result<T> result = new Result<T>(); result.data = data; result.success = true; result.code = 0; return result; } public static <T> Result<T> wrapSuccessfulResult(String message, T data) { Result<T> result = new Result<T>(); result.data = data; result.success = true; result.code = 0; result.message = message; return result; } public static <T> Result<T> wrapErrorResult(ServiceErrors error) { Result<T> result = new Result<T>(); result.success = false; result.code = error.getCode(); result.message = error.getMessage(); return result; } public static <T> Result<T> wrapErrorResult(ServiceErrors error, Object... extendMsg) { Result<T> result = new Result<T>(); result.success = false; result.code = error.getCode(); result.message = String.format(error.getMessage(), extendMsg); return result; } public static <T> Result<T> wrapErrorResult(Integer code, String message) { Result<T> result = new Result<T>(); result.success = false; result.code = code; result.message = message; return result; } public T getData() { return this.data; } public Result<T> setData(T data) { this.data = data; return this; } public boolean isSuccess() { return this.success; } public Result<T> setSuccess(boolean success) { this.success = success; return this; } public Integer getCode() { return this.code; } public Result<T> setCode(Integer code) { this.code = code; return this; } public String getMessage() { return this.message; } public Result<T> setMessage(String message) { this.message = message; return this; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("{"); sb.append("success="); sb.append(this.success); sb.append(","); sb.append("code="); sb.append(this.code); sb.append(","); sb.append("message="); sb.append(this.message); sb.append(","); sb.append("data="); sb.append(this.data); sb.append("}"); return sb.toString(); } }
2.4 定義業務異常類
在 demo-biz 層的 com.example.demo.biz 包中新增 exception 目錄並新建 BizException 異常類。
package com.example.demo.biz.exception; import com.example.demo.common.error.ServiceErrors; /** * @author linjian * @date 2019/3/15 */ public class BizException extends RuntimeException { private final Integer code; public BizException(ServiceErrors errors) { super(errors.getMessage()); this.code = errors.getCode(); } public BizException(Integer code, String message) { super(message); this.code = code; } public Integer getCode() { return this.code; } }
2.5 定義異常處理切面
不管是 HTTP 介面 還是 RPC 介面在處理業務邏輯時,可以通過丟擲業務異常,再由 AOP 切面捕捉並封裝返回值,從而達到對外介面返回值格式統一的目的。
① 首先在 demo-web 層的 com.example.demo.web 包中新增 aspect 目錄並新建 DubboServiceAspect 切面類。在其中通過攔截器及反射實現將業務異常封裝為 Result 返回。
package com.example.demo.web.aspect; import com.example.demo.biz.exception.BizException; import com.example.demo.common.entity.Result; import com.example.demo.common.error.DemoErrors; import lombok.extern.slf4j.Slf4j; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.Arrays; /** * @author linjian * @date 2019/3/14 */ @Slf4j @Component public class DubboServiceAspect implements MethodInterceptor { @Override public Object invoke(final MethodInvocation methodInvocation) throws Throwable { try { return methodInvocation.proceed(); } catch (BizException e) { log.error("BizException", e); return exceptionProcessor(methodInvocation, e); } catch (Exception e) { log.error("Exception:", e); return exceptionProcessor(methodInvocation, e); } } private Object exceptionProcessor(MethodInvocation methodInvocation, Exception e) { Object[] args = methodInvocation.getArguments(); Method method = methodInvocation.getMethod(); String methodName = method.getDeclaringClass().getName() + "." + method.getName(); log.error("dubbo服務[method=" + methodName + "] params=" + Arrays.toString(args) + "異常:", e); Class<?> clazz = method.getReturnType(); if (clazz.equals(Result.class)) { Result result = new Result(); result.setSuccess(false); if (e instanceof BizException) { result.setCode(((BizException) e).getCode()); result.setMessage(e.getMessage()); } else { result.setCode(DemoErrors.SYSTEM_ERROR.getCode()); result.setMessage(DemoErrors.SYSTEM_ERROR.getMessage()); } return result; } return null; } }
② 定義處理類之後再通過 Spring XML 的形式定義切面,在 demo-web 層的 resources 目錄中新建 spring-aop.xml 檔案,在其中定義 Dubbo 介面的切面。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <aop:config> <aop:pointcut id="dubboRemoteServiceAspect" expression="execution(* com.example.demo.remote.service.*.*(..))"/> <aop:advisor advice-ref="dubboServiceAspect" pointcut-ref="remoteServiceAspect"/> </aop:config> </beans>
③ 繼續在 demo-web 層的 resources 目錄中,再新建 application-context.xml 檔案統一管理所有 Spring XML 配置檔案,現在先往其中匯入 spring-aop.xml 檔案。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <import resource="classpath:spring-aop.xml"/> </beans>
④ 最後在 DemoWebApplication 入口類中通過 @ImportResource 註解匯入 Spring 的 XML 配置檔案。
@ImportResource({"classpath:application-context.xml"})
此時處理異常的切面已經配置完畢,接下來通過修改之前定義的 RpcDemoService.test 方法測試切面是否有效。
2.6 切面測試
① 首先將 RpcDemoService.test 方法的返回結果用 Result 包裝。
package com.example.demo.remote.service; import com.example.demo.common.entity.Result; import com.example.demo.remote.model.param.DemoParam; import com.example.demo.remote.model.result.DemoDTO; /** * @author linjian * @date 2019/3/15 */ public interface RpcDemoService { /** * Dubbo 介面測試 * * @param param DemoParam * @return DemoDTO */ Result<DemoDTO> test(DemoParam param); }
package com.example.demo.biz.service.impl.remote; import com.alibaba.dubbo.config.annotation.Service; import com.example.demo.biz.service.DemoService; import com.example.demo.common.entity.Result; import com.example.demo.remote.model.param.DemoParam; import com.example.demo.remote.model.result.DemoDTO; import com.example.demo.remote.service.RpcDemoService; import org.springframework.beans.factory.annotation.Autowired; /** * @author linjian * @date 2019/3/15 */ @Service public class RpcDemoServiceImpl implements RpcDemoService { @Autowired private DemoService demoService; @Override public Result<DemoDTO> test(DemoParam param) { DemoDTO demo = new DemoDTO(); demo.setStr(demoService.test(param.getId())); return Result.wrapSuccessfulResult(demo); } }
② 再修改 DemoService.test 方法的內部邏輯,查詢資料庫後先判斷是否有資料,沒有的話丟擲一個業務異常。
package com.example.demo.biz.service.impl; import com.example.demo.biz.exception.BizException; import com.example.demo.biz.service.DemoService; import com.example.demo.common.error.DemoErrors; import com.example.demo.dao.entity.UserDO; import com.example.demo.dao.mapper.business.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import java.util.Objects; /** * @author linjian * @date 2019/1/15 */ @Service public class DemoServiceImpl implements DemoService { @Autowired private UserMapper userMapper; @Override public String test(Integer id) { Assert.notNull(id, "id不能為空"); UserDO user = userMapper.selectById(id); if (Objects.isNull(user)) { throw new BizException(DemoErrors.USER_IS_NOT_EXIST); } return user.toString(); } }
③ 然後 cd 到 demo-remote 目錄,執行 mvn deploy 命令重新打包。此時服務提供者的調整工作已結束,接下來通過測試專案看效果。
④ 來到測試專案,調整中的 TestController.test 方法,增加 id 傳參。
package com.yibao.dawn.web.controller; import com.alibaba.dubbo.config.annotation.Reference; import com.example.demo.common.entity.Result; import com.example.demo.remote.model.param.DemoParam; import com.example.demo.remote.model.result.DemoDTO; import com.example.demo.remote.service.RpcDemoService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @author linjian * @date 2019/3/7 */ @RestController @RequestMapping("test") public class TestController { @Reference(version = "1.0.0.dev") private RpcDemoService rpcDemoService; @GetMapping("dubbo") public Result<DemoDTO> test(@RequestParam("id") Integer id) { DemoParam param = new DemoParam(); param.setId(id); return rpcDemoService.test(param); } }
⑤ 測試在傳參 id = 1 及 id = 2 的情況下,分別有如下返回結果:

因為此時資料庫中只有 id = 1 的一條資料,當傳參 id = 2 時就觸發了 DemoErrors.USER_IS_NOT_EXIST 的業務異常。
三、HTTP 介面統一返回值格式
3.1 定義切面處理類
package com.example.demo.web.aspect; import com.example.demo.biz.exception.BizException; import com.example.demo.common.entity.Result; import com.example.demo.common.error.DemoErrors; import lombok.extern.slf4j.Slf4j; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.stereotype.Component; /** * @author linjian * @date 2018/9/26 */ @Slf4j @Component public class HttpServiceAspect implements MethodInterceptor { @Override public Result invoke(final MethodInvocation methodInvocation) throws Throwable { Result result = new Result(); try { String methodName = methodInvocation.getMethod().getName(); if (log.isDebugEnabled()) { log.debug("starting business logic processing.... " + methodName); } result = (Result) methodInvocation.proceed(); if (log.isDebugEnabled()) { log.debug("finished business logic processing...." + methodName); } } catch (BizException e) { result.setSuccess(false); result.setCode(e.getCode()); result.setMessage(e.getMessage()); } catch (IllegalArgumentException e) { result.setSuccess(false); result.setCode(DemoErrors.PARAM_ERROR.getCode()); result.setMessage(e.getMessage()); } catch (RuntimeException e) { log.error("系統出錯", e); result.setSuccess(false); result.setCode(DemoErrors.SYSTEM_ERROR.getCode()); result.setMessage(DemoErrors.SYSTEM_ERROR.getMessage()); } return result; } }
3.2 定義切面
在 spring-aop.xml 檔案中追加一個切面定義。
<aop:config> <aop:pointcut id="resultControllerAspect" expression="@within(org.springframework.web.bind.annotation.RestController) and execution(com.example.demo.common.entity.Result *.*(..))"/> <aop:advisor advice-ref="httpServiceAspect" pointcut-ref="resultControllerAspect"/> </aop:config>