Java序列化、反序列化

前言

  说到序列化可能很多开发人员对此并不清楚,甚至没有了解,作为一个专业的程序员怎么能不懂这个呢,今天就来聊聊这个,我们都知道,程序运行时对象数据在内存中,程序关闭后,数据就不存在了,这个时候我们希望对象数据能够进行持久保存,需要的时候根据那就拿出来用。那就用到了序列化,通过序列化我们可以将对象数据转换为可取用的格式(比如二进制、JSON)存储到磁盘,或者通过网络传输到另一条机器上,在需要的时候在还原到内存中,这个过程叫反序列化。贴两句专业描述:

序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。

从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。

知道了什么是序列化,那如何实现序列化和反序列化呢?

实现序列化和反序列化

为了使对象可序列化,需实现java.io.Serializable接口或者Externalizable接口。

  • 对于使用Serializable声明的类,对象序列化可以自动保存和还原对象每个类的字段,并通过添加字段或超类型来自动处理不断演变的类。一个可序列化的类可以声明要保存或还原其哪些字段,以及写入和读取可选值和对象。
  • 对于使用Externalizable声明的类,对象序列化将完全控制其外部格式以及如何保存和还原超类型的状态委托给该类。

实现接口是不够的,我们还需要通过对象流来对数据进行读取和写入:

ObjectOutputStream类包含用于序列化对象的writeObject()方法。

public final void writeObject(Object obj)  throws IOException

ObjectInputStream类包含用于反序列化对象的readObject()方法。

public final Object readObject()  throws IOException, ClassNotFoundException

  关于序列化属性的要求,默认可序列化属性字段被声明为非transient和非static字段。在可序列化的类中声明serialPersistentFields字段,可以覆盖此默认计算 serialPersistentFields。必须使用ObjectStreamField列出目标对象和可序列化字段的类型的对象数组来初始化该字段。字段的修饰符必须是privatestatic的和final的,如果该字段的值为null或着不是此实例的ObjectStreamField[],或不存在privatestaticfinal修饰符,那么此定义没有任何作用。

serialVersionUID

  serialVersionUID 是 Java 为每个序列化类产生的版本标识,可用来保证在反序列时,发送方发送的和接受方接收的是可兼容的对象。如果接收方接收的类的 serialVersionUID 与发送方发送的serialVersionUID不一致,进行反序列时会抛出 InvalidClassException
  如果序列化的类未明确声明serialVersionUID,则序列化运行时将根据该类的各个方面为该类计算一个默认值。那么重新编译后的serialVersionUID将会发生变化,任何地方的更改都会影响到序列化的数据,因此建议所有的序列化类显式的声明serialVersionUID值。
  建议对UID使用private修饰符,因为它作为继承属性没有用。

  • 如果父类已经实现了Serializable接口,则子类不需要实现它,反之亦然。
  • 仅非静态属性通过序列化过程可以保存。
  • 静态属性和临时属性不会通过序列化过程保存。因此,如果不想保存非静态数据属性值,需要加入transient声明。
  • 反序列化对象时,永远不会调用对象的构造函数。
  • 关联对象必须实现Serializable接口。

用例

定义User


import lombok.Data;
import java.io.Serializable;

/**
 * User
 *
 * @author SanLi
 * Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2020/1/7 11:42
 */
@Data
public class User implements Serializable {
    /**
     * serialVersionUID
     */
    private static final long serialVersionUID = 4760229593345099240L;
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 住址
     */
    private transient String address;

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

原生方式
序列化对象
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.logging.Logger;

/**
 * 序列化
 *
 * @author SanLi
 * Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2020/1/7 11:43
 */
public class Serialization {
    /**
     * Logger
     */
    private static final Logger LOGGER = Logger.getAnonymousLogger();

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        File file = new File("user.txt");
        User user = new User("小美", 18);
        //将对象保存在文件中
        FileOutputStream fileOut = new FileOutputStream(file);
        ObjectOutputStream out = new ObjectOutputStream(fileOut);
        //对象序列化的方法
        out.writeObject(user);
        //关闭
        out.close();
        fileOut.close();
        LOGGER.info("对象已被序列化");
    }
}

运行程序,在当前项目下找到user.txt文件,打开查看,即可看到序列化后的内容。

反序列化
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.logging.Logger;

/**
 * 反序列化
 *
 * @author SanLi
 * Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2020/1/7 15:21
 */
public class Deserialization {
    /**
     * Logger
     */
    private static final Logger LOGGER = Logger.getAnonymousLogger();


    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //从文件中读取对象
        File file = new File("user.txt");
        FileInputStream fileInput = new FileInputStream(file);
        ObjectInputStream in = new ObjectInputStream(fileInput);
        //对象反序列化的方法
        User userRead = (User) in.readObject();
        //关闭
        fileInput.close();
        in.close();
        LOGGER.info("对象已被反序列化" + " " + userRead.toString());
    }
}

控制台输出,可以看到将刚才对象中的数据反序列化了出来,由于address属性我们声明了transient,序列化并没有保存。

信息: 对象已被反序列化 User(name=小美, age=18, address=null)
serialPersistentFields

在上述基础上,测试声明serialPersistentFields情况。

  • 定义为null,序列化后在反序列化,打印出了对象字段数据,无效
private static final ObjectStreamField[] serialPersistentFields = null;
  • 定义User类不存在字段、序列化抛出java.io.InvalidClassException 异常
private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("name_",String.class)};
  • 声明为{}没有序列化任何字段,序列化、序列化后进行打印,对象字段数据为null,生效
private static final ObjectStreamField[] serialPersistentFields = {};
  • 只配置序列化name字段,序列化后反序列化打印,对象输出只有name有值,生效。
private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("name",String.class)};
JSON序列化

   JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。JSON序列化就是将数据对象转为JSON字符串,JSON的可读性更好,方便调试。广泛使用的JSON库有Jacksonfastjson
   JSON序列化通常会通过网络传输对象、而对象中往往会存在敏感数据,所以序列化常常成为黑客攻击点,攻击者巧妙的利用反序列化过程构造恶意代码,是程序在反序列化过程中执行任意代码,Jacksonfastjson 都曾经出现过反序列化漏洞,那么如何防范攻击呢?有些敏感属性不需要进行序列化传输,可以加transient关键字声明,避免此属性序列化,如果一定要传递敏感数据,可以使用对称和非对称加密传输,在进行解密处理后还原到对象中。开发者一定要有安全防范意识,对传入数据内容进行校验或权限控制,及时更新JSON库安全漏洞,避免受到攻击。在工程中目前自己常用的JSON库是fastjson,fastjson可以通过定制序列化对需要序列化的数据进行处理。fastjson定制序列化

序列化
import com.alibaba.fastjson.JSON;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.logging.Logger;

/**
 * JSON序列化
 *
 * @author SanLi
 * Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2020/1/7 17:55
 */
public class Serialization {
    /**
     * Logger
     */
    private static final Logger LOGGER = Logger.getAnonymousLogger();

    public static void main(String[] args) throws IOException {
        //创建对象
        User user = new User("小美", 18, "山东省济南市");
        //序列化
        String json = JSON.toJSONString(user);
        //写入文件
        FileOutputStream fileOut = new FileOutputStream(new File("user.json"));
        fileOut.write(json.getBytes());
        LOGGER.info("序列化对象完成");
    }
}

打开项目根目录下的user.json,便可以看到对象序列化后的JSON串。

反序列化
import com.alibaba.fastjson.JSONObject;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.logging.Logger;

/**
 * JSON反序列化
 *
 * @author SanLi
 * Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2020/1/7 17:55
 */
public class Deserialization {
    /**
     * Logger
     */
    private static final Logger LOGGER = Logger.getAnonymousLogger();

    public static void main(String[] args) throws IOException {
        //读取文件
        FileInputStream fileOut = new FileInputStream(new File("user.json"));
        int len;
        byte[] buf = new byte[1024];
        while ((len = fileOut.read(buf)) != -1) {
            //反序列化
            User user = JSONObject.parseObject(new String(buf, 0, len), User.class);
            LOGGER.info(user.toString());
        }
        LOGGER.info("反序列化对象完成");
    }
}

控制台输出打印的对象,由于address 属性使用transient声明,所以并没有被序列化,所以也不会反序列化,所以是null

信息: User(name=小美, age=18, address=null)

JSON序列化应用场景是很多的,其中在自己负责的项目里,强制规定了对象日志打印格式为JSON,因为出现问题非常方便查看并 Debug,包括spring data redis 序列化用的也是JSON方式。

参考

Java Serializable Doc
oracle serialization doc

推荐阅读更多精彩内容