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