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

JDK序列化

在分佈式架構中,序列化是分佈式的基礎構成之一,咱們須要把單臺設備上的數據經過序列化(編碼、壓縮)後經過網絡傳輸給網絡中的其它設備,從而實現信息交換。 JDK對Java中的對象序列化提供了支持,原生的Java序列化要求序列化的類必須實現java.io.Serializable接口,該接口是一個標記接口(不包含任何方法)。 下面定義一個POJO類(僅用於演示,沒有任何實際意義),它將被序列化和反序列化java

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,將對象序列爲一個字節數組git

// 執行序列化
        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個字節,後面其它的序列化會與之造成對比。 當網絡一端接收到這個字節數組(數據流)後,會執行反序列化,獲得序列化前的數據,下面實現反序列化github

// 執行反序列化
        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對象,測試語句證實了反序列化對象與原對象不是一個對象(以前講對象克隆時提到過可使用序列化、反序列化來實現,這裏證實了這一點),但其屬性都是一致的,也就是說咱們正確獲得了序列化前的數據。apache

使用Serializable實現序列化時,若是某一個或某幾個字段不須要序列化,可使用transient關鍵字修改字段便可json

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及之後的版本中再也不提供)

結語

序列化在分佈式架構中(比較偏底層)是很重要的一環,好的序列化方案能夠節省大量的帶寬,而且提高程序處理速度。

後面列出的一些序列化方案本文未詳細解釋,這裏先留個坑,後面將專門撰文來說解。

源碼倉庫:

相關文章
相關標籤/搜索