微服务接口设计规范和统一异常处理策略

Owen Jia 2018年12月05日 378次浏览

背景

公司内部服务架构越来越趋向微服务,有着大量接口在相互调用。时间推移接口越来越多,服务的规模数量越急剧增加,同时每个服务的接口设计杂乱无章。如名称不同、判断逻辑不同、错误码不同、字段数量或多或少等等,这在一个分布式系统中是非常头疼的事情,往往一个实现需要对接多个服务(甚至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<T extend BaseModel> {
	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
@Component
public 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。

@ControllerAdvice
public 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,推荐关注他的博客:Owen Blog