Java拾遗:004 - JDK、Hadoop、Hessian序列化

JDK序列化

在分布式架构中,序列化是分布式的基础构成之一,我们需要把单台设备上的数据通过序列化(编码、压缩)后通过网络传输给网络中的其它设备,从而实现信息交换。 JDK对Java中的对象序列化提供了支持,原生的Java序列化要求序列化的类必须实现java.io.Serializable接口,该接口是一个标记接口(不包含任何方法)。 下面定义一个POJO类(仅用于演示,没有任何实际意义),它将被序列化和反序列化

public class Data implements Serializable {

    private Integer a;
    private Long b;
    private Float c;
    private Double d;
    private Boolean e;
    private Character f;
    private Byte g;
    private Short h;

    private int a0;
    private long b0;
    private float c0;
    private double d0;
    private boolean e0;
    private char f0;
    private byte g0;
    private short h0;

    private String i;
    private Date j;

    // getter / setter ...

}

使用Java序列化代码非常简单,我们需要构造一个ObjectOutputStream,该类接收一个输出流(用于输出序列化后的对象信息),这里为了方便演示,我用了ByteArrayOutputStream,将对象序列为一个字节数组

        // 执行序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream output = new ObjectOutputStream(baos);
        output.writeObject(data);
        baos.close();
        output.close();
        byte[] buf = baos.toByteArray();

        assertEquals(947, buf.length);

代码里省略了构造测试对象的代码(属性有点多),演示了序列化的过程,除了构造输出流和关闭注流代码,实际序列化代码只有一句:output.writeObject(data);,所以Java的序列化代码实现还是比较简单的。 测试代码中包含一个关于序列化后数据大小的测试,有947个字节,后面其它的序列化会与之形成对比。 当网络一端接收到这个字节数组(数据流)后,会执行反序列化,得到序列化前的数据,下面实现反序列化

        // 执行反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(buf);
        ObjectInputStream input = new ObjectInputStream(bais);
        Data data2 = (Data) input.readObject();
        bais.close();
        input.close();

        assertFalse(data == data2);
        assertEquals(data.getA(), data2.getA());
        assertEquals(data.getI(), data2.getI());
        assertEquals(data.getJ(), data2.getJ());

代码里实现了将字节数组反序列化为一个Data对象,测试语句证明了反序列化对象与原对象不是一个对象(之前讲对象克隆时提到过可以使用序列化、反序列化来实现,这里证明了这一点),但其属性都是一致的,也就是说我们正确得到了序列化前的数据。

使用Serializable实现序列化时,如果某一个或某几个字段不需要序列化,可以使用transient关键字修改字段即可

private transient String password;

JDK还提供另一种序列化方式,通过Externalizable接口来实现

public class Data3 implements Externalizable {

    private Integer id;
    private String name;
    private Date birthday;

    @Override
    public void writeExternal(ObjectOutput output) throws IOException {
        output.writeInt(this.id);
        output.writeUTF(this.name);
        output.writeObject(this.birthday);
    }

    @Override
    public void readExternal(ObjectInput input) throws IOException, ClassNotFoundException {
        this.id = input.readInt();
        this.name = input.readUTF();
        this.birthday = (Date) input.readObject();
    }
  
    // getter / setter ...

}

这里不解释,其与Hadoop提供的序列化机制几乎相同,所以请参考Hadoop的序列化。

Hadoop序列化

在Hadoop中由于经常需要向DataNode复制数据,Hadoop设计了一套特殊的序列化代码(实际仍是完全由JDK实现,其实现方式与Externalizable机制基本类似)。

public class Data2 {

    private Integer a;
    private Long b;
    private Float c;
    private Double d;
    private Boolean e;
    private Character f;
    private Byte g;
    private Short h;

    private int a0;
    private long b0;
    private float c0;
    private double d0;
    private boolean e0;
    private char f0;
    private byte g0;
    private short h0;

    private String i;
    private Date j;

    public byte[] serialize() throws IOException {
        return Data2.serialize(this);
    }

    /**
     * 序列化当前对象
     *
     * @return
     */
    public static final byte[] serialize(Data2 data) throws IOException {
        assert data != null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutput output = new DataOutputStream(baos);

        // 序列化的数据参考 JdkSerializeTest 中的Data对象
        // 序列化、反序列化的过程都是一个字段一个字段的实现,虽然繁琐,但序列化后的大小和性能都比JDK原生序列化API强很多
        output.writeInt(data.getA());
        output.writeInt(data.getA0());
        output.writeLong(data.getB());
        output.writeLong(data.getB0());
        output.writeFloat(data.getC());
        output.writeFloat(data.getC0());
        output.writeDouble(data.getD());
        output.writeDouble(data.getD0());
        output.writeBoolean(data.getE());
        output.writeBoolean(data.isE0());
        output.writeChar(data.getF());
        output.writeChar(data.getF0());
        output.writeByte(data.getG());
        output.writeByte(data.getG0());
        output.writeShort(data.getH());
        output.writeShort(data.getH0());
        writeString(output, data.getI());
        // 序列化日期时使用时间戳表示
        output.writeLong(data.getJ().getTime());

        return baos.toByteArray();
    }

    /**
     * 反序列化 Data2 对象
     *
     * @param buf
     * @return
     */
    public static final Data2 deserialize(byte[] buf) throws IOException {
        // 执行反序列化,注意读取的顺序与写入的顺序要一致
        ByteArrayInputStream bais = new ByteArrayInputStream(buf);
        DataInput input = new DataInputStream(bais);
        Data2 data = new Data2();
        data.setA(input.readInt());
        data.setA0(input.readInt());
        data.setB(input.readLong());
        data.setB0(input.readLong());
        data.setC(input.readFloat());
        data.setC0(input.readFloat());
        data.setD(input.readDouble());
        data.setD0(input.readDouble());
        data.setE(input.readBoolean());
        data.setE0(input.readBoolean());
        data.setF(input.readChar());
        data.setF0(input.readChar());
        data.setG(input.readByte());
        data.setG0(input.readByte());
        data.setH(input.readShort());
        data.setH0(input.readShort());
        data.setI(readString(input));
        data.setJ(new Date(input.readLong()));
        return data;
    }

    /**
     * 向 DataOutput 写入字符类型稍微复杂一些
     *
     * @param out
     * @param s
     * @throws IOException
     * @see org.apache.hadoop.io.WritableUtils#writeString(DataOutput, String)
     */
    private static final void writeString(DataOutput out, String s) throws IOException {
        if (s != null) {
            byte[] buffer = s.getBytes("UTF-8");
            int len = buffer.length;
            // 先写入字符串长度
            out.writeInt(len);
            // 再写入字符串内容(字节数组)
            out.write(buffer, 0, len);
        } else {
            out.writeInt(-1);
        }
    }

    /**
     * 与 writeString(DataOutput, String) 方法相反,用于读取字符串类型数据
     *
     * @param in
     * @return
     * @throws IOException
     * @see #writeString(DataOutput, String)
     */
    private static final String readString(DataInput in) throws IOException {
        int length = in.readInt();
        if (length == -1) return null;
        byte[] buffer = new byte[length];
        in.readFully(buffer);      // could/should use readFully(buffer,0,length)?
        return new String(buffer, "UTF-8");
    }

    // getter / setter ...

}

代码里实现了序列化和反序列化逻辑,Data2是一个POJO类,与上例中的Data类属性完全一样,只是多了序列化和反序列化方法(这两个方法写在POJO类中的原因是其序列化、反序列化有顺序要求,放在外面会难以控制)。 从实现代码中发现实际序列化、反序列化是由DataOutputDataInput两个接口及其实现类来实现的,这些类完全由JDK提供,并不依赖任何第三方的库,由于手动控制了序列化、反序列化,所以其性能和序列化后的大小控制都非常好

        // 序列化的数据参考 JdkSerializeTest 中的Data对象
        // 序列化、反序列化的过程都是一个字段一个字段的实现,虽然繁琐,但序列化后的大小和性能都比JDK原生序列化API强很多
        byte[] buf = data.serialize();

        // 测试序列化大小:JDK序列化后是947,这里只有204
        assertEquals(204, buf.length);

        // 执行反序列化,注意读取的顺序与写入的顺序要一致
        Data2 data2 = Data2.deserialize(buf);
        assertFalse(data == data2);
        assertEquals(data.getA(), data2.getA());
        assertEquals(data.getA0(), data2.getA0());
        // 由于浮点数在计算时会有误差,这里第三个参数用于控制误差
        assertEquals(data.getC(), data2.getC(), 0.0);
        assertEquals(data.getC0(), data2.getC0(), 0.0);
        assertEquals(data.getE(), data2.getE());
        assertEquals(data.isE0(), data2.isE0());
        assertEquals(data.getF(), data2.getF());
        assertEquals(data.getF0(), data2.getF0());
        assertEquals(data.getG(), data2.getG());
        assertEquals(data.getG0(), data2.getG0());
        assertEquals(data.getH(), data2.getH());
        assertEquals(data.getH0(), data2.getH0());
        assertEquals(data.getI(), data2.getI());
        assertEquals(data.getJ(), data2.getJ());

可以看出同样对象序列化后只有204个字节,约为之前的1/4,而且序列化的性能也调出很多,后面会给出简单对比。

Hessian序列化

在一些开源框架中(如:Dubbo),也使用Hessian库(这里指的是Hessian2)来实现序列化。

        // 执行序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(baos);
        hessian2Output.writeObject(data);
        hessian2Output.close();
        // 获取字节数组前,必须先关闭Hessian2Output,否则取得字节数组长度为0(原因暂不清楚)
        byte[] buf = baos.toByteArray();
        baos.close();
        // 测试断言
        Assert.assertNotNull(buf);
        Assert.assertEquals(373, buf.length);
        System.out.println(new String(buf));

        // 执行反序列化
        Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream(buf));
        Data data2 = (Data) hessian2Input.readObject();
        hessian2Input.close();
        // 测试断言
        assertFalse(data == data2);
        assertEquals(data.getA(), data2.getA());
        assertEquals(data.getI(), data2.getI());
        assertEquals(data.getJ(), data2.getJ());

相对JDK序列化和Hadoop序列化,其序列化后的数据大小居中,实际上性能也是居中的。但该库的优势在于,其跨语言的特性,也就是说可以向非Java语言的程序发送序列化数据,并能由对应语言的Hessian库实现反序列化。

性能比较

下面使用10,000次循环序列化、反序列化(单线程)来测试三种序列化方式的耗时(该测试仅供参考,场景有限,并不能真的说明三种方式优劣程度)。

  • jdk
    @Test
    public void performance() throws IOException, ClassNotFoundException {

        final int loop = 10_000;

        long time = System.currentTimeMillis();
        for (int i = 0; i < loop; i++) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream output = new ObjectOutputStream(baos);
            output.writeObject(data);
            baos.close();
            output.close();
            byte[] buf = baos.toByteArray();

            // 执行反序列化
            ByteArrayInputStream bais = new ByteArrayInputStream(buf);
            ObjectInputStream input = new ObjectInputStream(bais);
            input.readObject();
            bais.close();
            input.close();
        }

        // loop = 10,000 -> 程序执行耗时:1037 毫秒!
        System.out.println(String.format("程序执行耗时:%d 毫秒!", System.currentTimeMillis() - time));

    }
  • hadoop
    @Test
    public void performance() throws IOException {

        final int loop = 10_000;

        long time = System.currentTimeMillis();
        for (int i = 0; i < loop; i++) {
            // 执行序列化
            byte[] buf = data.serialize();

            // 执行反序列化
            Data2.deserialize(buf);
        }

        // loop = 10,000 -> 程序执行耗时:75 毫秒!
        System.out.println(String.format("程序执行耗时:%d 毫秒!", System.currentTimeMillis() - time));

    }
  • hessian
    public void performance() throws IOException {

        final int loop = 10_000;

        long time = System.currentTimeMillis();
        for (int i = 0; i < loop; i++) {
            // 执行序列化
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            Hessian2Output hessian2Output = new Hessian2Output(baos);
            hessian2Output.writeObject(data);
            hessian2Output.close();
            byte[] buf = baos.toByteArray();

            // 执行反序列化
            Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream(buf));
            hessian2Input.readObject();
            hessian2Input.close();
        }

        // loop = 10,000 -> 程序执行耗时:300 毫秒!
        System.out.println(String.format("程序执行耗时:%d 毫秒!", System.currentTimeMillis() - time));

    }

结论(非权威,有兴趣的自行研究吧) | 循环次数 | jdk (947bytes) | hadoop (204bytes) | hessian (373bytes) | | - | - | - | - | | 10,000 | 1,037ms | 75ms | 300ms |

其它序列化

实际应用中,序列化可选方案很多,像Hadoop还可以用Avro、Protobuf来进行序列化,下面列出一些常用的序列化库:

  • JSON,是一种规范,对应的库非常多,比如:Jackson、Fastjson等
  • Avro,Hadoop提供的一套跨平台序列化方案
  • Protobuf,Google提供的一套跨平台序列化方案
  • Thrift,Apache提供的一套跨平台序列化方案
  • Kryo
  • FST
  • Dubbo 后面三个都只能用于Java,其中Dubbo是Dubbo框架提供的序列化方案(经查阅源码,2.6.x及以后的版本中不再提供)

结语

序列化在分布式架构中(比较偏底层)是很重要的一环,好的序列化方案可以节省大量的带宽,并且提升程序处理速度。

后面列出的一些序列化方案本文未详细解释,这里先留个坑,后面将专门撰文来讲解。

源码仓库:

转载于:https://my.oschina.net/zhanglikun/blog/1922868

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值