2023-09-27
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/series/article/1661554920

Java 序列化之深入理解 Java 序列化和反序列化 一文中,大明哥说到,Java 的原生序列化存在三个缺点:

  1. 无法跨语言
  2. 序列化后的字节流太大
  3. 序列化时间太长

这三个缺陷是我们在开发过程中是无法容忍的,所以我们必须来寻找一个可替代的方案。Hessian 就是一个不错的选择。

什么是 Hessian

Hessian 是一种动态类型、二进制序列化和 Web 服务协议,专为面向对象的传输而设计。

与 Java 原生序列化类似,Hessian 也是采用二进制协议,不过它比 Java 原生序列化的性能更加高,序列化的字节数也更小。

与 Java 原生序列化相比,Hessian 具有如下几个特点(http://hessian.caucho.com/doc/hessian-serialization.html):

  • 它必须自我描述序列化类型,即不需要外部模式或接口定义
  • 它必须是独立于语言的,包括支持脚本语言
  • 它必须是可以通过单一方式进行读写
  • 它必须尽可能紧凑
  • 它必须简单,这样才能有效地测试和实现
  • 必须尽可能地快
  • 它必须支持Unicode字符串
  • 它必须支持8位二进制数据,而不需要转义或使用附件
  • 它必须支持加密、压缩、签名和事务上下文信封

目前 Hessian 已经到 2.0 版本了,相比 1.0 版本,Hessian 2.0 版本增加了压缩编码,其序列化二进制流大小事 Java 序列化的 50%,序列化耗时是 Java 序列化的 30%,反序列化耗时是 Java 序列化的20%。

Hessian 语法

Hessian 序列化的字节数更加小,与它的实现序列化的语法密不可分,如下:

           # starting production
top        ::= value

           # 8-bit binary data split into 64k chunks
binary     ::= x41 b1 b0 <binary-data> binary # non-final chunk
           ::= 'B' b1 b0 <binary-data>        # final chunk
           ::= [x20-x2f] <binary-data>        # binary data of
                                                 #  length 0-15
           ::= [x34-x37] <binary-data>        # binary data of
                                                 #  length 0-1023

           # boolean true/false
boolean    ::= 'T'
           ::= 'F'

           # definition for an object (compact map)
class-def  ::= 'C' string int string*

           # time in UTC encoded as 64-bit long milliseconds since
           #  epoch
date       ::= x4a b7 b6 b5 b4 b3 b2 b1 b0
           ::= x4b b3 b2 b1 b0       # minutes since epoch

           # 64-bit IEEE double
double     ::= 'D' b7 b6 b5 b4 b3 b2 b1 b0
           ::= x5b                   # 0.0
           ::= x5c                   # 1.0
           ::= x5d b0                # byte cast to double
                                     #  (-128.0 to 127.0)
           ::= x5e b1 b0             # short cast to double
           ::= x5f b3 b2 b1 b0       # 32-bit float cast to double

           # 32-bit signed integer
int        ::= 'I' b3 b2 b1 b0
           ::= [x80-xbf]             # -x10 to x3f
           ::= [xc0-xcf] b0          # -x800 to x7ff
           ::= [xd0-xd7] b1 b0       # -x40000 to x3ffff

           # list/vector
list       ::= x55 type value* 'Z'   # variable-length list
     ::= 'V' type int value*   # fixed-length list
           ::= x57 value* 'Z'        # variable-length untyped list
           ::= x58 int value*        # fixed-length untyped list
     ::= [x70-77] type value*  # fixed-length typed list
     ::= [x78-7f] value*       # fixed-length untyped list

           # 64-bit signed long integer
long       ::= 'L' b7 b6 b5 b4 b3 b2 b1 b0
           ::= [xd8-xef]             # -x08 to x0f
           ::= [xf0-xff] b0          # -x800 to x7ff
           ::= [x38-x3f] b1 b0       # -x40000 to x3ffff
           ::= x59 b3 b2 b1 b0       # 32-bit integer cast to long

           # map/object
map        ::= 'M' type (value value)* 'Z'  # key, value map pairs
     ::= 'H' (value value)* 'Z'       # untyped key, value

           # null value
null       ::= 'N'

           # Object instance
object     ::= 'O' int value*
     ::= [x60-x6f] value*

           # value reference (e.g. circular trees and graphs)
ref        ::= x51 int            # reference to nth map/list/object

           # UTF-8 encoded character string split into 64k chunks
string     ::= x52 b1 b0 <utf8-data> string  # non-final chunk
           ::= 'S' b1 b0 <utf8-data>         # string of length
                                             #  0-65535
           ::= [x00-x1f] <utf8-data>         # string of length
                                             #  0-31
           ::= [x30-x34] <utf8-data>         # string of length
                                             #  0-1023

           # map/list types for OO languages
type       ::= string                        # type name
           ::= int                           # type reference

           # main production
value      ::= null
           ::= binary
           ::= boolean
           ::= class-def value
           ::= date
           ::= double
           ::= int
           ::= list
           ::= long
           ::= map
           ::= object
           ::= ref
           ::= string

举几个例子:

  • boolean

Boolean 的语法如下:

boolean ::= T
        ::= F

T 代表 true,F 代表 false。

用如下代码演示下:

hessianOutput.writeObject(true);
System.out.println("序列化:" + Arrays.toString(bos.toByteArray()));

// --运行结果
序列化:[84]

84 的 ASCII 为 T。

  • int

Integer 语法如下:

int ::= 'I' b3 b2 b1 b0
    ::= [x80-xbf]
    ::= [xc0-xcf] b0
    ::= [xd0-xd7] b1 b0

一个 32 位有符合的整数,它由八位数x49('I')表示,后面是整数的4个八位数,以高位优先(big-endian)顺序排列。其中 value = (b3 << 24) + (b2 << 16) + (b1 << 8) + b0

用如下代码演示下:

hessianOutput.writeObject(1024);
System.out.println("序列化:" + Arrays.toString(bos.toByteArray()));

// --运行结果
序列化 :[73, 0, 0, 4, 0]

73 的 ASCII 为 I,4 << 8 就等于 1024。

  • 对象

对象的语法如下:

class-def  ::= 'C' string int string*

object     ::= 'O' int value*
           ::= [x60-x6f] value*

它的表示也很简单:

class Car {
  String color;
  String model;
}

out.writeObject(new Car("red", "corvette"));
out.writeObject(new Car("green", "civic"));

---

C                        # 类的描述信息
  x0b example.Car        # type is example.Car
  x92                    # two fields
  x05 color              # color field name      // 属性名
  x05 model              # model field name      // 属性名

O                        # object def (long form)
  x90                    # object definition #0
  x03 red                # color field value     // 属性值
  x08 corvette           # model field value     // 属性值

x60                      # object def #0 (short form)
  x05 green              # color field value     // 属性值
  x05 civic              # model field value     // 属性值

更多详情请参考官方文档:http://hessian.caucho.com/doc/hessian-serialization.html

Hessian 的使用

  • 先引入包,大明哥在写这篇文章的时候,最新版本为,4.0.65
<dependency>
  <groupId>com.caucho</groupId>
  <artifactId>hessian</artifactId>
  <version>4.0.65</version>
</dependency>
  • 序列化对象实现 Serializable 接口
public class Student implements Serializable {

    private String name;

    private int age;

    private Integer height;

    private transient String gender;
}
  • Hessian 序列化
public class Hessian02Test {
    public static void main(String[] args) throws IOException {
        Student student1 = new Student("张三",18,180,"男");

        // 序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        HessianOutput hessianOutput = new HessianOutput(bos);
        hessianOutput.writeObject(student1);
        System.out.println("序列化内容:" + Arrays.toString(bos.toByteArray()));

        //反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        HessianInput hessianInput = new HessianInput(bis);
        Student student2 = (Student) hessianInput.readObject();
        System.out.println("反序列化内容:" + student2);
    }
}
  • 执行结果
序列化内容:[77, 116, 0, 48, 99, 111, 109, 46, 115, 105, 107, 101, 46, 106, 97, 118, 97, 99, 111, 114, 101, 46, 115, 101, 114, 105, 97, 108, 105, 122, 101, 114, 46, 104, 101, 115, 115, 105, 97, 110, 46, 100, 116, 111, 46, 83, 116, 117, 100, 101, 110, 116, 83, 0, 4, 110, 97, 109, 101, 83, 0, 2, -27, -68, -96, -28, -72, -119, 83, 0, 3, 97, 103, 101, 73, 0, 0, 0, 18, 83, 0, 6, 104, 101, 105, 103, 104, 116, 73, 0, 0, 0, -76, 122]
反序列化内容:Student(name=张三, age=18, height=180, gender=null)

从执行结果来看,反序列化完美将 Student 对象还原了。

这里需要注意的是,无论是 Java 原生序列化还是 Hessian 序列化,对象都必须实现 Serializable 接口,否则会报 must implement java.io.Serializable 异常。

与 Java 原生序列化一样,用 transient 标识的属性是不需要序列化和反序列化的。

还有一点,若对象经过 Hessian 序列化后,在不加 serialVersionUID 的情况下,我们改变该对象的属性都不会引起 Hessian 反序列化失败,所以 Hessian 序列化对象不需要再使用 serialVersionUID 来标注对象版本了。

我们再看一个稍微复杂的对象。

public class SubStudent extends Student{
    private Integer height;

    private String subValue;
}

SubStudent 继承 Student,并且拥有同一个属性 height。我们对 SubStudent 进行序列化和反序列化。

public class Hessian03Test {
    public static void main(String[] args) throws IOException {
        SubStudent subStudent = new SubStudent();
        subStudent.setName("李四");
        subStudent.setHeight(185);
        subStudent.setSubValue("lisi");
        subStudent.setGender("男");

        // 序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        HessianOutput hessianOutput = new HessianOutput(bos);
        hessianOutput.writeObject(subStudent);
    
        //反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        HessianInput hessianInput = new HessianInput(bis);
        SubStudent student2 = (SubStudent) hessianInput.readObject();
        System.out.println("反序列化内容:" + student2);
    }
}

运行结果

反序列化内容:SubStudent(height=null, subValue=lisi)

从反序列化结果我们看到 height 属性值为 null,但是我们明明对其值设置为 185 ,在反序列化时怎么会是 0 呢?这其实是与 Hessian 对复杂对象的处理逻辑相关:Hessian 会把复杂对象的所有属性存储在一个 Map 中进行序列化。所以在父类、子类中存在同名成员变量的情况下,Hessian 序列化时,先序列化子类,然后序列化父类。因此,反序列化结果会导致子类同名成员变量被父类的值覆盖。

Hessian VS Java 原生

在开头大明哥说到,Java 原生序列化性能太差,Hessian 可以作为一个好的替代者,相比 Java 原生序列化它的速度更快,序列化后的字节流更小,且跨语言,兼容性更好。咱们来验证下。

public class Hessian04Test {
    public static void main(String[] args) throws IOException {
        Student student = new Student("张三",18,180,"男");

        // Hessian 序列化
        ByteArrayOutputStream bos1 = new ByteArrayOutputStream();
        HessianOutput hessianOutput = new HessianOutput(bos1);
        hessianOutput.writeObject(student);
        System.out.println("Hessian 序列化字节流大小:" + bos1.toByteArray().length);
        
        // Java 原生序列化
        ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(bos2);
        outputStream.writeObject(student);
        System.out.println("Java 原生序列化字节流大小:" + bos2.toByteArray().length);
    }
}

结果

Hessian 序列化字节流大小:94
Java 原生序列化字节流大小:224

从执行结果来看,Hessian 序列化后的字节流确实是比 Java 原生序列化后的字节流要小的多。

阅读全文