关于服务返回值的设计

Result

使用 Result<T> 模式的话, 基本上每个方法会长成下面的样子, 此外必须保证 try/catch 外的操作不会抛出运行时异常, 并且 Result 一定非 null , 否则还用 Result 干嘛呢?

有的调用者用起来有点不舒服, 因为每次都需要判断 result.getCode()==Code.OKresult.isSuccess() 然后再用 result.getData() 获取真实的返回值

另外 出错的时候 也无法非常知道非常具体的原因(没有调用栈的信息), 不过通常我们也不需要知道, 比如提示"参数错误"这样基本足够了.

@Override
public Result<List<String>> getXXX(int a, String b, List<Integer> c) {
    if (a <= 0 || StringUtils.isEmpty(b) || CollectionUtils.isEmpty(c)) {//简单的检查
        return (Result<List<String>>) CommonResults.INVALID_PARAMS;
    }
    Result<List<String>> result = new Result<List<String>>();
    try {
        List<String> xxxList = new ArrayList<String>();
        //process ... 这里可能会抛出各种异常
        result.setCode(Code.OK);
        result.setData(xxxList);
    } catch (Exception e) {
        log.warn("...", e);
        result.setCode(Code.ERROR);
        //result.setMsg(..);
        //result.setDesc();
    }
    return result;
}
复制代码

但是在每个方法里都来一个大大的 try/catch , 看了不免心生不爽(如果没有的话就可以跳过了), 并且还要保证 try/catch 外的语句不抛异常. 这种场景 AOP 可以解决.

这样可以保证, 返回值一定非null, 一定不抛出业务异常.

@Override
public Result<List<String>> getXXX2(int a, String b, List<Integer> c) {
    if (a <= 0 || StringUtils.isEmpty(b) || CollectionUtils.isEmpty(c)) {
        return (Result<List<String>>) CommonResults.INVALID_PARAMS;
    }
    List<String> xxxList = new ArrayList<String>();
    //process ... 这里可能会抛出运行时异常 但是并不做处理, 统一使用AOP
    return ResultBuilder.ok(xxxList);
}

用Spring 提供的 AOP机制
@Around("aspectj表达式: 拦截所有返回值为 Result 的方法的执行")
public Object around(ProceedingJoinPoint pjp) {
    try {
        Result<?> result = (Result<?>)pjp.proceed();
        if(result==null){
            result = CommonsResults.EMPTY;
        }
        return result;
    } catch (Exception e) {//发现了未捕获的异常
        //if(log.isDebugEnabled()){log.debug(e);}
        //这里可以实现定义好一些 异常类与code 的映射
        int code = getCodeByException(e);
        return ResultBuilder.create().code(code)..其他方法..build();
    } finally {
        //nothing
    }
}

复制代码

Exception

首先这里处理的是业务异常, 如果HSF本身抛异常(比如找不到服务), 那不在这个的解决范畴里.

在开始之前我们需要先了解一下HSF的反序列化问题

反序列化问题

如果方法随便抛出各种异常的话, 那么调用方可能会遇到: ClassCastExcepation: HashMap cannot be cast to xxx.

这是因为服务抛出了异常, HSF在反序列化这个异常类的时候, 在本地找不到相关的异常类, 因此将一些错误信息做成一个 HashMap , 作为方法的返回值进行返回(而不是抛出反序列化失败异常) , HashMap跟我们的方法返回值类型不一样, 导致类型转换错误.

要是我的返回值就是一个Map呢? 异常被吞了?

另外发现: HSF反序列化异常的时候, 异常似乎被变平了(flatten), 异常的cause被清理掉了(调用反序列化后的异常的getCause()方法返回null).

虽然现在还没有在文档或源码里找到相关的证据, 不过表现出来的确实是如此.

因此将任何抛出的异常套上一个双方都知道的异常就可以避免反序列化失败的问题(但是异常的信息似乎少了一些).

下面是一个例子

最内层抛出的异常是 MyBatisException(自己模拟的一个类), 然后又用 ServiceException 包装了一层.

客户端是没有 UserServiceImpl 和 MyBatisException这些类的, 只有 ServiceException .

下面是客户端打印出的异常结果: 注意观察第一行, 所有异常都被列出来了,

com.xxx.ServiceException: com.yyy.provider.service.impl.MyBatisException: d不能为3
at com.yyy.provider.service.impl.UserServiceImpl.f3(UserServiceImpl.java:72)
at com.yyy.provider.service.impl.UserServiceImpl.f2(UserServiceImpl.java:67)
at com.yyy.provider.service.impl.UserServiceImpl.f1(UserServiceImpl.java:63)
at com.yyy.provider.service.impl.UserServiceImpl.ex3(UserServiceImpl.java:53)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
...省略一堆
但是没有 caused by
复制代码

抛异常的服务

抛异常的好处就是可以快速知道调用链路, 方便调试. 但, 根据上面提到的反序列化问题, 我们的服务就 不能乱抛异常 了, 只能抛一些通用的客户端也知道的异常, 为了保证这点, 我们似乎也只能在每个方法里都放一个大大的 try/catch 捕获所有异常, 然后根据需要进行转型然后重新抛出… 带来了不少麻烦啊…

直接上AOP吧

思路和前面Result处理异常是一样的.

好处是:

1. 在方法里很多情况下不用处理异常了, 随便抛(也不能太任性)

2. 客户端收到的异常是统一的, 都转成了某个异常类的子类

public class ServiceException extends RuntimeException{
    private int code;
    ...
    public ServiceException(){
        this(Code.ERROR);
    }
    public ServiceException(int code){
        this.code=code;
    }
    public int getCode(){
        return code;
    }
}
public class InvalidParamsException extends ServiceException{
    public InvalidParamsException (){
        super(Code.INVALID_PARAMS);
    }
}
public List<String> getXXX3(int a, String b, List<Integer> c) {
    if (a <= 0 || StringUtils.isEmpty(b) || CollectionUtils.isEmpty(c)) {
        throw new InvalidParamsException(); //或 return Collections.emptyList();
    }

    List<String> xxxList = new ArrayList<String>();
    //process ... 这里可能会抛出各种运行时异常(不是ServiceException的子类) 但是并不做处理, 统一使用AOP
    return xxxList;
}

@Around("aspectj表达式: 拦截所有的方法的执行")
public Object around(ProceedingJoinPoint pjp) {
    try {
        return pjp.proceed();
    } catch (Exception e) {//发现了未捕获的异常
        //if(log.isDebugEnabled()){log.debug(e);}

        //判断异常是否已经是 ServiceException(自定义的一个运行时异常)的子类 主要是为了统一抛出类型的异常
        if (e instanceof ServiceException) {
            throw (ServiceException) e;
        }
        throw new ServiceException(e);
    } finally {
        // do nothing
    }
}
复制代码

统一

可以发现, 由于采用的模式不一样, 因此服务实现起来也不一样: 一个是不停的setCode 一个是不停的抛异常, 另外客户端只能跟从服务提供方的模式, 服务端Result 模式, 那你只能使用 Result 接收返回值. 能不能做到服务只用一种模式, 但客户端却可以选择两种模式?

其实重要的是要坚持一种模式, 这样就可以跳过这节了.

对Result 和 ServiceException 进行改造

通过在 Result 里添加 exception 属性, 在 ServiceException 里添加 code 字段, 就基本上实现了两者的互相转换(可能会丢失一些信息).

public class Result<T>{
    private int code;
    private ServiceExcepation exception;
    ...
}
public class ServiceException extends RuntimeException {
    private int code;
    ...
}
复制代码

适配

假设我们的服务都是基于抛异常的, 那么可以新定义一套基于 Result 的接口, 然后在写个适配器, 在每个方法里将请求委托给原有的服务并 catch 异常(是一个 ServiceException 异常), 然后根据这个异常上携带的 code , 就可以构建出 Result

反之也是:

假设我们的服务都是基于 Result<T> 的, 那么可以新定义一套基于抛异常的接口, 然后在写个适配器, 在每个方法里将请求委托给原有的服务, 根据返回值, 决定是否要抛出 ServiceException (可能是子类)异常.

由于适配器里每个方法的实现几乎一样, 因此可以考虑来一个 InvocationHandler :

利用JDK的 Proxy 创建一个代理对象, 来完成每个接口里的方法的 ExceptionResult 的转换.

public interface ExceptionBasedService {
    String getUsernameById(int id);
}
public interface ResultBasedService {
    Result<String> getUsernameById(int id);
}

public class XXXInvocationHandler implements InvocationHandler {
    private final ExceptionBasedService target;

    public XXXInvocationHandler(ExceptionBasedService target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        Method targetMethod = target.getClass().getMethod(methodName, method.getParameterTypes());

        Result result = new Result();
        try {
            Object returnValue = targetMethod.invoke(target, args);
            result.setData(returnValue);
            result.setCode(Code.OK);
        } catch (InvocationTargetException ite) {
            ServiceException e = (ServiceException) ite.getTargetException();
            result.setCode(e.getCode());
        }
        return result;
    }
}
复制代码

TODO

  1. 各自适用场景
  2. 简单性能比较

关于服务返回值的设计

原文 

https://juejin.im/post/5c89dfa26fb9a04a0d57b89c

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 关于服务返回值的设计

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址