上篇在介绍 Spring Boot 集成 Dubbo 时,埋下了有关返回值格式的一个小小伏笔。本篇将主要介绍一种常用的返回值格式以及通过什么手段去达成这个目的。
我们在应用中经常会涉及到 server 和 client 的交互,目前比较流行的是基于 json 格式的数据交互。但是 json 只是消息的格式,其中的内容还需要我们自行设计。不管是 HTTP 接口还是 RPC 接口保持返回值格式统一很重要,这将大大降低 client 的开发成本。
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; } }
继续在 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(); } }
在 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; } }
不管是 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 方法测试切面是否有效。
① 首先将 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 的业务异常。
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; } }
在 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>