hessian反序列化问题

今天工作中遇到一个诡异的问题:dubbo服务中,controller调用service的系统暴露的服务,同步调用,controller报空指针,反序列化失败:

Caused by: com.alibaba.com.caucho.hessian.io.HessianProtocolException: ‘com.xwtech.wingservice.entity.vo.jsjq.JqDesignFeeVo’ could not be instantiated
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:275)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:155)
at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:397)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2070)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2005)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:1990)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:1538)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize(JavaDeserializer.java:396)
… 29 more
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:525)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:271)
… 36 more
Caused by: java.lang.NullPointerException
at com.xwtech.wingservice.entity.jsjq.JqDesignFee.(JqDesignFee.java:502)
at com.xwtech.wingservice.entity.vo.jsjq.JqDesignFeeVo.(JqDesignFeeVo.java:58)
… 41 more

at com.alibaba.dubbo.remoting.exchange.support.DefaultFuture.returnFromResponse(DefaultFuture.java:190) ~[dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.remoting.exchange.support.DefaultFuture.get(DefaultFuture.java:110) ~[dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.remoting.exchange.support.DefaultFuture.get(DefaultFuture.java:84) ~[dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.protocol.dubbo.DubboInvoker.doInvoke(DubboInvoker.java:96) ~[dubbo-2.5.3.jar:2.5.3]
... 61 common frames omitted

报了一个JqDesignFee的一个空指正,那一行代码是刚好类构造函数.通过Debug发现在Service中在返回数据的时候始终会进入这个构造函数,猜测是序列化的时候可能会调用一下POJO类的构造函数. 在返回的实体类中增加了一个空的构造函数就OK了; 在 JavaDeserializer.java:275中有这么一段:

public JavaDeserializer(Class cl)
{
_type = cl;
_fieldMap = getFieldMap(cl);

_readResolve = getReadResolve(cl);

if (_readResolve != null) {
  _readResolve.setAccessible(true);
}

Constructor []constructors = cl.getDeclaredConstructors();
long bestCost = Long.MAX_VALUE;

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

  for (int j = 0; j < param.length; j++) {
cost = 4 * cost;

if (Object.class.equals(param[j]))
  cost += 1;
else if (String.class.equals(param[j]))
  cost += 2;
else if (int.class.equals(param[j]))
  cost += 3;
else if (long.class.equals(param[j]))
  cost += 4;
else if (param[j].isPrimitive())
  cost += 5;
else
  cost += 6;
  }

  if (cost < 0 || cost > (1 << 48))
cost = 1 << 48;

  cost += param.length << 48;

  if (cost < bestCost) {
    _constructor = constructors[i];
    bestCost = cost;
  }
}

if (_constructor != null) {
  _constructor.setAccessible(true);
  Class []params = _constructor.getParameterTypes();
  _constructorArgs = new Object[params.length];
  for (int i = 0; i < params.length; i++) {
    _constructorArgs[i] = getParamArg(params[i]);
  }
}

上面这段代码,是hessian在反序列化的时候,用于在被反序列化的类里面找一个“得分最低”的构造函数,反序列化时会加以调用; 构造函数的“得分”规则大致是:参数越少得分越低;参数个数相同时,参数类型越接近JDK内置类得分越低 而我们的调用返回的类只有一个构造函数,当然只有这个构造函数会被选中 但是,hessian反序列化调用被选中的构造函数时,是这样来创造该构造函数需要的参数的:

protected static Object getParamArg(Class cl)
{
if (! cl.isPrimitive())
return null;
else if (boolean.class.equals(cl))
return Boolean.FALSE;
else if (byte.class.equals(cl))
return new Byte((byte) 0);
else if (short.class.equals(cl))
return new Short((short) 0);
else if (char.class.equals(cl))
return new Character((char) 0);
else if (int.class.equals(cl))
return new Integer(0);
else if (long.class.equals(cl))
return new Long(0);
else if (float.class.equals(cl))
return new Float(0);
else if (double.class.equals(cl))
return new Double(0);
else
throw new UnsupportedOperationException();

可以看到,如果参数不是primitive类型,会被null代替;但不巧的是我们的构造函数内部对该参数调用了一个方法,因此抛出了空指针… 也就是说这个空指针是抛在客户端反序列化的时候而不是服务端内部,因此服务端当然找不到对应的错误日志了; 解决的办法也很简单:可以新增一个无参构造函数(无参构造函数肯定“得分”最低一定会被选中);或者修改代码保证任意参数为null的时候都不会出问题