美文网首页
分布式架构基础-序列化和反序列化

分布式架构基础-序列化和反序列化

作者: Lemonrel | 来源:发表于2019-11-28 18:43 被阅读0次

    了解序列化的意义
    Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当 JVM处于运行时,这些对象才可能存在,即这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象,Java 对象序列化就能够帮助我们实现该功能。
    简单来说,序列化是把对象的状态信息转化为可存储或传输的形式,也就是把对象转化为字节序列的过程称为对象的序列化。反序列化是序列化的逆向过程,把字节数组反序列化为对象,把字节序列恢复为对象的过程称为对象的反序列化。

    【加群】:857565362
    序列化面临的挑战
    评价一个序列化算法优劣的两个重要指标是:
    1》序列化以后的数据大小
    2》序列化操作本身的速度及系统资源开销(CPU、内存)
    Java语言本身提供了对象序列化机制,也是Java语言本身最重要的底层机制之一,Java本身提供的序列化机制存在两个问题:
    1》序列化的数据比较大,传输效率低
    2》其他语言无法识别和对接
    如何实现一个序列化操作
    在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。
    先定义一个序列化接口:
    public interface ISerializer {
        <T> byte[] serializer(T obj);
        <T> T deSerializer(byte[] data,Class<T> clazz);
    }
    

    JDK提供了Java对象的序列化方式 , 主要通过输出流java.io.ObjectOutputStream 和对象输入流 java.io.ObjectInputStream来实现。其中,被序列化的对象需要实现 java.io.Serializable接口。

    public class JavaSerializer implements ISerializer {
        /**
         * 序列化方法
         * @param obj
         * @param <T>
         * @return
         */
        @Override
        public <T> byte[] serializer(T obj) {
            ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
            ObjectOutputStream objectOutputStream=null;
            try {
                objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
                objectOutputStream.writeObject(obj);
                return byteArrayOutputStream.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if(objectOutputStream!=null){
                    try {
                        objectOutputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    byteArrayOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }
        /**
         * 反序列化方法
         * @param data
         * @param clazz
         * @param <T>
         * @return
         */
        @Override
        public <T> T deSerializer(byte[] data, Class<T> clazz) {
            ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(data);
            ObjectInputStream objectInputStream=null;
            try {
                objectInputStream=new ObjectInputStream(byteArrayInputStream);
                return (T)objectInputStream.readObject();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if(objectInputStream!=null){
                   try {
                       objectInputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    byteArrayInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }
    }
    

    具体实现,对User对象进行序列化操作。

    public class User implements Serializable{
        private String name;
        private int age;
        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 Test {
        public static void main(String[] args) {
            ISerializer serializer=new JavaSerializer();
            User user=new User();
            user.setName("taofut");
            user.setAge(27);
            byte[] bytes=serializer.serializer(user);
            User user1=serializer.deSerializer(bytes,User.class);
            System.out.println(user1);
        }
        //执行结果:User{name='taofut', age=27}
    }
    

    序列化的高阶认识
    serialVersionUID的作用
    Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。
    如果没有为指定的class配置serialVersionUID,那么java编译器会自动给这个class进行一个摘要算法,类似于指纹算法,只要这个文件有任何改动,得到的UID就会截然不同,可以保证在这么多类中,这个编号是唯一的。
    serialVersionUID有两种显示的生成方式:
    一是默认的1L,比如:private static final long serialVersionUID = 1L;
    二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段。
    当实现java.io.Serializable接口的类没有显式地定义一个serialVersionUID变量的时候,Java序列化机制会根据编译的Class自动生成一个serialVersionUID当作序列化版本来比较用,这种情况下,如果Class文件(类名,方法名等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID也不会变化。
    **静态变量序列化 **
    在User中添加一个全局的静态变量num , 在执行序列化以后修改num的值为10, 然后通过反序列化以后得到的对象去输出num的值。

    public class User implements Serializable{
        private String name;
        private int age;
        public static int num=5;//设置静态变量num值为5
    ...
    ...
    ...
    }
    public class Test {
        public static void main(String[] args) {
            ISerializer serializer=new JavaSerializer();
            User user=new User();
            user.setName("taofut");
            user.setAge(27);
            byte[] bytes=serializer.serializer(user);
            //序列化以后,将num值改为10
            User.num=10;
            User user1=serializer.deSerializer(bytes,User.class);
            System.out.println(user1+"--"+User.num);
        }
        //执行结果:User{name='taofut', age=27}--10
    }
    

    最后的输出是10,我们可能会觉得,10是在序列化之后修改的,按理说反序列化应该输出的是5才对。理论上打印的num是从读取的对象里获得的,应该是保存时的状态才对。之所以打印10的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。
    父类的序列化
    一个子类实现了Serializable接口,而它的父类却没有实现Serializable接口,在子类中设置父类的成员变量的值,接着序列化该子类对象。再反序列化出来以后输出父类属性的值。结果应该是什么?

    public class SuperUser {
        String sex;
        public String getSex() {
            return sex;
        }
        public void setSex(String sex) {
            this.sex = sex;
        }
    }
    public class Test {
        public static void main(String[] args) {
            ISerializer serializer=new JavaSerializer();
            User user=new User();
            user.setName("taofut");
            user.setAge(27);
            user.setSex("男");
            byte[] bytes=serializer.serializer(user);
            User.num=10;
            User user1=serializer.deSerializer(bytes,User.class);
            System.out.println(user1+"--"+user1.getSex());
        }
        //执行结果:User{name='taofut', age=27}--null
    }
    

    最终结果发现,父类的sex字段的值为null,也就是说父类没有实现序列化。
    结论:
    1》 当一个父类没有实现序列化时,子类继承该父类并且实现了序列化。在反序列化该子类后,是没办法获取到父类的属性值的。
    2》当一个父类实现序列化,子类自动实现序列化,不需要再显示实现Serializable接口。
    3》当一个对象的实例变量引用了其他对象,序列化该对象时也会把引用对象进行序列化,但是前提是该引用对象必须实现序列化接口。
    **Transient关键字 **
    Transient关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值被设为初始值,如 int型的是0,对象型的是null。

    public class UserTwo extends SuperUser implements Serializable{
        private String name;
        private int age;
        public static int num=5;
        private transient String address;
        ...
        ...
        ...
    }
    public class Test {
        public static void main(String[] args) {
            ISerializer serializer=new JavaSerializer();
            UserTwo user=new UserTwo();
            user.setName("taofut");
            user.setAge(27);
            user.setSex("男");
           user.setAddress("浙江省");//该字段被transient修饰过
           byte[] bytes=serializer.serializer(user);
           UserTwo user1=serializer.deSerializer(bytes,UserTwo.class);
            System.out.println(user1+"--"+user1.getAddress());
        }
        //执行结果:User{name='taofut', age=27}--null
    }
    

    绕开transient机制的办法

    public class UserTwo extends SuperUser implements Serializable{
        private String name;
        private int age;
        public static int num=5;
        private transient String address;
        //序列化对象
        private void writeObject(ObjectOutputStream objectOutputStream) throws IOException {
            objectOutputStream.defaultWriteObject();
            objectOutputStream.writeObject(address);
        }
        //反序列化
        private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
            objectInputStream.defaultReadObject();
            address=(String)objectInputStream.readObject();
        }
       ...
        ...
        ...
    }
    public class Test {
        public static void main(String[] args) {
            ISerializer serializer=new JavaSerializer();
            UserTwo user=new UserTwo();
            user.setName("taofut");
            user.setAge(27);
            user.setSex("男");
            user.setAddress("浙江省");
            byte[] bytes=serializer.serializer(user);
            UserTwo user1=serializer.deSerializer(bytes,UserTwo.class);
            System.out.println(user1+"--"+user1.getAddress());
        }
        //执行结果:User{name='taofut', age=27}--浙江省
    }
    

    以上代码可能会产生一个疑问:writeObject和readObject这两个私有的方法,既不属于 Object、也不是Serializable,为什么能够在序列化的时候被调用呢? 原因是,ObjectOutputStream使用了反射来寻找是否声明了这两个方法。因为ObjectOutputStream使用getPrivateMethod,(反射可以绕开权限限制)所以这些方法必须声明为priate,以至于供ObjectOutputStream来使用。
    对希望采用自定义序列化的字段用transient修饰,然后在先调用writeObject和readObject方法中对transient修饰的字段进行序列化,并在方法最开始调用defaultReadObject和defaultReadObject方法,对其他字段采用默认序列化方式。这样的好处是方便兼容。
    被transient修饰的成员,只是不能被默认的序列化方法序列化(从源码中也可以看到),但却可以被自定义的序列化方法序列化。
    序列化的存储规则

    public class StoreRuleDemo {
        public static void main(String[] args) throws IOException{
            ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream(new File("user")));
            User user=new User();
            user.setName("taofut");
            user.setAge(27);
            user.setSex("男");
            outputStream.flush();
            outputStream.writeObject(user);
            System.out.println(new File("user").length());
            outputStream.writeObject(user);
            outputStream.flush();
            outputStream.close();
            System.out.println(new File("user").length());
        }
        //执行结果
        //89
        //94
    }
    

    我们发现,同一对象两次写入文件,打印出写入一次对象后的存储大小和写入两次后的存储大小,第二次写入对象时文件只增加了5个字节。
    这是因为,Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的5个字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,该存储规则极大的节省了存储空间。
    序列化实现深克隆
    1》浅克隆机制:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。

    public class CloneDemo {
        public static void main(String[] args) throws CloneNotSupportedException {
            Email email=new Email();
            email.setContent("今天晚上6点开会");
            Person p1=new Person("taofut");
            p1.setEmail(email)
            Person p2=p1.clone();
            p2.setName("fut");
            p2.getEmail().setContent("今天晚上6点半开会");
            System.out.println(p1.getName()+"->"+p1.getEmail().getContent());
            System.out.println(p2.getName()+"->"+p2.getEmail().getContent());
        }
        //执行结果
        //taofut->今天晚上6点半开会
        //fut->今天晚上6点半开会
    }
    

    以上案例很好的说明了,浅克隆不能复制新的引用,Email引用还是指向的同一个对象,这就导致了都是”今天晚上6点半开会”的结果出现。
    2》深克隆机制:被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。

    public class Email implements Serializable{
        private String content;
        public String getContent() {
            return content;
        }
        public void setContent(String content) {
            this.content = content;
        }
    }
    public class Person implements Cloneable,Serializable{
        private String name;
        private Email email;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Email getEmail() {
            return email;
        }
        public void setEmail(Email email) {
            this.email = email;
        }
        @Override
        protected Person clone() throws CloneNotSupportedException {
            return (Person) super.clone();
        }
        public Person(String name) {
            this.name = name;
        }
        //深克隆方法
        public Person deepClone() throws IOException,ClassNotFoundException{
            ByteArrayOutputStream bos=new ByteArrayOutputStream();
            ObjectOutputStream outputStream=
                    new ObjectOutputStream(bos);
            outputStream.writeObject(this);
            ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream inputStream=new ObjectInputStream(bis);
            return (Person)inputStream.readObject()
        }
    public class DeepDemo {
        public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
            Email email=new Email();
            email.setContent("今天晚上6点开会");
            Person p1=new Person("taofut");
            p1.setEmail(email);
            Person p2=p1.deepClone();
            p2.setName("fut");
            p2.getEmail().setContent("今天晚上6点半开会");
            System.out.println(p1.getName()+"->"+p1.getEmail().getContent());
            System.out.println(p2.getName()+"->"+p2.getEmail().getContent());
        }
        //执行结果:
        //taofut->今天晚上6点开会
        //fut->今天晚上6点半开会
    

    这样就能实现深克隆效果,原理是把对象序列化输出到一个流中,然后在把对象从序列化流中读取出来,这个对象就不是原来的对象了。
    推荐阅读文章:Java高级架构

    相关文章

      网友评论

          本文标题:分布式架构基础-序列化和反序列化

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