一、问题回顾和分析
前段时间,项目中有一个重构需求,将本来在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来存放这些字段值,便于后续的匹配过滤。
(5) 在将datax的数据维护迁移的B中时,为了尽力少的改动,直接使用ConfigModel作为Rpc接口的返回值,然而却报出Dubbo序列化异常。通过异常栈信息可以看到,项目中Dubbo使用了Hession进行序列化和反序列化,并且是在调用JavaDeserializer.instantiate()方法进行Range对象的创建时出现异常。
(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提供的一些类型,或者自己定义的一些比较熟悉的实体类型,避免入坑。