分布式架构基础 二、序列化和反序列化技术

一、Java对象如何进行远程传输

1. 使用socket进行简单通信传输

我们知道Java中可以使用Socket进行跨JVM的字节流传输,简单demo如下。
Server:服务端接收到客户端的连接请求,并发送一条信息至客户端


import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) {

        try {
            ServerSocket serverSocket = new ServerSocket(8081);

            Socket socket = serverSocket.accept();

            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bw.write("Hi this's server" + "\n");
            bw.flush();

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

Client: 客户端收到消息并打印

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

public class Client {

    public static void main(String[] args) {

        try {
            Socket socket = new Socket("127.0.0.1", 8081);

            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String msg = br.readLine();
            System.out.println(msg);

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

2. 使用socket进行Java对象传输

我们可以使用Socket进行远程通信,那么针对Java对象又该怎么传输呢?传输至另一台服务器又该怎么解析呢?
Java中提供了java.io.Serializable接口来帮助我们将对象进行序列化与反序列化,并根据对象内生产唯一的序列化标识serialVersionUID进行辨认。

User: 创建测试对象,实现Serializable接口,并让IDE自动生产serialVersionUID
tips: IntelliJ Idea 设置 ·Serializable class without serialVersionUID· 来生产序列化ID

import java.io.Serializable;

public class User implements Serializable {

    private static final long serialVersionUID = -8042817895003209941L;

    private String name;

    private int age;

    private String location;

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

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

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

}

Server: 服务端在前面demo基础上进行改造,使用对象输出流ObjectOutputStream来写出对象


import java.io.BufferedWriter;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) {

        try {
            ServerSocket serverSocket = new ServerSocket(8081);

            Socket socket = serverSocket.accept();

//            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//            bw.write("Hi this's server" + "\n");
//            bw.flush();

            User user = new User("shen", 18, "HK");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeObject(user);

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

Client:客户端,使用对象输入流ObjectInputStream来写入对象

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.net.Socket;

public class Client {

    public static void main(String[] args) {

        try {
            Socket socket = new Socket("127.0.0.1", 8081);

//            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//            String msg = br.readLine();
//            System.out.println(msg);

            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            User user = (User) objectInputStream.readObject();
            System.out.println(user);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

}

··· 输出结果
User{name='shen', age=18, location='HK'}

3. 序列化的意义

通过上例我们了解到为User类添加Serializable接口,可以将对象进行传输,其实也可以试一下去掉Serializable接口或者在客户端接收到字节流进行反序列化时更改对象的serialVersionUID,是抛出java.io.NotSerializableExceptionInvalidCastException异常的。
Java允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才能存在,这些对象的生命周期不回比JVM生产周期更长。那如果我们要跨JVM进行对象传输或者在JVM停止运行前保存(持久化)指定的对象,并在将来重新读取被保存的对象,这时就用到了序列化技术,这也是序列化的意义。
简单来说:
序列化是把对象的状态信息转化为可存储或传输的形式过程,也就是把对象转化为字节序列的过程称为对象的序列化;
反序列化是序列化的逆向过程,把字节数组反序列化为对象,把字节序列恢复为对象的过程成为对象的反序列化;

二、序列化的高阶认识

1. 序列化ID serialVersionUID

Java的序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时, JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是 InvalidCastException 。

tips:
serialVersionUID 有两种显示的生成方式:
一是默认的1L ,比如 private static final long serialVersionUID = 1L;
二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段;
当实现java.io.Serializable接口的类没有显式地定义一个serialVersionUID变量时候,Java序列化机制会根据编译的Class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,如果Class文件类名,方法明等没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID也不会变化的。

2. transient关键字

在使用序列化进行服务间通信的时候,某些时候我们传输的对象的一些属性可能不想被另一方看到,此时我们可以将这些变量用transient修饰。
Transient关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后, transient 变量的值被设为初始值,如 int 型的是0,对象型的是null。

//location 被 transient 修饰,不会被序列化
private transient String location;

3. 绕开transient机制的办法

虽然 location 被 transient 修饰,但是通过writeObjectreadObject两个方法依然能够使得 location 字段正确被序列化和反序列化


import java.io.IOException;
import java.io.Serializable;

public class User implements Serializable {

    private static final long serialVersionUID = 8312123884146537715L;

    private String name;

    private int age;

    private transient String location;

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

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

    private void writeObject(java.io.ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeObject(location);
    }

    private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        location = (String)s.readObject();
    }

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

}

可以使用上面的socket案例进行通信测试。
至于writeObjectreadObject是在什么地方调用的,可以从源码里来跟踪下:

writeObject入口: s.defaultWriteObject();  
-> java.io.ObjectOutputStream#defaultWriteObject  -> java.io.ObjectOutputStream#defaultWriteFields -> java.io.ObjectOutputStream#writeObject0
-> java.io.ObjectOutputStream#writeOrdinaryObject -> java.io.ObjectOutputStream#writeSerialData -> java.io.ObjectStreamClass#invokeWriteObject

java.io.ObjectStreamClass {
	
    /** class-defined writeObject method, or null if none */
    private Method writeObjectMethod;
    /** class-defined readObject method, or null if none */
    private Method readObjectMethod;
	...
	...
    void invokeWriteObject(Object obj, ObjectOutputStream out)
        throws IOException, UnsupportedOperationException
    {
        requireInitialized();
        if (writeObjectMethod != null) {
            try {
		// 可见,writeObject是通过反射来调用的,达到重新读出的目的
                writeObjectMethod.invoke(obj, new Object[]{ out });
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof IOException) {
                    throw (IOException) th;
                } else {
                    throwMiscException(th);
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

}

readObject逻辑和write类似,底层也是基于反射调用
readObject入口:s.defaultReadObject();
-> java.io.ObjectInputStream#defaultReadObject -> java.io.ObjectInputStream#defaultReadFields -> java.io.ObjectInputStream#readObject0
-> java.io.ObjectInputStream#readOrdinaryObject -> java.io.ObjectInputStream#readSerialData -> java.io.ObjectStreamClass#invokeReadObject

java.io.ObjectStreamClass{


    /** class-defined writeObject method, or null if none */
    private Method writeObjectMethod;
    /** class-defined readObject method, or null if none */
    private Method readObjectMethod;
	...
	...
    void invokeReadObject(Object obj, ObjectInputStream in)
        throws ClassNotFoundException, IOException,
               UnsupportedOperationException
    {
        requireInitialized();
        if (readObjectMethod != null) {
            try {
		// 反射调用,达到重新读出的作用
                readObjectMethod.invoke(obj, new Object[]{ in });
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof ClassNotFoundException) {
                    throw (ClassNotFoundException) th;
                } else if (th instanceof IOException) {
                    throw (IOException) th;
                } else {
                    throwMiscException(th);
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }
	...
	...
}

Java序列化的一些简单总结

  1. Java 序列化只是针对对象的状态进行保存,至于对象中的方法,序列化不关心
  2. 当一个父类实现了序列化,那么子类会自动实现序列化,不需要显示实现序列化接口
  3. 当一个对象的实例变量引用了其他对象,序列化这个对象的时候会自动把引用的对象也进行序列化(实现深度克隆)
  4. 当某个字段被申明为 transient 后,默认的序列化机制会忽略这个字段
  5. 被申明为 transient 的字段,如果需要序列化,可以添加两个私有方法: writeObject 和 readObject

三、分布式架构下常见的序列化技术

1. 了解序列化的发展

随着分布式架构、微服务架构的普及。服务与服务之间的通信成了最基本的需求。这个时候,我们不仅需要考虑通信的性能,也需要考虑到语言多元化问题所以,对于序列化来说,如何去提升序列化性能以及解决跨语言问题,就成了一个重点考虑的问题。
由于Java 本身提供的序列化机制存在两个问题
  1. 序列化的数据比较大,传输效率低
  2. 其他语言无法识别和对接
以至于在后来的很长一段时间,基于XML格式编码的对象序列化机制成为了主流,一方面解决了多语言兼容问题,另一方面比二进制的序列化方式更容易理解。以至于基于 XML 的 SOAP协议及对应的 WebService 框架在很长一段时间内成为各个主流开发语言的必备的技术。再到后来,基于 JSON 的简单文本格式编码的 HTTP REST 接口又基本上取代了复杂的 WebService 接口,成为分布式架构中远程通信的首要选择。但是 JSON 序列化存储占用的空间大、性能低等问题,同时移动客户端应用需要更高效的传输数据来提升用户体验。在这种情况下与语言无关并且高效的二进制编码协议就成为了大家追求的热点技术之一。首先诞生的一个开源的二进制序列化框架 MessagePack 。它比 google 的 Protocol Buffers 出现得还要早。

XML Web service -> Json Rest -> Protocol Buffers

2. 各种序列化技术概览

本章以User对象的序列化为例,分别使用java、xml、json、protobuffer等技术进行序列化来比较大小等性能消耗。
公用测试对象User:


import java.io.Serializable;

public class User implements Serializable {

    private static final long serialVersionUID = 8312123884146537715L;

    private String name;

    private int age;

    private String location;

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

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

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

}

公用接口: ISerializer

public interface ISerializer {

    <T> byte[] serialize(T obj);

    <T> T deserialize(byte[] data, Class<T> clazz);

}

2.1 Java序列化

JavaSerializer : 序列化的Java实现

import java.io.*;

public class JavaSerializer implements ISerializer {

    @Override
    public <T> byte[] serialize(T obj) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        try {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            objectOutputStream.writeObject(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return byteArrayOutputStream.toByteArray();
    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
        try {
            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
            Object obj = objectInputStream.readObject();
            return (T) obj;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

}


MainTest : 测试类


import com.bigshen.distributed.model.User;
import com.bigshen.distributed.serializer.ISerializer;
import com.bigshen.distributed.serializer.JavaSerializer;

public class MainTest {

    public static void main(String[] args) {

        User user = new User("shen", 18, "HK");

        /**
         * java serializer
         */
        ISerializer serializer = new JavaSerializer();
        // 序列化
        byte[] bytes = serializer.serialize(user);
        System.out.println("java serializer size: " + bytes.length);
        // 反序列化
        User seriUser = serializer.deserialize(bytes, User.class);
        System.out.println(seriUser);

    }

}

... 运行结果
java serializer size: 121
User{name='shen', age=18, location='HK'}

使用java本身技术继续序列化 对象大小为 : 121

将序列化对象写入文件

import java.io.*;

public class JavaSerializerWithFile implements ISerializer {

    @Override
    public <T> byte[] serialize(T obj) {

        try {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File("User")));
            objectOutputStream.writeObject(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new byte[0];
    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
        try {
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("User")));
            return (T) objectInputStream.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

}

MainTest : 测试类


import com.bigshen.distributed.model.User;
import com.bigshen.distributed.serializer.ISerializer;
import com.bigshen.distributed.serializer.JavaSerializerWithFile;

public class MainTest {

    public static void main(String[] args) {

        User user = new User("shen", 18, "HK");

        /**
         * java file serializer
         */
        ISerializer serializer = new JavaSerializerWithFile();
        // 服务端存储
        serializer.serialize(user);
        // 客户端读取
        User seriUser = serializer.deserialize(bytes, User.class);
        System.out.println(seriUser);

    }

}

...运行结果
User{name='shen', age=18, location='HK'}

生产的user文件内容

�� sr "com.bigshen.distributed.model.UsersZ���4� I ageL locationt Ljava/lang/String;L nameq ~ xp   t HKt shen

将序列化对象存储至文件内来达到持久化目的,当服务重启后来取用。

2.2 XML序列化

XML序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的字节码文件比较大,而且效率不高,适用于对性能不高,而且 QPS 较低的企业级内部系统之间的数据交换的场景,同时 XML 又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟知的 Webs ervice ,就是采用 XML 格式对数据进行序列化的。 XML序列化反序列化的实现方式有很多,熟知的方式有 XStream 和 Java 自带的 XML 序列化和反序列化两种

序列化样例:
引入依赖

            <dependency>
                <groupId>com.thoughtworks.xstream</groupId>
                <artifactId>xstream</artifactId>
                <version>1.4.10</version>
            </dependency>

XmlSerializer :


import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

public class XmlSerializer implements ISerializer {

    XStream xStream = new XStream(new DomDriver());

    @Override
    public <T> byte[] serialize(T obj) {

        return xStream.toXML(obj).getBytes();
    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
        return (T)xStream.fromXML(new String(data));
    }

}

2.3 Json 序列化框架

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,相对于XML来说,JSON的字节流更小,而且可读性也非常好。现在JSON数据格式在企业运用是最普遍的。
JSON序列化常用的开源工具有很多

  1. Jackson https://github.com/FasterXML/jackson
  2. 阿里开源的FastJson https://github.com/alibaba/fastjon
  3. Google的GSON https://github.com/google/
    这几种json序列化工具中, Jackson与fastjson要比GSON的性能要好,但是Jackson、GSON的稳定性要比Fastjson好,而fastjson的优势在于提供的api非常容易使用。

2.4 Hessian 序列化框架

Hessian是一个支持跨语言传输的二进制序列化协议,相对于Java默认的序列化机制来说,Hessian具有更好的性能和易用性,而且支持多种不同的语言。实际上Dubbo采用的就是Hessian序列化来实现,只不过Dubbo对Hessian进行了重构,性能更高。

HessianSerializer

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class HessianSerializer implements ISerializer{
    
    @Override
    public <T> byte[] serialize(T obj) {
        ByteArrayOutputStream outputStream=new ByteArrayOutputStream();
        HessianOutput hessianOutput=new HessianOutput(outputStream);
        try {
            hessianOutput.writeObject(obj);
            return outputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return new byte[0];
    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
        ByteArrayInputStream inputStream=new ByteArrayInputStream(data);
        HessianInput hessianInput=new HessianInput(inputStream);
        try {
            return (T)hessianInput.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

2.5 Protobuf序列化框架

Protobuf是Google的一种数据交换格式,它独立于语言、独立于平台。Google提供了多种语言来实现,比如Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件。Protobuf是一个纯粹的表示层协议,可以和各种传输层协议一起使用。
Protobuf使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要求高的RPC调用。另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中。但是要使用Protobuf会相对来说麻烦些,因为他有自己的语法,有自己的编译器,如果需要用到的话必须要去投入成本在这个技术的学习中protobuf有个缺点就是要传输的每一个类的结构都要生成对应的proto文件,如果某个类发生修改,还得重新生成该类对应的proto文件。

四、protobuf序列化的使用及原理

五、序列化技术选型

技术层面
1.序列化空间开销,也就是序列化产生的结果大小,这个影响到传输的性能
2.序列化过程中消耗的时长,序列化消耗时间过长影响到业务的响应时间
3.序列化协议是否支持跨平台,跨语言。因为现在的架构更加灵活,如果存在异构系统通信需求,那么这个是必须要考虑的
4.可扩展性/兼容性,在实际业务开发中,系统往往需要随着需求的快速迭代来实现快速更新,这就要求我们采用的序列化协议基于良好的可扩展性/兼容性,比如在现有的序列化数据结构中新增一个业务字段,不会影响到现有的服务
5.技术的流行程度,越流行的技术意味着使用的公司多,那么很多坑都已经淌过并且得到了解决,技术解决方案也相对成熟
6.学习难度和易用性

选型建议
1.对性能要求不高的场景,可以采用基于XML的SOAP协议
2.对性能和间接性有比较高要求的场景,那么Hessian、Protobuf、Thrift、Avro都可以。
3.基于前后端分离,或者独立的对外的api服务,选用JSON是比较好的,对于调试、可读性都很不错
4.Avro设计理念偏于动态类型语言,那么这类的场景使用Avro是可以的

序列化技术的性能比较
这个地址有针对不同序列化技术进行性能比较:
https://github.com/eishay/jvm-serializers/wiki

posted @ 2019-08-03 18:38  BigShen  阅读(312)  评论(0)    收藏  举报