Skip to content

Latest commit

 

History

History
439 lines (299 loc) · 14.1 KB

File metadata and controls

439 lines (299 loc) · 14.1 KB

Java 领域的对象如何传输

序列化是把对象的状态信息转化为可存储或传输的形式过程,也就是把对象转化为字节序列 的过程称为对象的序列化 反序列化是序列化的逆向过程,把字节数组反序列化为对象,把字节序列恢复为对象的过程 成为对象的反序列化

public class User implements Serializable {


    private static final long serialVersionUID = -434539422310062943L;

    private String name;
    private int age;
    
    private void writeObject(java.io.ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();

        s.writeObject(name);
    }

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


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ServerSocket serverSocket=null;

        serverSocket=new ServerSocket(8080);

        Socket socket=serverSocket.accept(); //建立好连接

        ObjectInputStream objectInputStream=
                new ObjectInputStream(socket.getInputStream());

        User user=(User)objectInputStream.readObject();

        System.out.println(user);

    }
}
public class Client {

    public static void main(String[] args) throws IOException {
        Socket socket=null;

        socket=new Socket("localhost",8080);

        User user=new User();
        user.setAge(18);
        user.setName("Mic");

        ObjectOutputStream out=new ObjectOutputStream
                (socket.getOutputStream());
        out.writeObject(user);

        socket.close();
    }
}

序列化的高阶认识

简单认识一下 Java 原生序列化

java.io.ObjectOutputStream:表示对象输出流 , 它的 writeObject(Object obj)方法可以对参 数指定的 obj 对象进行序列化,把得到的字节序列写到一个目标输出流中。

java.io.ObjectInputStream:表示对象输入流 ,它的 readObject()方法源输入流中读取字节序 列,再把它们反序列化成为一个对象,并将其返回

序列号

一是默认的 1L,比如:private static final long serialVersionUID = 1L;

二是根据类名、接口名、成员方法及属性等来生成一个 64 位的哈希字段

当实现 java.io.Serializable 接口的类没有显式地定义一个 serialVersionUID 变量时候,Java 序 列化机制会根据编译的 Class 自动生成一个 serialVersionUID 作序列化版本比较用,这种情况 下,如果 Class 文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算 再编译多次,serialVersionUID 也不会变化的。

public interface ISerializer {

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


    <T> T deserialize(byte[] data,Class<T> clazz);
}
public class JavaSerializer implements ISerializer{
    
    @Override
    public <T> byte[] serialize(T obj) {
        ByteArrayOutputStream byteArrayOutputStream=
                new ByteArrayOutputStream();
        try {
            ObjectOutputStream outputStream=
                    new ObjectOutputStream(byteArrayOutputStream);

            outputStream.writeObject(obj);

            return  byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new byte[0];
    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
        ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(data);
        try {
            ObjectInputStream objectInputStream=
                    new ObjectInputStream(byteArrayInputStream);

            return (T) objectInputStream.readObject();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}
public class JavaSerializerWithFile implements ISerializer{


    @Override
    public <T> byte[] serialize(T obj) {
        try {
            ObjectOutputStream outputStream=
                    new ObjectOutputStream(new FileOutputStream
                            (new File("user")));
            outputStream.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;
    }
}

Transient 关键字

Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变 量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

绕开 transient 机制的办法

虽然 name 被 transient 修饰,但是通过我们写的这两个方法依然能够使得 name 字段正确 被序列化和反序列化

writeObject 和 readObject 原理

writeObject 和 readObject 是两个私有的方法,他们是什么时候被调用的呢?从运行结果来 看,它确实被调用。而且他们并不存在于 Java.lang.Object,也没有在 Serializable 中去声明。 我们唯一的猜想应该还是和 ObjectInputStream 和 ObjectOutputStream 有关系,所以基于 这个入口去看看在哪个地方有调用

Java 序列化的一些简单总结

1.Java 序列化只是针对对象的状态进行保存,至于对象中的方法,序列化不关心

2.当一个父类实现了序列化,那么子类会自动实现序列化,不需要显示实现序列化接口

3.当一个对象的实例变量引用了其他对象,序列化这个对象的时候会自动把引用的对象也进 行序列化(实现深度克隆)

4.当某个字段被申明为 transient 后,默认的序列化机制会忽略这个字段

5.被申明为 transient 的字段,如果需要序列化,可以添加两个私有方法:writeObject 和 readObject

影响RPC通信性能的因素

1.序列化的计算性能

2.数据包的大小

3.跨语言

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

1.序列化的数据比较大,传输效率低

2.其他语言无法识别和对接

简单了解各种序列化技术

XML 序列化框架介绍

public class XStreamSerializer 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));
    }
}
public class SerialDemo {

    public static void main(String[] args) {
//        ISerializer iSerializer=new JavaSerializer();
//        ISerializer iSerializer=new JavaSerializerWithFile();
//        ISerializer iSerializer=new XStreamSerializer();
        ISerializer iSerializer=new FastJsonSeriliazer();
//        ISerializer iSerializer=new HessianSerializer();

        User user=new User();// JVM内存中.  如何把内存中的对象进行网络传输.(实体)->字节序列
        user.setAge(18);
        user.setName("Mic");

        //原生实现
        byte[] bytes=iSerializer.serialize(user);
        System.out.println("byte.length:"+bytes.length);
        /**
         * json: byte.leng 23
         * xmlstream: byte.leng 198
         * hessian: byte.length: 50
         * java原生: byte.length: 92
         * protobuf: byte.length:7
         */
        System.out.println(new String(bytes));// byte长度 198   -> 23
        User user1=iSerializer.deserialize(bytes,User.class);
        System.out.println(user1);
    }
} 

JSON 序列化框架

public class FastJsonSeriliazer implements ISerializer{
    @Override
    public <T> byte[] serialize(T obj) {

        return JSON.toJSONString(obj).getBytes();
    }

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

Hessian 序列化框架

Hessian 是一个支持跨语言传输的二进制序列化协议,相对于 Java 默认的序列化机制来说,

Hessian 具有更好的性能和易用性,而且支持多种不同的语言 实际上 Dubbo 采用的就是 Hessian 序列化来实现,只不过 Dubbo 对 Hessian 进行了重构, 性能更高

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;
    }
}

Avro 序列化

kyro 序列化框架

Protobuf 序列化框架

public class ProtobufDemo {

    public static void main(String[] args) {
        UserProtos.User user=UserProtos.User.newBuilder().
                setAge(300).setName("Mic").build();

        byte[] bytes=user.toByteArray();
        System.out.println(bytes.length);

        for (int i = 0; i < bytes.length; i++) {
            System.out.print(bytes[i]+" ");
        }
        //10 3 77 105 99 16 18

    }
}

Protobuf 序列化的原理

protobuf 的基本应用

使用 protobuf 开发的一般步骤是

1.配置开发环境,安装 protocol compiler 代码编译器

2.编写.proto 文件,定义序列化对象的数据结构

3.基于编写的.proto 文件,使用 protocol compiler 编译器生成对应的序列化/反序列化工具 类

4.基于自动生成的代码,编写自己的序列化应用

Protobuf 案例演示

image-20220305214735430

protobuf 序列化原理

image-20220305214757262

字符如何转化为编码 “Mic”这个字符,需要根据 ASCII 对照表转化为数字。 M =77、i=105、c=99 所以结果为 77 105 99 大家肯定有个疑问,这里的结果为什么直接就是 ASCII 编码的值呢?怎么没有做压缩呢?有 没有同学能够回答出来 原因是,varint 是对字节码做压缩,但是如果这个数字的二进制只需要一个字节表示的时候, 其实最终编码出来的结果是不会变化的

image-20220305214835792

image-20220305214845636

image-20220305214908092

image-20220305214923494

总结

Protocol Buffer 的性能好,主要体现在 序列化后的数据体积小 & 序列化速度快,最终使得 传输效率高,其原因如下:

序列化速度快的原因:

a.编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等)

b.采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成

序列化后的数据量体积小(即数据压缩效果好)的原因:

a. 采用了独特的编码方式,如 Varint、Zigzag 编码方式等等

b. 采用 T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑

序列化技术的选型

技术层面

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