背景
公司内部服务架构越来越趋向微服务,有着大量接口在相互调用。时间推移接口越来越多,服务的规模数量越急剧增加,同时每个服务的接口设计杂乱无章。如名称不同、判断逻辑不同、错误码不同、字段数量或多或少等等,这在一个分布式系统中是非常头疼的事情,往往一个实现需要对接多个服务(甚至7-8个服务调用)。
公司的Dubbo微服务架构,很多公司都搭建在内部产品中去使用,越来越趋向于阿里的大中台架构。针对这样的背景我们需要进行接口返回规则统一设计,以达到公司内部所有服务都统一的输出规则。
也类似于开放平台返回参数设计,如微信、支付宝等都是统一的JSON格式加上返回码的策略。这块的定义大多数公司思路是相似的,归宗来看主要如下:
public class Result{ Code code; String msg; Object data;}
data用来返回数据,可以是对象也可以是列表;msg用来返回错误的描述;code返回的是规定格式的错误码,枚举是最为合适;再加上分页结果集的设计基本涵盖到所有场景。
我们设计的思路就是:
要规范返回参数字段的名字和数量,约定所有的接口返回是一套标准,尽可能是简单字段越少越好。如:统一封装到Result对象。
详细的接口设计思路和例子
详细的接口返回类设计思路,主要考虑enum用来作为消息类型,Object或T作为数据类型来使用。
public class Result{ Code code; String msg; Object data; protected Result(){} private Result(Code code,String msg,Object data){ this.code = code; this.msg = msg; this.data = data; } public static Result success(){ return new Result(Code.success,Code.success.getDesc(),null); } public static Result error(){ return new Result(Code.system_error,Code.system_error.getDesc(),null); } // 这里针对异常处理封装 public static Result error(Throwable e){ if(e instanceof ResultException){ ResultException ex = (ResultException)e; return new Result(ex.getCode(),e.getMsg(),null); } return new Result(Code.system_error,Code.system_error.getDesc(),null); } //省略很多代码success(..),error(..)复制方法}
public enum Code{ success(0,"成功"), system_error(-1,"系统错误"), paramter_invalid(1,"请求参数不合法"), ; private int num; private String desc;// 省略contruct \ getXX \setXX}
如考虑严格限制返回类型,可以考虑将Object data换成范型 T data,这样可以限制接口返回必须是规定的类型。参考如下:
public class Result{ Code code; String msg; T data; //类似上面Result设计}
这里的BaseModel是空对象,返回的数据对象需要继承它。
public abstract class BaseModel implements Serializable{}
UserInfo是具体的业务对象,参考具体的业务场景来定义。
public class UserInfo extends BaseModel{ Long id; String name; //省略代码}
针对分页返回结果集设计重点是分页信息类,这点和Mybatis的PageHelper的分页类思路相似,如下格式:
public class PageInfo{ int size; int number; int total; //省略代码}
public class ResultPage{ PageInfo page; private ResultPage(){} private ResultPage(int size,int number,int total){ super(); this.page = new PageInfo(size,number,total); } public static ResultPage success(){ //代码省略 } public static ResultPage error(){ return new ResultPage(0,0,0); } //省略很多代码success(..)和error(..)}
统一异常处理设计
一般的业务思路下使用Result.success()和Result.error()基本涵盖需求。针对事务的回退要求,需要我们进行throw exception操作。常规写法如下:
public class IDemoServiceImpl implements IDemoService{ @Override @Transcational public Result searchDemoInfo(){ //具体业务逻辑 }}
在每个方法里面写try..catch来单独处理异常,这样虽能能解决问题但代码冗余太重也很笨。新定义一个方法来实现事务的throw exception,如下:
public class IDemoServiceImpl implements IDemoService{ @Override public Result searchDemoInfo(){ try{ this.doOne(); } catch (Exception e){ //省略 } } @Transcational private void doOne(){ //具体业务逻辑 }}
我们需要全局来统一处理,而不是对业务进行侵入;只有分离解藕后续我们才能灵活的进行迭代改造。目前使用最多的Http Rest和Dubbo Rpc协议接口,分别使用Spring MVC和Dubbo这两种框架。统一异常处理核心的思想是Spring AOP的aspect,Dubbo比较特别一点可以抛出异常到service customer端处理。
Dubbo接口的异常统一策略
深入聊一下dubbo异常抛出的策略,查看源码类:ExceptionFilter.class。dubbo异常抛出策略主要有以下几种:
- RuntimeException和Exception异常可以抛出;
- 接口上申明了异常类的,可以直接抛出到服务调用者;
- 异常类和接口在一个jar包内,已可以直接抛出到调用者。
- 若异常类的package前缀是java.*或javax.*也可以直接抛出;
- dubbo本身的RcpException可以直接抛出。
@Activate(group = Constants.PROVIDER)public class ExceptionFilter implements Filter {//省略很多代码public Result invoke(Invoker invoker, Invocation invocation) throws RpcException {try { Result result = invoker.invoke(invocation); if (result.hasException() && GenericService.class != invoker.getInterface()) { try { Throwable exception = result.getException(); // 如果是checked异常,直接抛出 if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) { return result; } // 在方法签名上有声明,直接抛出该申明异常 try { Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes()); Class [] exceptionClassses = method.getExceptionTypes(); for (Class exceptionClass : exceptionClassses) { if (exception.getClass().equals(exceptionClass)) { return result; } } } catch (NoSuchMethodException e) { return result; } // 未在方法签名上定义的异常,在服务器端打印ERROR日志 logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception); // 异常类和接口类在同一jar包里,直接抛出 String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface()); String exceptionFile = ReflectUtils.getCodeBase(exception.getClass()); if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){ return result; } // 是JDK自带的异常,直接抛出 String className = exception.getClass().getName(); if (className.startsWith("java.") || className.startsWith("javax.")) { return result; } // 是Dubbo本身的异常,直接抛出 if (exception instanceof RpcException) { return result; } // 否则,包装成RuntimeException抛给客户端 return new RpcResult(new RuntimeException(StringUtils.toString(exception))); } catch (Throwable e) { logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e); return result; } } return result;} catch (RuntimeException e) { logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e); throw e;}}}
我们采用自定义异常类来统一封装处理,在接口包里面定义异常类:ResultException extend RuntimeException。这样可以对异常进行统一封装处理返回Result或者直接抛出自定义异常ResultException,这里推荐采用Aspect进行处理后返回给调用者Result,通过Code状态码判断即可。
public class ResultException extends RuntimeException{ Code code; String msg; public ResultException(Code code){ this.code = code; this.msg = code.system_error.getDesc(); } public ResultException(Code code,String msg){ this.code = code; this.msg = msg; } //省略很多代码}
统一异常处理,AOP思想的around方式包裹整个method进行异常捕获,转换成标准输出给调用者,如下:
@Aspect@Componentpublic class DubboResultExceptionHandler{ @Around("execution(public * com.xxx.xx.xx.service.I*Impl.*(..))") public Result aroudResult(ProceedingJoinPoint pjp){ try{ Object result = pjp.proceed(); if(!(result instanceof Result)) return Result.error(); return (Result)result; } catch(Throwable e){ // 这里请参考前面Result.error(..)的设计 return Result.error(e); } }}
接口设计思路,听过抛出异常来滚回事务,如下:
public class IDemoServiceImpl implements IDemoService{ @Override @Transcational public Result searchDemoInfo(Long id){ this.doOne(); if(id < 10) throw new ResultException(Code.paramter_invalid,"id不能小于10"); return Result.success(); } private void doOne(){ //其他业务实现 }}
SpringMVC接口异常统一策略
SpringMVC异常处理依赖@ControllerAdvice和ResponseEntityExceptionHandler,可以拦截Controller层抛出的指定异常处理统一返回Result。
@ControllerAdvicepublic class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({Exception.class, RuntimeException.class}) @ResponseBody public Result doHandler(Exception e){ Result error; if(e instanceof ResultException){ ResultException me = (ResultException) e; error = Result.error(me.getCode(),me.getMsg()) } else { e.printStackTrace(); error = Result.error(Code.system_error,Code.system_error.getDesc()); } return error; }}
技术的路上我们风雨同行,感谢你们的支持。
作者:Owen jia,推荐关注他的博客: 。