前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java对象的序列化和反序列化源码阅读

Java对象的序列化和反序列化源码阅读

作者头像
Ryan-Miao
发布于 2018-03-14 02:51:20
发布于 2018-03-14 02:51:20
1.2K00
代码可运行
举报
文章被收录于专栏:Ryan MiaoRyan Miao
运行总次数:0
代码可运行

前言

序列化和反序列化看起来用的不多,但用起来就很关键,因为稍一不注意就会出现问题。序列化的应用场景在哪里?当然是数据存储和传输。比如缓存,需要将对象复刻到硬盘存储,即使断电也可以重新反序列化恢复。下面简单理解序列化的用法以及注意事项。

如何序列化

Java中想要序列化一个对象,必须实现Serializable接口。然后就可以持久化和反序列化了。下面是一个简单用法。

项目测试代码: https://github.com/Ryan-Miao/someTest/blob/master/src/main/java/com/test/java/serial/TestSerialize.java

我们给一个测试类:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.test.java.serial;

import lombok.Builder;
import lombok.Data;

import java.io.Serializable;

/**
 * @author Ryan Miao
 */
@Data
@Builder
public class Foo implements Serializable {

    private static final String LOGGER = "logger";
    public static final String PUB_STATIC_FINAL = "publicStaticFinal";
    public static String PUB_STATIC;

    public String fa;
    private String fb;
    transient public String ta;
    transient private String tb;
}

然后,测试序列化和反序列的数据是否丢失。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class TestSerialize {

    private static final String filename = "D:/test.txt";

    @Test
    public void testSer() throws IOException, ClassNotFoundException {
        final Foo foo = Foo.builder()
                .fa("fa")
                .fb("fb")
                .ta("ta")
                .tb("tb")
                .build();

        Foo.PUB_STATIC  = "test";

        ObjectOutputStream os = new ObjectOutputStream(
                new FileOutputStream(filename));
        os.writeObject(foo);
        os.flush();
        os.close();

    }

    @Test
    public void testRead() throws IOException, ClassNotFoundException {
        ObjectInputStream is = new ObjectInputStream(new FileInputStream(filename));
        Foo foo2 = (Foo) is.readObject();
        is.close();

        Assert.assertEquals("fa", foo2.getFa());
        Assert.assertEquals("fb", foo2.getFb());
        Assert.assertEquals(null, foo2.getTa());
        Assert.assertEquals(null, foo2.getTb());

        Assert.assertNull(foo2.PUB_STATIC);
    }
}

显然,transient修饰的字段不能被序列化,至于静态字段,这里不做测试,但要清楚。静态字段只和class类相关,和实例无关。而序列化是针对实例的,所以无所谓对比内容变化。那么,静态字段反序列化后数据是什么样子的呢?当然是类变量本身应该的样子。如果没有初始化,则是默认值, 本测试中的结果为null。

为什么可以序列化

我们只要实现了Serialiable就可以序列化,那么为什么呢?查看ObjectOutputStreamwriteObject方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// remaining cases
if (obj instanceof String) {
    writeString((String) obj, unshared);
} else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
    writeOrdinaryObject(obj, desc, unshared);
} else {
    if (extendedDebugInfo) {
        throw new NotSerializableException(
            cl.getName() + "\n" + debugInfoStack.toString());
    } else {
        throw new NotSerializableException(cl.getName());
    }
}

显然,只针对String,Enum以及Serializable做了处理,因此想要序列化必须要实现这个接口。当然,String和Enum也实现了Serializable。

如何自定义序列化,Java基础类库中的ArrayList等为什么用transient还能序列化

简单的对象,对于不想序列化的字段,只要声明为transient就好。而有时候,我想对部分字段处理后序列化。比如ArrayList中存储数据的transient Object[] elementData;。我们知道ArrayList是可以序列化的,根源就在于自定义这里了。下面跟踪ObjectOutputStream源码,知道自定义的执行部分就可以验证了。

入口: java.io.ObjectOutputStream#writeObject

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public final void writeObject(Object obj) throws IOException {
    if (enableOverride) {
        writeObjectOverride(obj);
        return;
    }
    try {
        writeObject0(obj, false);
    } catch (IOException ex) {
        if (depth == 0) {
            writeFatalException(ex);
        }
        throw ex;
    }
}

然后,核心方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private void writeObject0(Object obj, boolean unshared)
        throws IOException{
    boolean oldMode = bout.setBlockDataMode(false);depth++;
    try {
        //省略若干行
        for (;;) {
            // 省略若干行
            desc = ObjectStreamClass.lookup(cl, true);
            //省略若干行
        }
        //省略若干行
        if (obj instanceof String) {
            writeString((String) obj, unshared);
        } else if (cl.isArray()) {
            writeArray(obj, desc, unshared);
        } else if (obj instanceof Enum) {
            writeEnum((Enum<?>) obj, desc, unshared);
        } else if (obj instanceof Serializable) {
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            //....
        }
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}

这里,显然可以看到真正的执行序列化代码是writeOrdinaryObject(obj, desc, unshared);。 但直接追踪进去发现里面有许多初始化的字段是在之前做的处理。因此,先卖个关子,看前面初始化的部分,只找到我们想要初始化的字段即可。

进入desc = ObjectStreamClass.lookup(cl, true);

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
static ObjectStreamClass lookup(Class<?> cl, boolean all) {
    //省略若干行
    if (entry == null) {
        try {
            entry = new ObjectStreamClass(cl);
        } catch (Throwable th) {
            entry = th;
        }
        //.....
    }
    //省略若干行
}

进入entry = new ObjectStreamClass(cl);这里就是真正的初始化地方,前面省略的代码是缓存处理,当然缓存使用的ConcurrentHashMap。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private ObjectStreamClass(final Class<?> cl) {
    //省略无数行以及括号
    writeObjectMethod = getPrivateMethod(cl, "writeObject",
                            new Class<?>[] { ObjectOutputStream.class },
                            Void.TYPE);
    readObjectMethod = getPrivateMethod(cl, "readObject",
                            new Class<?>[] { ObjectInputStream.class },
                            Void.TYPE);
    //省略无数行

没错,费了这么大劲就是为了找到这两个method。通过反射,获取到目标class的两个私有方法writeObject, readObject。这两个就是自定义方法所在。

初始化完毕之后,我们再来继续序列化的代码. 回到刚才的核心方法,找到writeOrdinaryObject(obj, desc, unshared);, 进入,然后,继续找到writeSerialData(obj, desc);, 到这里就是真正执行序列化的代码了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
{
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;
        if (slotDesc.hasWriteObjectMethod()) {
            //....
            try {
                curContext = new SerialCallbackContext(obj, slotDesc);
                bout.setBlockDataMode(true);
                slotDesc.invokeWriteObject(obj, this);
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
            } finally {
                //...
            }

            curPut = oldPut;
        } else {
            defaultWriteFields(obj, slotDesc);
        }
    }
}

显然,判断writeObject这个method是否初始化了,如果有,则直接调用这个方法,没有则默认处理。到此,跟踪完毕,我想要自定义序列化只要重写writeObject, readObject这两个方法即可。

下面看看ArrayList是怎么做的

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

因为数组被设置不允许序列化,先默认序列化其他信息,然后单独处理数组里的内容,挨着写入元素。然后,对应读取方法也要改。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

为什么要这么做?因为数组元素有很多空余空间,对我们来说不需要序列化。通过这样自定义,把需要的元素序列化,可以节省空间。

serialVersionUID为什么有的有,有的没有,什么时候用,意义是什么

以下内容来自: https://www.cnblogs.com/ouym/p/6654798.html

什么是serialVersionUID ?

serialVersionUID表示:“串行化版本统一标识符”(serial version universal identifier),简称UID

serialVersionUID必须定义成下面这种形式:static final long serialVersionUID = xxxL;

serialVersionUID 用来表明类的不同版本间的兼容性。有两种生成方式: 一个是默认的1L;另一种是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段 。

为什么要声明serialVersionUID

java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。 java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

只有实现了Serializable或Externalizable接口的类的对象才能被序列化。

Externalizable接口继承自Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。 凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:private static final long serialVersionUID;

类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID。显式地定义serialVersionUID有两种用途:

  1. 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;在某些场合,不希望类的不同版本对序列化兼容, 因此需要确保类的不同版本具有不同的serialVersionUID。
  2. 当你序列化了一个类实例后,希望更改一个字段或添加一个字段,不设置serialVersionUID,所做的任何更改都将导致无法反序化旧有实例,并在反序列化时抛出一个异常。 如果你添加了serialVersionUID,在反序列旧有实例时,新添加或更改的字段值将设为初始化值(对象为null,基本类型为相应的初始默认值),字段被删除将不设置。

注意事项

  1. 序列化时,只对对象的状态进行保存,而不管对象的方法;
  2. 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
  3. 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
  4. 并非所有的对象都可以序列化,,至于为什么不可以,有很多原因了,比如:
    1. 安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。
    2. 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分 配,而且,也是没有必要这样实现。

参考

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
PHP反序列化漏洞
魔术方法是PHP面向对象中特有的特性。它们在特定的情况下被触发,都是以双下划线开头,你可以把它们理解为钩子,利用模式方法可以轻松实现PHP面向对象中重载(Overloading即动态创建类属性和方法)
Andromeda
2022/10/27
1K0
PHP反序列化漏洞
详解php反序列化
“所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。”
用户8824291
2021/07/13
7800
PHP的反序列化和POP链利用
POP面向属性编程,常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。类似于PWN中的ROP,有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,利用一次次的“gadget”找到漏洞点。
Andromeda
2023/10/21
1.1K0
PHP的反序列化和POP链利用
从CTF中学习PHP反序列化的各种利用方式
为了方便数据存储,php通常会将数组等数据转换为序列化形式存储,那么什么是序列化呢?序列化其实就是将数据转化成一种可逆的数据结构,自然,逆向的过程就叫做反序列化。
Ms08067安全实验室
2022/09/26
3.4K0
web安全 -- php反序列化漏洞
序列化是将对象转换为字节流,在序列化期间,对象将当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象状态,重新创建该对象,序列化的目的是便于对象在内存、文件、数据库或者网络之间传递。
Gh0st1nTheShel
2022/01/24
8750
php反序列化漏洞简单总结
​ 这其实是为了解决 PHP 对象传递的一个问题,因为 PHP 文件在执行结束以后就会将对象销毁,那么如果下次有一个页面恰好要用到刚刚销毁的对象就会束手无策,总不能你永远不让它销毁,等着你吧,于是人们就想出了一种能长久保存对象的方法,这就是 PHP 的序列化,那当我们下次要用的时候只要反序列化一下就 ok 啦。
pankas
2022/08/10
7680
php反序列化漏洞简单总结
CTF笔记-1-PHP序列化与反序列化(__sleep与__wakeup)
__sleep 与 __wakeup 序列化: 将一个对象转化成字符串 反序列化:将一个字符串转换成对象
Baige
2022/03/22
1.3K0
CTF笔记-1-PHP序列化与反序列化(__sleep与__wakeup)
PHP反序列化漏洞说明
PHP程序为了保存和转储对象,提供了序列化的方法,序列化是为了在程序运行的过程中对对象进行转储而产生的。
Ms08067安全实验室
2020/01/02
7840
CTFshow刷题日记-WEB-反序列化(web254-278)PHP反序列化漏洞、pop链构造、PHP框架反序列化漏洞、python反序列化漏洞
只要 get 传参反序列化后的字符串有 ctfshow_i_love_36D 就可以
全栈程序员站长
2022/09/14
2K0
CTFshow刷题日记-WEB-反序列化(web254-278)PHP反序列化漏洞、pop链构造、PHP框架反序列化漏洞、python反序列化漏洞
初探序列化与反序列化
在写程序尤其是写网站的时候,经常会构造类,并且有时候会将实例化的类作为变量进行传输。
偏有宸机
2020/11/04
8340
初探序列化与反序列化
CTF竞赛 | PHP反序列化基础
通过序列化与反序列化我们可以很方便的在PHP中传递对象,下面小编给大家介绍反序列化的原理和一些常见的利用方式。
安全小王子
2021/02/24
1.3K0
CTF竞赛 | PHP反序列化基础
PHP反序列化漏洞
  在反序列化的过程中自动触发了某些魔术方法。未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致XSS、代码执行、文件写入、文件读取等不可控后果。
LuckySec
2022/11/02
5840
PHP反序列化漏洞
PHP反序列化深入理解
在PHP中右serialize()和unserialize()两个函数,php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。
V站CEO-西顾
2018/06/25
9731
反序列化漏洞理论实战详解
反序列化漏洞是基于序列化和反序列化的操作,在反序列化——unserialize()时存在用户可控参数,而反序列化会自动调用一些魔术方法,如果魔术方法内存在一些敏感操作例如eval()函数,而且参数是通过反序列化产生的,那么用户就可以通过改变参数来执行敏感操作,这就是反序列化漏洞。
litbaizhang
2021/03/11
2.9K1
反序列化漏洞理论实战详解
php反序列化漏洞
类的概念:类是具有相同属性和操作的一组对象的集合。它为属于该类的所有对象提供了统一的抽象描述, 其内部包括属性和操作两个主要部分。在面向对象的编程语言中,类是一个独立的程序单位, 它应该有一个类名并包括属性说明和操作说明两个主要部分。 简单点说类就是某一物件的模型 类的关键字 class 示例
宸寰客
2020/09/01
7750
php反序列化漏洞
PHP 魔术方法、序列化与对象复制
__construct()、__destruct()、__call()、__callStatic()、__get()、__set()、__isset()、__unset()、__sleep()、 __wakeup()、__toString()、__invoke()、__set_state()、__clone() 和 __debugInfo()。
学院君
2020/07/21
1.9K0
PHP 魔术方法、序列化与对象复制
几种反序列化漏洞
xxe.xml 和 xxe.dtd 构造见我的 XXE 文章,XXE XML外部实体注入(https://www.cnblogs.com/Night-Tac/articles/16931091.html)
红队蓝军
2023/09/13
4990
CTFshow之web入门反序列化
PHP反序列化实际上已经开始是Web安全的进阶操作了,虽然在这个时代Web选手上分极其困难,PHP反序列化已经成为了基础…..
十二惊惶
2024/02/28
3860
PHP反序列化漏洞
序列化就是将一个对象转换成字符串。字符串包括 属性名 属性值 属性类型和该对象对应的类名。
用户2700375
2022/06/09
5190
PHP反序列化漏洞
经验分享 | PHP-反序列化(超细的)
ps:很多小伙伴都催更了,先跟朋友们道个歉,摸鱼太久了,哈哈哈,今天就整理一下大家遇到比较多的php反序列化,经常在ctf中看到,还有就是审计的时候也会需要,这里我就细讲一下,我建议大家自己复制源码去搭建运行,只有自己去好好理解,好好利用了才更好的把握,才能更快的找出pop链子,首先呢反序列化最重要的就是那些常见的魔法函数,很多小伙伴都不知道这个魔法函数是干啥的,今天我就一个一个,细致的讲讲一些常见的魔法函数,以及最后拿一些ctf题举例,刚开始需要耐心的看,谢谢大家的关注,我会更努力的。
F12sec
2022/09/29
2.3K0
经验分享 | PHP-反序列化(超细的)
相关推荐
PHP反序列化漏洞
更多 >
目录
  • 前言
  • 如何序列化
  • 为什么可以序列化
  • 如何自定义序列化,Java基础类库中的ArrayList等为什么用transient还能序列化
    • 下面看看ArrayList是怎么做的
  • serialVersionUID为什么有的有,有的没有,什么时候用,意义是什么
    • 什么是serialVersionUID ?
    • 为什么要声明serialVersionUID
  • 注意事项
  • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档