概念

序列化:就是把对象转化成字节。
反序列化:把字节数据转换成对象。

对象序列化场景:

1、对象网络传输
例如:在微服务系统中或给第三方提供接口调用时,使用rpc进行调用,一般会把对象转化成字节序列,才能在网络上传输;接收方则需要把字节序列再转化为java对象。

2、对象保存至文件中
例如:hibernate中的二级缓存:把从数据库中查询出的对象,序列化转存到硬盘中,下次读取的时候,首先从内存中找是否有该对象,如果没有在去二级缓存(硬盘)中去查找。减少数据库的查询次数,提升性能。

3、tomcat的钝化和活化

  • tomcat 的session 钝化和活化之 StandarManager :
    当Tomcat服务器关闭或者重启时tomcat服务器会将当前内存中的session对象钝化到服务器文件系统中;
    另一种情况是web应用程序被重新加载时(其实原理也是重启tomcat),内存中的session对象也会被钝化到服务器的文件系统中
    当系统启动时,会把序列化到硬盘上session重新加载到内存中来。这样用户还保持这登录状态,提供系统的可用性。
    这样,tomcat重启,如果用户在tomcat重启之前登录过,然后在tomcat重启后可以不需要登录(前提是session没过期前,默认是30分钟过期)。

  • tomcat 的session 钝化和活化之 Persistentmanager:
    当网站有大量用户访问的时候,服务器会创建大量的session,会占用大量的服务器内存资源,当用户开着浏览器一分钟不操作页面的话建议将session钝化,将session生成文件放在tomcat工作目录下。

可参考 : Tomcat 之 Session的活化和钝化 源码分析

1. java 序列化 Serializable

java 中只要对象实现了 java.io.Serializable 就可以进行序列化。

public class User implements Serializable {
    private String userName;
    private String password;
    private String addr;

    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getAddr() {
        return addr;
    }
    public void setAddr(String addr) {
        this.addr = addr;
    }
    @Override
    public String toString() {
        return "User [userName=" + userName + ", password=" + password + ", addr=" + addr + "]";
    }
}

该 User 类实现了 Serializable 接口,那么该类应该怎么序列化和发序列化呢?

2. ObjectInputStream 和 ObjectOutputStream

Java IO 包中为我们提供了 ObjectInputStream 和 ObjectOutputStream 两个类。
java.io.ObjectOutputStream 类实现类的序列化功能。
java.io.ObjectInputStream 类实现了反序列化功能。

示例如下:

public class Test{
    public static void main(String[] args) throws Exception {
        File file = new File("d:\\a.user");
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
        User user1 = new User();
        user1.setUserName("zhangsan");
        user1.setPassword("123456");
        user1.setAddr("北京中关村");
        oos.writeObject(user1);
        
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        User user2 = (User)ois.readObject();
        System.out.println(user2);
    }
}

输出结果:
User [userName=zhangsan, password=123456, addr=北京中关村]

  1. 使用 ObjectOutputStream 把 user1 实例序列化到 d:\user 文件中。
  2. 使用 ObjectInputStream 把 d:\user 文件中的数据反序列化成 user2 实,并打印。

如果考虑安全问题,我们不想把密码序列化进行保存,那么该怎么做呢?

3. transient关键字

当某个字段被声明为transient后,默认序列化机制就会忽略该字段。此处将User类中的password字段声明为transient,如下所示,

public class User implements Serializable {
    private String userName;
    private transient String password;
    private String addr;
    ... ...

然后在执行Test类的 main 方法,执行结果如下:

输出结果:
User [userName=zhangsan, password=null, addr=北京中关村]


当我们把 User 对象序列化保存到文件中,这时 User 类结构添加了一个新字段,那么它能成功反序列化吗?

serialVersionUID的作用

 

 

User 类中添加一个新属性 email 字段,如下图:
 
Java 序列化 之 Serializable
 

 

 

然后再执行反序列化
 
Java 序列化 之 Serializable
 

执行结果如下:

Exception in thread "main" java.io.InvalidClassException: cn.com.infcn.serial.User; local class incompatible: stream classdesc serialVersionUID = 1318824539146791009, local class serialVersionUID = 7884536922902331245

执行反序列化报 java.io.InvalidClassException 异常。这是由于 User 类修改了,
也就是修改过后的class,不兼容了,处于安全机制考虑,程序抛出了错误,并且拒绝载入。从异常信息中可以看出,它是根据 serialVersionUID 值进行判断类是否修改过。

如果在添加新字段 email 后,还可以继续加载之前的字段怎么办呢?

 

我们可以在类中添加 serialVersionUID 属性字段。
 
Java 序列化 之 Serializable
 

serialVersionUID 的值和报错中的 "stream classdesc serialVersionUID" 的值一样就可以反序列化了。

如果类中没有显示的声明 serialVersionUID 属性,那么java编译器会自动为我们生成一个 serialVersionUID (应该是根据 属性和方法进行摘要算出来的,方法里面内容变动 serialVersionUID 的值不会改变。)

如果 User 对象升级版本,修改了结构,而且不想兼容之前的版本,那么只需要修改下 serialVersionUID 的值就可以了。

建议,每个需要序列化的对象,都要添加一个 serialVersionUID 字段。

如果需要把设置的 transient 的字段也需要序列化和发序列化,我们应该怎么办?我们需要对密码加密序列化,反序列化后解密处理,又应该怎么做?

readObject 和 writeObject

 
Java 序列化 之 Serializable
 
crypto 方法,实现加解密功能。
    /**
     * 简单加密加密解密字符串 加密解密思路:先将字符串变成byte数组,再将数组每位与key做位运算,得到新的数组就是加密或解密后的byte数组.
     * 知识:^ 是java位运算,可以百度了解下,a = b ^ skey 反之也成立,即b = a ^ skey
     * 
     * @param str 解密/加密 字符串
     * @return
     * @throws Exception
     */
    static String crypto(String str) {
        try {
            byte skey = (byte) 88; //密钥
            byte[] bytes = str.getBytes("GBK");

            for (int i = 0; i < bytes.length; i++) {
                bytes[i] = (byte) (bytes[i] ^ skey);
            }
            return new String(bytes, "GBK");
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

我们只需要在当前 User 类中添加 readObject() 和 writeObject() 方法,在 writeObject 方法中实现对 password 的字段加密,在 readObject 方法中实现对 password 字段解密,并赋值给 User 对象即可。

readObject() 和 writeObject() 可以实现对 transient 和 非transient字段进行序列化。

ArrayList 序列化源码分析

我们知道,ArrayList 是通过数组进行存储数据的,当数组中元素达到数组的最大容量时,会自动生成一个更大的数组,并复制到更大的数组中。

 
Java 序列化 之 Serializable
 

打开ArrayList 源码,我们可以知道,数据是存储在 Object[] elementData 数组中。
该属性是 transient 关键字修饰的,通过上面代码可以知道,用 transient 关键字修饰的字段,默认是不能被序列化的。ArrayList 如果要实现序列化,那么就必须通过 readObject() 和 writeObject() 方法去实现序列化,那么他这是多此一举吗?

writeObject() 方法

 
Java 序列化 之 Serializable
 

通过源码,我们可以看到,ArrayList 序列化数组元素时做了优化。
因为 ArrayList 的 elementData 数组大小,不是ArrayList 的实际容量,这里只把实际存储在 elementData中的数据,进行序列化。这样减少了序列化的流大小。

 
Java 序列化 之 Serializable
 
 
 
25人点赞
 
 


作者:jijs
链接:https://www.jianshu.com/p/af2f0a4b03b5
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关文章: