一、前言
上篇在介紹 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 接口,在處理業務邏輯時,都可以通過拋出業務異常,再由 Spring AOP 切面捕捉並封裝返回值,從而達到對外接口返回值格式統一的目的。
① 首先在 demo-web 層的 pom 文件中引入 Spring AOP 的依賴包。該包已經集成在 Spring Boot 提供的父工程中,這里直接引入即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
② 其次在 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>
四、結語
至此接口統一返回值格式的方法介紹完畢,如果公司內部項目多了,可以將一些公用的組件提取出來單獨作為一個項目打成二方包供其他項目依賴,保持內部項目的統一。
注:相關代碼已同步至 GitHub:https://github.com/SymonLin/demo