Hi!我是小小,今天是本周的第四篇,本篇将会着重讲解关于Java序列化的内容

Java序列化

java序列化和反序列化数据,是通过ObjectOutputStream和ObjectInputStream这两个类来实现的,

举个例子:

要序列化的对象data1

public class data1 implements Serializable {

        private int id;
        private String name;
        private String pwd;
        private String pwd2;

        public int getId(){ return id; }
        public void setId(int id){ this.id = id; }

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

        public String getPwd(){ return pwd; }
        public void setPwd(String pwd){ this.pwd = pwd; }

        public String getPwd2(){ return pwd2; }
        public void setPwd2(String pwd2){ this.pwd2 = pwd2; }
    }

序列化操作类SerializeTest

public class SerializeTest {
        public void serialize() throws Exception{
            data1 d = new data1();
            d.setId(1036);
            d.setName("data1");
            d.setPwd("pwd1");
            d.setPwd2("pwd2");
            FileOutputStream fos = new FileOutputStream("d:/project/serial/data1");
            ObjectOutputStream oos = new ObjectOutputStream(fos); //创建Object输出流对象
            oos.writeObject(d); //向data1文件中写入序列化数据data1类
            fos.close();
            oos.close();
            System.out.println("序列化完成");
        }
        public data1 deSerialize() throws Exception{
            FileInputStream fis = new FileInputStream("d:/project/serial/data1");
            ObjectInputStream ois = new ObjectInputStream(fis); //创建Object输入流对象
            data1 d = (data1)ois.readObject(); //从data1文件中反序列化出data1类数据
            ois.close();
            fis.close();
            return d;
        }

        public static void main(String[] args) throws Exception{
            SerializeTest s = new SerializeTest();
            s.serialize();
            data1 d = s.deSerialize();
            System.out.println("id:"+d.getId());
            System.out.println("name:"+d.getName());
            System.out.println("pwd:"+d.getPwd());
        }
    }

执行后会发现 序列化成功,输出文件data1,同时反序列化成功,我们可以从data1文件中反序列化出data1类,能够获取其中的信息。

我们看看data1文件, notepad打开它长这样

建议 | 在中国不是程序猿,不建议你用序列化!插图

再按十六进制打开看看,
建议 | 在中国不是程序猿,不建议你用序列化!插图1

是的,这就是序列化。

Java序列化缺陷

无法跨语言

Java序列化目前只支持Java语言实现的框架,其它语言大部分都没有使用Java的序列化框架,也没有实现Java序列化这套协议,因此,如果两个基于不同语言编写的应用程序之间通信,使用Java序列化,则无法实现两个应用服务之间传输对象的序列化和反序列化。

容易被攻击

Java官网安全编码指导方针里有说明,“对于不信任数据的反序列化,从本质上来说是危险的,应该避免“。可见Java序列化并不是安全的。

我们知道对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,这个方法其实是一个神奇的构造器,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这也就意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。

对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致 hashCode 方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。例如下面这个案例就可以很好地说明。

Set root = new HashSet();  
Set s1 = root;  
Set s2 = new HashSet();  
for (int i = 0; i < 100; i++) {  
   Set t1 = new HashSet();  
   Set t2 = new HashSet();  
   t1.add("test"); //使t2不等于t1  
   s1.add(t1);  
   s1.add(t2);  
   s2.add(t1);  
   s2.add(t2);  
   s1 = t1;  
   s2 = t2;   
} 

实现攻击的原理:Apache Commons Collections允许链式的任意的类函数反射调用,攻击者通过实现了Java序列化协议的端口,把攻击代码上传到服务器上,再由Apache Commons Collections里的TransformedMap来执行。

序列化后的流太大

序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。

Java 序列化中使用了 ObjectOutputStream 来实现对象转二进制编码,那么这种序列化机制实现的二进制编码完成的二进制数组大小,相比于 NIO 中的 ByteBuffer 实现的二进制编码完成的数组大小,有没有区别呢?

User user = new User();
user.setUserName("test");
user.setPassword("test");

ByteArrayOutputStream os =new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(os);
out.writeObject(user);
byte[] testByte = os.toByteArray();
System.out.print("ObjectOutputStream 字节编码长度:" + testByte.length + "n");

ByteBuffer byteBuffer = ByteBuffer.allocate( 2048);

byte[] userName = user.getUserName().getBytes();
byte[] password = user.getPassword().getBytes();
byteBuffer.putInt(userName.length);
byteBuffer.put(userName);
byteBuffer.putInt(password.length);
byteBuffer.put(password);        
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
System.out.print("ByteBuffer 字节编码长度:" + bytes.length+ "n");


结果

ObjectOutputStream 字节编码长度:99
ByteBuffer 字节编码长度:16

性能太差

如果序列化的速度慢,就会影响网络通信的效率,从而增加系统的响应时间。

User user = new User();
    user.setUserName("test");
    user.setPassword("test");

    long startTime = System.currentTimeMillis();

     for(int i=0; i<1000; i++) {
        ByteArrayOutputStream os =new ByteArrayOutputStream();
          ObjectOutputStream out = new ObjectOutputStream(os);
          out.writeObject(user);
          out.flush();
          out.close();
          byte[] testByte = os.toByteArray();
          os.close();
     }


long endTime = System.currentTimeMillis();
System.out.print("ObjectOutputStream 序列化时间:" + (endTime - startTime) + "n");

long startTime1 = System.currentTimeMillis();
for(int i=0; i<1000; i++) {
   ByteBuffer byteBuffer = ByteBuffer.allocate( 2048);

        byte[] userName = user.getUserName().getBytes();
        byte[] password = user.getPassword().getBytes();
        byteBuffer.putInt(userName.length);
        byteBuffer.put(userName);
        byteBuffer.putInt(password.length);
        byteBuffer.put(password);

        byteBuffer.flip();
        byte[] bytes = new byte[byteBuffer.remaining()];
}
long endTime1 = System.currentTimeMillis();
System.out.print("ByteBuffer 序列化时间:" + (endTime1 - startTime1)+ "n");

运行结果

ObjectOutputStream 序列化时间:29
ByteBuffer 序列化时间:6

总结

  1. Java 默认的序列化是通过 Serializable 接口实现的,只要类实现了该接口,同时生成一个默认的版本号,就可以实现序列化
  2. 序列化存在存在安全漏洞、不跨语言以及性能差等缺陷,
  3. FastJson、Protobuf、Kryo 是比较有特点的,而且性能以及安全方面都得到了业界的认可,我们可以结合自身业务来选择一种适合的序列化框架

关于作者

我是小小,双鱼座的程序猿,我们下期再见~bye~