`

Hessian和Java反序列化问题小结

 
阅读更多

Hessian反序列化问题

众所周知,Hessian框架提供的序列化方式,在性能上要优于Java自己的序列化方式。他将对象序列化,生成的字节数组的数量要相对于Java自带的序列化方式要更简洁。

目前公司的一个项目中,有RPC调用的需要,这里我们使用了公司自己的开源RPC框架Dubbo作为远程调用框架,进行业务方法的调用和对象的序列化。这里,我们没有对Dubbo做出特殊配置,Dubbo在Remoting层组件默认的序列化方式就是采用的Hessian协议处理。但是在真正部署测试时,走到需要远程调用的方式时,报出了一下异常(只截取了最核心的异常堆栈):

 

Caused by: com.alibaba.com.caucho.hessian.io.HessianProtocolException: 'com.alibaba.ais.bdc.person.vo.CountVO$CountObject' 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.CollectionDeserializer.readLengthList(CollectionDeserializer.java:93)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:1678)
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize(JavaDeserializer.java:396)
	... 42 more
Caused by: java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:271)
	... 50 more
Caused by: java.lang.IllegalArgumentException: [Assertion failed] - this argument is required; it must not be null
	at org.springframework.util.Assert.notNull(Assert.java:112)
	at org.springframework.util.Assert.notNull(Assert.java:123)
	at com.alibaba.ais.bdc.person.vo.CountVO$CountObject.<init>(CountVO.java:101)
	... 55 more

从最下面的异常信息可以看出,CountObject这个内部类在对象初始化时,报了参数校验的失败。这个看一下CountObject的出问题的构造函数就一目了然了:

        public CountObject(SimplePerson simplePerson, String imagePrefix){
            Assert.notNull(simplePerson);
            if (StringUtils.isEmpty(imagePrefix)) {
                throw new IllegalArgumentException("imagePrefix [" + imagePrefix + "] is meaningless.");
            }
            this.id = simplePerson.getEmployeeId();
            this.name = simplePerson.getRealName();
            this.imagePath = StringUtils.isEmpty(simplePerson.getImagePathSuffix()) ? null : imagePrefix
                                                                                             + simplePerson.getImagePathSuffix();
        }

现在在构造函数的第一行的Assert就失败了。可是哪里调用这个构造函数导致失败呢?继续网上翻看异常堆栈给出的信息。可以看出在JavaDeserializer.instantiate中抛出了HessianProtocolException异常。进去看一下Hessian这块的源码如下:

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

这里结合上面的异常堆栈可以知道,上面出问题的关键是_constructor_constructorArgs。这两个东东又到底是啥呢?继续来看代码:

  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 += (long) param.length << 48;
      // _constructor will reference to the constructor with least parameters.
      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]);
      }
    }
  }

JavaDeserializer的构造方法中可以看出,这里_constructor会被赋予参数最少的那个构造器。再回过头去看看CountObject的构造器(就上面列出来的那一个),不难看出,这里的_constructor就是上面的那个构造器了。

  /**
   * Creates a map of the classes fields.
   */
  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 Integer.valueOf(0);
    else if (long.class.equals(cl))
      return Long.valueOf(0);
    else if (float.class.equals(cl))
      return Float.valueOf(0);
    else if (double.class.equals(cl))
      return Double.valueOf(0);
    else
      throw new UnsupportedOperationException();
  }

参看上面的getParamArg方法,就可以知道,由于CountObject唯一的一个构造器的两个参数都不是基本类型,所以这里_constructorArgs所包含的值全部是null。

OK,到这里,上面的异常就搞清楚了,Hessian反序列化时,使用反射调用构造函数生成对象时,传入的参数不合法,造成了上面的异常。知道了原因,解决的方法也很简单,就是添加了一个无参的构造器给CountObject,于是上面的问题就解决了。。。

这里,需要注意的是,如果序列化机制使用的是Hessian,序列化的对象又没有提供默认的无参构造器时,需要注意上面类似的问题了。

Java本身反序列化问题

Java本身的反序列化机制虽然性能稍差一些,但本身使用的约束条件相对却要宽松一些,其实只要满足下面两条,一个类对象就是可以完美支持序列化机制了:


  1. 类实现java.io.Serializable接口。
  2. 类包含的所有属性都是实现了java.io.Serializable接口的,或者被标记为了transient。

对于构造函数本身没有任何约束。这里,Java序列化本身其实也是和new以及Java反射机制“平级”的实例化对象的方式。所以,对于单例模式的场景,还是需要考虑是否会有序列化因素造成的单例失效(因为他实例化对象不依赖于构造器,所以一个private的构造器显然没法阻止他的“胡作非为”)。当然,对于这种情况,也可以自己实现下面的方法:

 

private Object readResolve()

通过实现上面的方法,自己可以在其中明确指定,放回的对象的实例是哪一个。但对于通过如上方式保证的单例模式至少需要注意一下两点:


  1. readResolve方法的可见性(public/protected/private)问题:因为如果这个方法不是private的,就有可能被起子类直接继承过去。这可能造成你在反序列化子类对象时出错(因为这个方法返回了父类的某个固定的对象)。
  2. 使用readResolve方法时,往往比较容易返回某个固定的对象。但这其实和真正的对象反序列化其实是有点矛盾的。因为你反序列化对象时,多数场景都是希望恢复原来的对象的“状态”,而不是固定的某个对象。所以只要你的类内的属性有没有被标识成transient的,就要格外小心了。

鉴于上面所说的稍微复杂的现象,如果单纯的考虑单例的需要,更好的方式是通过枚举来实现,因为枚举至少可以在JVM层面,帮你保证每个枚举实例一定是单例的,即使使用反序列化机制,也无法绕过这个限制,所以可以帮你省不少心。

好了,上面扯的有点远了,关于Java本身的序列化机制,下面写了一个简单的把对象序列化成字节数组,再由字节数组反序列化回来的例子,看完之后应该会更明了一些:

 

public class Person implements Serializable {

    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
    }

    private static class Employee extends Person{

        String title;

        private Employee(String name, int age, String title) {
            super(name, age);
            this.title = title;
        }

        @Override
        public String toString() {
            return "Employee{" + "name='" + name + '\'' +
                ", age=" + age + '\'' +
                ", title='" + title + '\'' +
                '}';
        }
    }

    public static void main(String[] args) {
        byte[] bytes;
        Person person1 = new Person( "test1",20 );
        Person person2;
        Employee employee1 = new Employee( "employee1",25,"Manager" );
        Employee employee2;

        ByteArrayOutputStream byteOutputStream = null;
        ObjectOutputStream objectOutputStream = null;

        ByteArrayInputStream byteArrayInputStream = null;
        ObjectInputStream objectInputStream = null;

        try {
            //generate byteArray.
            byteOutputStream = new ByteArrayOutputStream( );
            objectOutputStream = new ObjectOutputStream( byteOutputStream);
            //serialize person1
            objectOutputStream.writeObject( person1 );
            //serialize employee1
            objectOutputStream.writeObject( employee1 );

            bytes = byteOutputStream.toByteArray();

            for (byte aByte : bytes) {
                System.out.print(aByte);
            }
            System.out.println();
            System.out.println("Bytes's length is :"+bytes.length);

            //generate Object from byteArray.
            byteArrayInputStream = new ByteArrayInputStream( bytes );
            objectInputStream = new ObjectInputStream( byteArrayInputStream );
            //deserialize person1
            person2 = (Person)objectInputStream.readObject();
            //deserialize employee1
            employee2 = (Employee)objectInputStream.readObject();
            System.out.println("person2 got from byteArray is : "+person2);
            System.out.println("employee2 got from byteArray is : "+employee2);

            System.out.println("person1's memory id :"+Integer.toHexString(person1.hashCode()));
            System.out.println("person2's memory id :"+Integer.toHexString(person2.hashCode()));
            System.out.println("employee1's memory id :"+Integer.toHexString(employee1.hashCode()));
            System.out.println("employee2's memory id :"+Integer.toHexString(employee2.hashCode()));

        } catch (IOException e) {
            e.printStackTrace();
        }catch ( ClassNotFoundException ce ){
            ce.printStackTrace();
        }
        finally {
            try {
                byteOutputStream.close();
                objectOutputStream.close();
                byteArrayInputStream.close();
                objectInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

上面代码执行的结果如下:

-84-19051151140329911110946115107121461191191194611510111410597108105122971161051111104680101114115111110-97-123-26-11-111120-40-115202730397103101760411097109101116018761069711897471089711010347831161141051101035912011200020116051161011151164911511404199111109461151071214611911911946115101114105971081051229711610511111046801011141151111103669109112108111121101101-11-66110-28-62-10611536201760511610511610810111301260112011301260000025116091011091121081111211011014911607779711097103101114
Bytes's length is :200
person2 got from byteArray is : Person{name='test1', age=20}
employee2 got from byteArray is : Employee{name='employee1', age=25', title='Manager'}
person1's memory id :29173ef
person2's memory id :96fa474
employee1's memory id :6c121f1d
employee2's memory id :95c083

最后再补充一个Java序列化规范的地址,有时间时再细读一下:http://docs.oracle.com/javase/7/docs/platform/serialization/spec/serial-arch.html  

     

分享到:
评论
2 楼 chembo 2015-03-03  
读君一博,解我一周愁
1 楼 fanjicaho 2012-10-06  
不错 挺好的,和我想的差不多。大概都一样。

相关推荐

    hessian学习基础篇——序列化和反序列化

    本文将深入探讨Hessian框架的基础知识,它是一个高效的二进制序列化协议,广泛应用于Java和.NET之间跨语言通信。通过学习Hessian,我们可以更有效地处理数据传输,提高应用性能。 首先,让我们理解什么是序列化。...

    S25-hessian反序列化1

    与原生Java序列化相比,Hessian序列化具有更快的速度和更小的数据传输量。 在Java中,通常只有实现了`java.io.Serializable`接口的类才能被序列化。然而,描述中提到,即使类如`javax.naming.spi....

    Hessian 的字段序列化小记

    在序列化过程中,Hessian会将Java对象转换为字节流,反序列化时则将字节流恢复为原始对象。 1. **Hessian序列化流程** - 首先,Hessian序列化器会遍历Java对象的所有字段,对每个字段进行处理。 - 对于基本类型,...

    Nacos JRaft Hessian 反序列化 RCE 分析.pdf

    这一安全问题的核心在于 Nacos JRaft 在处理来自客户端的特定构造的数据时未能正确地验证或过滤这些数据,从而允许攻击者通过发送恶意构造的 Hessian 数据包来触发反序列化操作,进而执行任意代码。 #### 三、漏洞...

    java 反序列化利用工具marshalsec-0.0.3-SNAPSHOT-all

    -a:生成exploit下的所有payload(例如:hessian下的SpringPartiallyComparableAdvisorHolder, SpringAbstractBeanFactoryPointcutAdvisor, Rome, XBean, Resin) -t:对生成的payloads进行解码测试 -v:verbose mode...

    hessian序列化规范

    3. 自定义Hessian序列化器以支持自定义类型的序列化和反序列化。 总结,Hessian序列化规范是分布式系统中的重要工具,理解其原理和使用方法,能够帮助我们构建高效、可靠的跨网络通信方案。然而,任何工具都有其...

    浅谈Java序列化和hessian序列化的差异

    Java序列化和Hessian序列化的差异 Java序列化和Hessian序列化是两种常用的序列化机制,它们都可以将对象转换为字节流,以便在网络上传输。但是,两者之间有着很大的差异,今天我们就来比较一下它们的实现机制和特点...

    基于混合分析的Java反序列化利用链挖掘方法.docx

    根据反序列化机制的不同,Java反序列化器可以大致分为两类:基于bean的反序列化器(如Fastjson和Jackson,通过调用setter方法设置属性值)和基于field的反序列化器(如JND、XStream、Hessian,通过反射直接对属性...

    hessian java +c# 数据测试

    总之,Hessian为Java和C#之间的跨平台通信提供了便利,通过高效的数据序列化和反序列化,实现了不同语言间的无缝对接。在实际开发中,开发者需要理解Hessian的工作原理,熟练掌握两端的库使用,确保数据正确传输和...

    removal RCE、Hessian 反序列化、Yaml反序列化、密码解密、部分常用敏感路径(漏洞更新截止2024.9.12)

    removal RCE、Hessian 反序列化、Yaml反序列化、密码解密、部分常用敏感路径(漏洞更新截止2024.9.12)

    hessian小例(java)

    2. 通过代理调用服务端的方法,Hessian会自动处理网络通信和反序列化结果。 使用Apache Commons Hessian库,我们可以很容易地创建Hessian代理,如下所示: ```java HessianProxyFactory factory = new ...

    Hessian 2.0序列化协议规范.docx

    在分布式计算和网络通信中,数据的序列化和反序列化是至关重要的环节。Hessian 2.0是一种高效的二进制序列化协议,它旨在减少网络传输的数据量,提高数据交换的效率。本文将详细介绍Hessian 2.0的序列化规范,包括其...

    hessian php与java通讯demo源码

    Hessian库会自动处理序列化和反序列化,使得客户端可以像调用本地方法一样调用远程服务。 3. **PHP客户端** (`php-client.rar`): - PHP客户端同样利用Hessian协议来调用Java服务器上的服务。 - PHP中,你可以...

    HessianCSharp_Test.rar_Hessian java

    5. **序列化与反序列化**: Hessian协议涉及对象的序列化(将对象转换为字节流)和反序列化(将字节流还原为对象)。C#和Java的Hessian库都提供了相应的实现。 6. **安全性与性能**: 使用Hessian可以减少网络开销,...

    基于java序列化的Netty编解码技术

    2. **性能问题**:Java序列化相比其他序列化库(如Protocol Buffers、Apache Avro、JSON等)来说,效率较低,因为它产生的字节流较大,且序列化和反序列化过程较慢。在对性能有较高要求的场景下,建议考虑使用更高效...

    Java Hessian小试(转)

    标签中的"源码"可能意味着要深入理解Hessian的内部工作原理,这包括了解其序列化和反序列化的算法,以及如何处理不同类型的Java对象。而"工具"可能是指Hessian可以作为一个工具来简化Java应用之间的通信,提供了一种...

    dubbo-exp:Dubbo反序列化一键快速攻击测试工具,支持dubbo协议和http协议,支持hessian反序列化和java原生反序列化

    a --args gadget入参,多个参数使用多次该命令传入,例-a -a Calc-p --protocol [dubbo|http] 通讯协议名称,默认缺省dubbo-s --serialization [hessian|java] 序列化类型,默认缺省hessian-t --target 目标,例:...

    java+hessian 远程调用

    - **支持基本类型和复杂对象**:不仅可以处理基本数据类型,还可以序列化和反序列化Java对象,包括集合和自定义类。 4. **使用说明文档**: - **配置详解**:文档通常会包含如何配置Hessian服务端和客户端的详细...

Global site tag (gtag.js) - Google Analytics