transient的作用及序列化

共 6344字,需浏览 13分钟

 ·

2021-02-21 18:51

点击上方蓝色字体,选择“标星公众号”

优质文章,第一时间送达

  作者 |  沉淀所有的痛

来源 |  urlify.cn/Uba6Nb

76套java从入门到精通实战课程分享

1.transient 介绍

Java中的transient关键字,transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。

 

(1)序列化

      Java中对象的序列化指的是将对象转换成以字节序列的形式来表示,这些字节序列包含了对象的数据和信息,一个序列化后的对象可以被写到数据库或文件中,也可用于网络传输。一般地,当我们使用缓存cache(内存空间不够有可能会本地存储到硬盘)或远程调用rpc(网络传输)的时候,经常需要让实体类实现Serializable接口,目的就是为了让其可序列化。当然,序列化后的最终目的是为了反序列化,恢复成原先的Java对象实例。所以序列化后的字节序列都是可以恢复成Java对象的,这个过程就是反序列化。

 

(2)为什么要用transient关键字?

      在持久化对象时,对于一些特殊的数据成员(如用户的密码,银行卡号等),我们不想用序列化机制来保存它。为了在一个特定对象的一个成员变量上关闭序列化,可以在这个成员变量前加上关键字transient。

 

(3)transient的作用

      transient是Java语言的关键字,用来表示一个成员变量不是该对象序列化的一部分。当一个对象被序列化的时候,transient型变量的值不包括在序列化的结果中。而非transient型的变量是被包括进去的。   注意static修饰的静态变量天然就是不可序

 

 

 

 

 2. transient 使用总结

(1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法被访问。

 

(2) transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。

 

(3)一个静态变量不管是否被transient修饰,均不能被序列化(如果反序列化后类中static变量还有值,则值为当前JVM中对应static变量的值)。序列化保存的是对象状态,静态变量保存的是类状态,因此序列化并不保存静态变量。

 

3. 序列化及反序列化

  • 序列化
    是指将Java对象保存为二进制字节码的过程。

  • 反序列化
    将二进制字节码重新转成Java对象的过程。

(1)为什么序列化

  • 我们知道,一般Java对象的生命周期比Java虚拟机短,而实际的开发中,我们需要
    在Jvm停止后能够继续持有对象,这个时候就需要用到序列化技术将对象持久到磁盘或数据库。

  • 在多个项目进行RPC调用的,需要在网络上传输JavaBean对象。我们知道数据只能以二进制的
    形式才能在网络上进行传输。所以也需要用到序列化技术。

(2)序列化的底层原理

  1. 程序入口:

writeObject(obj)
Student  stu1 = new Student(1001, "jack""play");
Student  stu2 = new Student(1002, "tom""sleep");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:\\stu.dat"));
oos.writeObject(stu1);
oos.writeObject(stu2);
oos.close(); 
  1. 序列化

  • 在调用writeObject()方法之前,会先调用ObjectOutputStream的构造函数,生成
    一个ObjectOutputStream对象。

public ObjectOutputStream(OutputStream out) throws IOException {
    verifySubclass();
    // bout是底层的数据字节容器
    bout = new BlockDataOutputStream(out);
    handles = new HandleTable(10, (float) 3.00);
    subs = new ReplaceTable(10, (float) 3.00);
    enableOverride = false;
    // 写入序列化文件头
    writeStreamHeader();
    // 设置文件缓存刷新配置
    bout.setBlockDataMode(true);
    if (extendedDebugInfo) {
        debugInfoStack = new DebugTraceInfoStack();
    } else {
        debugInfoStack = null;
    }
}


  writeStreamHeader的方法内容如下:

protected void writeStreamHeader() throws IOException {
    bout.writeShort(STREAM_MAGIC);
    bout.writeShort(STREAM_MAGIC);
} 

  3. 调用writeObject()方法进行具体的序列化写入

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;
    }
} 
  • writeObject0的具体内容

private void writeObject0(Object obj, boolean unshared)
    throws IOException
{
    boolean oldMode = bout.setBlockDataMode(false);
    depth++;
    try {
        // handle previously written and non-replaceable objects
        int h;
        if ((obj = subs.lookup(obj)) == null) {
            writeNull();
            return;
        } else if (!unshared && (h = handles.lookup(obj)) != -1) {
            writeHandle(h);
            return;
        } else if (obj instanceof Class) {
            writeClass((Class) obj, unshared);
            return;
        } else if (obj instanceof ObjectStreamClass) {
            writeClassDesc((ObjectStreamClass) obj, unshared);
            return;
        }
 
        // check for replacement object
        Object orig = obj;
        // 需要序列的对象的Class对象
        Class cl = obj.getClass();
        ObjectStreamClass desc;
        for (;;) {
            // 提示:跳过检查string和数组
            // REMIND: skip this check for strings/arrays?
            Class repCl;
            // 创建描述c1的ObjectStreamClass对象
            desc = ObjectStreamClass.lookup(cl, true);
            if (!desc.hasWriteReplaceMethod() ||
                (obj = desc.invokeWriteReplace(obj)) == null ||
                (repCl = obj.getClass()) == cl)
            {
                break;
            }
            cl = repCl;
        }
        if (enableReplace) {
            Object rep = replaceObject(obj);
            if (rep != obj && rep != null) {
                cl = rep.getClass();
                desc = ObjectStreamClass.lookup(cl, true);
            }
            obj = rep;
        }
 
        // if object replaced, run through original checks a second time
        if (obj != orig) {
            subs.assign(orig, obj);
            if (obj == null) {
                writeNull();
                return;
            } else if (!unshared && (h = handles.lookup(obj)) != -1) {
                writeHandle(h);
                return;
            } else if (obj instanceof Class) {
                writeClass((Class) obj, unshared);
                return;
            } else if (obj instanceof ObjectStreamClass) {
                writeClassDesc((ObjectStreamClass) obj, unshared);
                return;
            }
        }
 
        // remaining cases
        // 根据实际要写入的类型,进行不同的写入操作
        // 由此可以看出String、Array、Enum是直接写入操作的
        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) {
            // 实现序列化接口的都会执行下面的方法
            // 从这里也可以看出Serializable是一个标记接口,其本身并没有什么意义
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
            } else {
                throw new NotSerializableException(cl.getName());
            }
        }
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}

从上面可以看出主要做了两件事

  • 创建了ObjectStreamClass对象

  • 根据实际要写入的类型,进行不同的写入操作

  writeOrdinaryObject()

  

为什么说序列化并不安全

  因为序列化的对象数据转换为二进制,并且完全可逆。但是在RMI调用时

  所有private字段的数据都以明文二进制的形式出现在网络的套接字上,这显然是不安全的

 

  解决方案:

  • 1、 序列化Hook化(移位和复位)

  • 2、 序列数据加密和签名

  • 3、 利用transient的特性解决

  • 4、 打包和解包代理

4. transient 在序列化底层的应用

static和transient修饰的字段不能被序列化。

private static ObjectStreamField[] getDefaultSerialFields(Class cl) {
        Field[] clFields = cl.getDeclaredFields();
        ArrayList list = new ArrayList<>();
        int mask = Modifier.STATIC | Modifier.TRANSIENT;
 
        for (int i = 0; i < clFields.length; i++) {
            if ((clFields[i].getModifiers() & mask) == 0) {
                list.add(new ObjectStreamField(clFields[i], falsetrue));
            }
        }
        int size = list.size();
        return (size == 0) ? NO_FIELDS :
            list.toArray(new ObjectStreamField[size]);
    }






粉丝福利:Java从入门到入土学习路线图

👇👇👇

👆长按上方微信二维码 2 秒


感谢点赞支持下哈 


浏览 39
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报