美文网首页Java拾遗
Java拾遗:004 - JDK、Hadoop、Hessian序

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

作者: ed72fd6aaa3c | 来源:发表于2018-08-05 19:24 被阅读27次

    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及以后的版本中不再提供)

    结语

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

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

    源码仓库:

    相关文章

      网友评论

        本文标题:Java拾遗:004 - JDK、Hadoop、Hessian序

        本文链接:https://www.haomeiwen.com/subject/frzbvftx.html