Guava Range和Dubbo一起使用出现的坑

王二北原创,转载请标明出处:来自王二北

一、问题回顾和分析

前段时间,项目中有一个重构需求,将本来在A端中暂存的数据dataX(A中本不该维护这份数据),移到B服务中,然后A通过RPC访问B来获得数据,这样的好处是提高内聚性、理清业务边界。
结果在实施的过程中,却出现了问题:

(1)在跑自测时发现,A通过B提供的RPC接口获取dataX时,报错:

com.alibaba.com.caucho.hessian.io.HessianFieldException:com.wang.erbei.model.ConfigModel.priceRange:
 Caused by: com.alibaba.com.caucho.hessian.io.HessianProtocolException: 'com.google.common.collect.Range' could not be instantiated

(2)感觉很奇怪,priceRange这个字段是Guava Range类型的,在从A迁移到B中之前,一直使用这个类型,很方便进行业务的匹配处理,为什么通过RPC传输时就有问题了呢,思考无果后,本着有问题,先Google的原则,google了一圈,也没有找到类似的问题的解决办法。
(3)于是只好结合者错误信息从源码层面分析问题。
(4)在A业务中,一条dataX对应一个CofigModel对象,其中价格、税率是进行范
围匹配的,因此使用了GuavaRange来存放这些字段值,便于后续的匹配过滤。

image

image

(5) 在将datax的数据维护迁移的B中时,为了尽力少的改动,直接使用ConfigModel作为Rpc接口的返回值,然而却报出Dubbo序列化异常。通过异常栈信息可以看到,项目中Dubbo使用了Hession进行序列化和反序列化,并且是在调用JavaDeserializer.instantiate()方法进行Range对象的创建时出现异常。

image

(6)下面是JavaDeserializer.instantiate()方法的实现,作用是调用目标类的构造器,创建对象,既然在调用这个方法时出错,那就说在调用Rnage的构造函数时出现了异常。

protected Object instantiate() throws Exception {
        try {
            return this._constructor != null ? this._constructor.newInstance(this._constructorArgs) : this._type.newInstance();
        } catch (Exception var2) {
            throw new HessianProtocolException("'" + this._type.getName() + "' could not be instantiated", var2);
        }
    }

(7) 查看Range类,发现其只有一个带参数的构造方法,而且该方法只有在范围值设置无效(比如最大值小于最小值)时,或者构造参数为null时才会出现构建失败。通过case分析,排除了参数设置无效的可能,另外一种就是参数为null。

 private Range(Cut<C> lowerBound, Cut<C> upperBound) {
    if (lowerBound.compareTo(upperBound) > 0 || lowerBound == Cut.<C>aboveAll()
        || upperBound == Cut.<C>belowAll()) {
      throw new IllegalArgumentException("Invalid range: " + toString(lowerBound, upperBound));
    }
    this.lowerBound = checkNotNull(lowerBound);
    this.upperBound = checkNotNull(upperBound);
  }
public static <T> T checkNotNull(T reference) {
    if (reference == null) {
      throw new NullPointerException();
    }
    return reference;
  }

(8)构造参数为null? 不可能啊,在业务中,这两个字段是必填的,而且在B服务中debug可以看到,传值时,Rnage对应的字段都是有值的,怎么会为null呢?
回头看一下上面(6)中讲JavaDeserializer.instantiate()是调用目标类的构造函数创建对象,那么是怎么传参数的呢?通过_constructorArgs传入。_constructorArgs整个是怎么来的呢?
通过下面的JavaDeserializer源码,可以看出来,一个目标类对象对应一个JavaDeserializer对象,当通过目标类(如Range)构造函数创建对象之前,需要显构建所需参数的初始值,在getParamArg方法中,如果参数是引用类型(包括String),则返回null,如果是基本类型,则返回对应的初始值,比如INT类型,则返回0。

public class JavaDeserializer extends AbstractMapDeserializer {
    private Class<?> _type;
    private HashMap<?, JavaDeserializer.FieldDeserializer> _fieldMap;
    private Method _readResolve;
    private Constructor<?> _constructor;
    // 目标类构造方法参数
    private Object[] _constructorArgs;

    public JavaDeserializer(Class<?> cl) {
        this._type = cl;
        this._fieldMap = this.getFieldMap(cl);
        this._readResolve = this.getReadResolve(cl);
        if (this._readResolve != null) {
            this._readResolve.setAccessible(true);
        }
        // 下面是选择目标类的构造方法
        Constructor<?>[] constructors = cl.getDeclaredConstructors();
        long bestCost = 9223372036854775807L;

        for(int i = 0; i < constructors.length; ++i) {
            Class<?>[] param = constructors[i].getParameterTypes();
            long cost = 0L;

            for(int j = 0; j < param.length; ++j) {
                cost = 4L * cost;
                if (Object.class.equals(param[j])) {
                    ++cost;
                } else if (String.class.equals(param[j])) {
                    cost += 2L;
                } else if (Integer.TYPE.equals(param[j])) {
                    cost += 3L;
                } else if (Long.TYPE.equals(param[j])) {
                    cost += 4L;
                } else if (param[j].isPrimitive()) {
                    cost += 5L;
                } else {
                    cost += 6L;
                }
            }

            if (cost < 0L || cost > 65536L) {
                cost = 65536L;
            }

            cost += (long)param.length << 48;
            if (cost < bestCost) {
                this._constructor = constructors[i];
                bestCost = cost;
            }
        }

        if (this._constructor != null) {
            this._constructor.setAccessible(true);
            // 目标类构造方法的参数类型
            Class<?>[] params = this._constructor.getParameterTypes();
            // 构造方法入参参数数组
            this._constructorArgs = new Object[params.length];
            // 这里处理目标构造方法的入参的初始值
            for(int i = 0; i < params.length; ++i) {
                this._constructorArgs[i] = getParamArg(params[i]);
            }
        }

    }
    
    // 下面这个方法就是获得不同类型的初始值,如果是引用类型,初始值null
    // 基本类型,则返回对应的基本类型值
    protected static Object getParamArg(Class<?> cl) {
        // 判断是否引用类型,如果是,则返回null
        if (!cl.isPrimitive()) {
            return null;
        } else if (Boolean.TYPE.equals(cl)) {
            return Boolean.FALSE;
        } else if (Byte.TYPE.equals(cl)) {
            return new Byte((byte)0);
        } else if (Short.TYPE.equals(cl)) {
            return new Short((short)0);
        } else if (Character.TYPE.equals(cl)) {
            return new Character('\u0000');
        } else if (Integer.TYPE.equals(cl)) {
            return 0;
        } else if (Long.TYPE.equals(cl)) {
            return 0L;
        } else if (Float.TYPE.equals(cl)) {
            return 0.0F;
        } else if (Double.TYPE.equals(cl)) {
            return 0.0D;
        } else {
            throw new UnsupportedOperationException();
        }
    }
    
    
     /**
     * Determines if the specified {@code Class} object represents a
     * primitive type..
     *
     * @return true if and only if this class represents a primitive type
     *
     * @see     java.lang.Boolean#TYPE
     * @see     java.lang.Character#TYPE
     * @see     java.lang.Byte#TYPE
     * @see     java.lang.Short#TYPE
     * @see     java.lang.Integer#TYPE
     * @see     java.lang.Long#TYPE
     * @see     java.lang.Float#TYPE
     * @see     java.lang.Double#TYPE
     * @see     java.lang.Void#TYPE
     * @since JDK1.1
     */
    public native boolean isPrimitive();

(9)、在(7)中我们讲过,Range构造函数中,会校验参数是否为null,会判断参数是否为null,如果null则直接抛出空指针异常.在业务中用到的值都是String类型的,自然其初始值也是null,这就触发了Rnage构造函数为null的校验,报出空指针异常,而这个异常会被JavaDeserializer捕获,抛出could not be instantiated,这样就找到了问题的根源。

解决办法

找到了问题的根源,要解决就好办了,要么修改工具的源码,要么修改你自己的业务源码,修改工具源码成本太大,那就只能修改业务代码了:</br>
将ConfigModel替换为ConfigModelBase类,将其中的Range类型字段换为String类型,A在获取到数据后,再转为ConfigModel去使用。

二、总结

1、Guava Rnage和Dubbo一起使用时,会有坑,通过Dubbo Rpc(使用Hession序列化)传输的实体中包含guava Range类型的字段时,会报异常。

2、原因是Hession 反序列化调用目标类的构造函数创建目标类对象时,会将使用构造参数的初始值去创建对象, 引用类型(包括String)的初始值为null。而Guava Rnage的构造函数中会校验构造参数是否为null,如果为null就会报空指针异常。

3、所以一般在使用Dubbo Rpc传输时,最好使用java提供的一些类型,或者自己定义的一些比较熟悉的实体类型,避免入坑。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容