Pay attention to the serialization problem of hessian

Collection from: http://blog.163.com/jekyll_zhou@126/blog/static/182047382012103033035925/

 

Recently, a very strange problem was found in the project. The ProductDraftDO object was transferred to the remote service, and the ActionTrace of the ProductDraftDO object obtained by the remote service was null . And it's obviously worthwhile before it's transmitted. The ActionTrace class has implemented the serialization interface and all its properties are serializable.

Finally found out the reason, it is a serialization problem. Since the remote service in the project is implemented with dubbo , Hessian is the default serialization protocol of dubbo, and its serialization performance is much higher than that of java . When Hessian serializes an object, the default serializer class is com.caucho.hessian.io.JavaSerializer .

The code snippet of the writeObject method of JavaSerializer is as follows:

    try {

      out.writeMapBegin(cl.getName());

      for (int i = 0; i < _fields.length; i++) {

           Field field = _fields[i];

           out.writeString(field.getName());

           out.writeObject(field.get(obj));

      }

      out.writeMapEnd();

    } catch (IllegalAccessException e) {

      throw new IOException(String.valueOf(e));

}

It seems that there is no problem with writing attributes and attribute values ​​to the stream, but how did these Fields come from? Take a look at the constructor of JavaSerializer :

  

public JavaSerializer(Class cl) {

        _writeReplace = getWriteReplace(cl);

        if (_writeReplace != null) _writeReplace.setAccessible(true);

 

        ArrayList primitiveFields = new ArrayList();

        ArrayList compoundFields = new ArrayList();

        for (; cl != null; cl = cl.getSuperclass()) {

            Field[] fields = cl.getDeclaredFields();

            for (int i = 0; i < fields.length; i++) {

                Field field = fields[i];

                if (Modifier.isTransient(field.getModifiers()) || Modifier.isStatic(field.getModifiers())) continue;

 

                // XXX: could parameterize the handler to only deal with public

                field.setAccessible(true);

 

                if (field.getType().isPrimitive() || field.getType().getName().startsWith("java.lang.")

                    && !field.getType().equals(Object.class)) primitiveFields.add(field);

                else compoundFields.add(field);

            }

        }

 

        ArrayList fields = new ArrayList();

        fields.addAll(primitiveFields);

        fields.addAll(compoundFields);

 

        _fields = new Field[fields.size()];

        fields.toArray(_fields);

    }

获取当前class的所有字段,接着获取父类的所有字段。序列化的时候,所有字段都放在一个ArrayList里,然后依次写入到二进制流中,反序列化的时候,所有字段放在了一个HashMap里,HashMapkey不能重复,悲剧就出现了,如果子类和父类有同名的字段就会有问题,父类的值会把子类的值覆盖掉。

 

看看反序列化时,JavaDeserializergetFieldMap方法,父类字段会把子类字段覆盖掉。

/**

   * Creates a map of the classes fields.

   */

  protected HashMap getFieldMap(Class cl)

  {

    HashMap fieldMap = new HashMap();

   

    for (; cl != null; cl = cl.getSuperclass()) {

      Field []fields = cl.getDeclaredFields();

      for (int i = 0; i < fields.length; i++) {

        Field field = fields[i];

 

        if (Modifier.isTransient(field.getModifiers()) ||

            Modifier.isStatic(field.getModifiers()))

          continue;

        else if (fieldMap.get(field.getName()) != null)

          continue;

 

        // XXX: could parameterize the handler to only deal with public

        try {

          field.setAccessible(true);

        } catch (Throwable e) {

          e.printStackTrace();

        }

 

        fieldMap.put(field.getName(), field);

      }

    }

 

    return fieldMap;

  }

 

然后根据字段接收值,看看JavaDeserializerreadMap方法的代码片段

      while (!in.isEnd()) {

            Object key = in.readObject();

            Field field = (Field) _fieldMap.get(key);

            if (field != null) {

                Object value = in.readObject(field.getType());

                try {

                    field.set(obj, value);

                } catch (Throwable e) {

                    IOException e1 = new IOException("Failed setting: " + field + " with " + value + "\n"

                                                     + e.toString());

                    e1.initCause(e);

                    throw e1;

                }

            } else {

                Object value = in.readObject();

            }

        }

发送二进制数据时,子类数据在前,父类数据在后。

接收二进制数据时,子类数据在前,父类数据在后。

所以对于同名字段,子类的该字段值会被赋值两次,总是被父类的值覆盖,导致子类的字段值丢失。分析完了,再回过头来看看,ProductDraftDO有一个ActionTrace类型字段actionTrace,父类ProductDO也有一个ActionTrace类型字段actionTrace。由于反序列化时,ProductDraftDOactionTrace一直被ProductDOactionTrace覆盖,所以ProductDraftDOactionTrace总是空的。至于为什么父类和子类都有一个ActionTrace,这是历史原因,这里不讨论。这个问题最初由海滔发现,我对代码做了一个完整的分析。

 所以,使用hessian序列化时,一定要注意子类和父类不能有同名字段。在使用dubbo时如果没有指定序列化协议,则也要注意这个问题。

建议dubbo对这个做强制检查。

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326575897&siteId=291194637