`
huxiaojun_198213
  • 浏览: 102287 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

Java深度历险(十)——Java对象序列化与RMI

    博客分类:
  • j2se
阅读更多
对于一个存在于Java虚拟机中的对象来说,其内部的状态只保持在内存中。JVM停止之后,这些状态就丢失了。在很多情况下,对象的内部状态是需要被持久化下来的。提到持久化,最直接的做法是保存到文件系统或是数据库之中。这种做法一般涉及到自定义存储格式以及繁琐的数据转换。对象关系映射(Object-relational mapping)是一种典型的用关系数据库来持久化对象的方式,也存在很多直接存储对象的对象数据库。对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。除了可以很简单的实现持久化之外,序列化机制的另外一个重要用途是在远程方法调用中,用来对开发人员屏蔽底层实现细节。

基本的对象序列化

由于Java提供了良好的默认支持,实现基本的对象序列化是件比较简单的事。待序列化的Java类只需要实现Serializable接口即可。Serializable仅是一个标记接口,并不包含任何需要实现的具体方法。实现该接口只是为了声明该Java类的对象是可以被序列化的。实际的序列化和反序列化工作是通过ObjectOuputStreamObjectInputStream来完成的。ObjectOutputStream的writeObject方法可以把一个Java对象写入到流中,ObjectInputStream的readObject方法可以从流中读取一个Java对象。在写入和读取的时候,虽然用的参数或返回值是单个对象,但实际上操纵的是一个对象图,包括该对象所引用的其它对象,以及这些对象所引用的另外的对象。Java会自动帮你遍历对象图并逐个序列化。除了对象之外,Java中的基本类型和数组也是可以通过 ObjectOutputStream和ObjectInputStream来序列化的。

try {
    User user = new User("Alex", "Cheng");
    ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("user.bin"));
    output.writeObject(user);
    output.close();
} catch (IOException e) {
    e.printStackTrace();
}
 
try {
    ObjectInputStream input = new ObjectInputStream(new FileInputStream("user.bin"));
    User user = (User) input.readObject();
    System.out.println(user);
} catch (Exception e) {
    e.printStackTrace();
} 



上面的代码给出了典型的把Java对象序列化之后保存到磁盘上,以及从磁盘上读取的基本方式。 User类只是声明了实现Serializable接口。

在默认的序列化实现中,Java对象中的非静态和非瞬时域都会被包括进来,而与域的可见性声明没有关系。这可能会导致某些不应该出现的域被包含在序列化之后的字节数组中,比如密码等隐私信息。由于Java对象序列化之后的格式是固定的,其它人可以很容易的从中分析出其中的各种信息。对于这种情况,一种解决办法是把域声明为瞬时的,即使用transient关键词。另外一种做法是添加一个serialPersistentFields? 域来声明序列化时要包含的域。从这里可以看到在Java序列化机制中的这种仅在书面层次上定义的契约。声明序列化的域必须使用固定的名称和类型。在后面还可以看到其它类似这样的契约。虽然Serializable只是一个标记接口,但它其实是包含有不少隐含的要求。下面的代码给出了 serialPersistentFields的声明示例,即只有firstName这个域是要被序列化的。

private static final ObjectStreamField[] serialPersistentFields = { 
    new ObjectStreamField("firstName", String.class) 
};  


自定义对象序列化

基本的对象序列化机制让开发人员可以在包含哪些域上进行定制。如果想对序列化的过程进行更加细粒度的控制,就需要在类中添加writeObject和对应的 readObject方法。这两个方法属于前面提到的序列化机制的隐含契约的一部分。在通过ObjectOutputStream的 writeObject方法写入对象的时候,如果这个对象的类中定义了writeObject方法,就会调用该方法,并把当前 ObjectOutputStream对象作为参数传递进去。writeObject方法中一般会包含自定义的序列化逻辑,比如在写入之前修改域的值,或是写入额外的数据等。对于writeObject中添加的逻辑,在对应的readObject中都需要反转过来,与之对应。

在添加自己的逻辑之前,推荐的做法是先调用Java的默认实现。在writeObject方法中通过ObjectOutputStream的defaultWriteObject来完成,在readObject方法则通过ObjectInputStream的defaultReadObject来实现。下面的代码在对象的序列化流中写入了一个额外的字符串。

private void writeObject(ObjectOutputStream output) throws IOException {
    output.defaultWriteObject();
    output.writeUTF("Hello World");
}
private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
    input.defaultReadObject();
    String value = input.readUTF();
    System.out.println(value);
}  



序列化时的对象替换

在有些情况下,可能会希望在序列化的时候使用另外一个对象来代替当前对象。其中的动机可能是当前对象中包含了一些不希望被序列化的域,比如这些域都是从另外一个域派生而来的;也可能是希望隐藏实际的类层次结构;还有可能是添加自定义的对象管理逻辑,如保证某个类在JVM中只有一个实例。相对于把无关的域都设成transient来说,使用对象替换是一个更好的选择,提供了更多的灵活性。替换对象的作用类似于Java EE中会使用到的传输对象(Transfer Object)。

考虑下面的例子,一个订单系统中需要把订单的相关信息序列化之后,通过网络来传输。订单类Order引用了客户类Customer。在默认序列化的情况下,Order类对象被序列化的时候,其引用的Customer类对象也会被序列化,这可能会造成用户信息的泄露。对于这种情况,可以创建一个另外的对象来在序列化的时候替换当前的Order类的对象,并把用户信息隐藏起来。

private static class OrderReplace implements Serializable {
    private static final long serialVersionUID = 4654546423735192613L;
    private String orderId;
    public OrderReplace(Order order) {
        this.orderId = order.getId();
    }
    private Object readResolve() throws ObjectStreamException {
        //根据orderId查找Order对象并返回
    }
}    


这个替换对象类OrderReplace只保存了Order的ID。在Order类的writeReplace方法中返回了一个OrderReplace对象。这个对象会被作为替代写入到流中。同样的,需要在OrderReplace类中定义一个readResolve方法,用来在读取的时候再转换回 Order类对象。这样对调用者来说,替换对象的存在就是透明的。

private Object writeReplace() throws ObjectStreamException {
    return new OrderReplace(this);
} 


序列化与对象创建

在通过ObjectInputStream的readObject方法读取到一个对象之后,这个对象是一个新的实例,但是其构造方法是没有被调用的,其中的域的初始化代码也没有被执行。对于那些没有被序列化的域,在新创建出来的对象中的值都是默认的。也就是说,这个对象从某种角度上来说是不完备的。这有可能会造成一些隐含的错误。调用者并不知道对象是通过一般的new操作符来创建的,还是通过反序列化所得到的。解决的办法就是在类的readObject方法里面,再执行所需的对象初始化逻辑。对于一般的Java类来说,构造方法中包含了初始化的逻辑。可以把这些逻辑提取到一个方法中,在readObject方法中调用此方法。

版本更新

把一个Java对象序列化之后,所得到的字节数组一般会保存在磁盘或数据库之中。在保存完成之后,有可能原来的Java类有了更新,比如添加了额外的域。这个时候从兼容性的角度出发,要求仍然能够读取旧版本的序列化数据。在读取的过程中,当ObjectInputStream发现一个对象的定义的时候,会尝试在当前JVM中查找其Java类定义。这个查找过程不能仅根据Java类的全名来判断,因为当前JVM中可能存在名称相同,但是含义完全不同的Java 类。这个对应关系是通过一个全局惟一标识符serialVersionUID来实现的。通过在实现了Serializable接口的类中定义该域,就声明了该Java类的一个惟一的序列化版本号。JVM会比对从字节数组中得出的类的版本号,与JVM中查找到的类的版本号是否一致,来决定两个类是否是兼容的。对于开发人员来说,需要记得的就是在实现了Serializable接口的类中定义这样的一个域,并在版本更新过程中保持该值不变。当然,如果不希望维持这种向后兼容性,换一个版本号即可。该域的值一般是综合Java类的各个特性而计算出来的一个哈希值,可以通过Java提供的serialver命令来生成。在Eclipse中,如果Java类实现了Serializable接口,Eclipse会提示并帮你生成这个serialVersionUID。

在类版本更新的过程中,某些操作会破坏向后兼容性。如果希望维持这种向后兼容性,就需要格外的注意。一般来说,在新的版本中添加东西不会产生什么问题,而去掉一些域则是不行的。

序列化安全性

前面提到,Java对象序列化之后的内容格式是公开的。所以可以很容易的从中提取出各种信息。从实现的角度来说,可以从不同的层次来加强序列化的安全性。

•对序列化之后的流进行加密。这可以通过CipherOutputStream来实现。
•实现自己的writeObject和readObject方法,在调用defaultWriteObject之前,先对要序列化的域的值进行加密处理。
•使用一个SignedObjectSealedObject来封装当前对象,用SignedObject或SealedObject进行序列化。
•在从流中进行反序列化的时候,可以通过ObjectInputStream的registerValidation方法添加ObjectInputValidation接口的实现,用来验证反序列化之后得到的对象是否合法。

RMI

RMI(Remote Method Invocation)是Java中的远程过程调用(Remote Procedure Call,RPC)实现,是一种分布式Java应用的实现方式。它的目的在于对开发人员屏蔽横跨不同JVM和网络连接等细节,使得分布在不同JVM上的对象像是存在于一个统一的JVM中一样,可以很方便的互相通讯。之所以在介绍对象序列化之后来介绍RMI,主要是因为对象序列化机制使得RMI非常简单。调用一个远程服务器上的方法并不是一件困难的事情。开发人员可以基于Apache MINA或是Netty这样的框架来写自己的网络服务器,亦或是可以采用REST架构风格来编写HTTP服务。但这些解决方案中,不可回避的一个部分就是数据的编排和解排(marshal/unmarshal)。需要在Java对象和传输格式之间进行互相转换,而且这一部分逻辑是开发人员无法回避的。RMI的优势在于依靠Java序列化机制,对开发人员屏蔽了数据编排和解排的细节,要做的事情非常少。JDK 5之后,RMI通过动态代理机制去掉了早期版本中需要通过工具进行代码生成的繁琐方式,使用起来更加简单。

RMI采用的是典型的客户端-服务器端架构。首先需要定义的是服务器端的远程接口,这一步是设计好服务器端需要提供什么样的服务。对远程接口的要求很简单,只需要继承自RMI中的Remote接口即可。Remote和Serializable一样,也是标记接口。远程接口中的方法需要抛出RemoteException。定义好远程接口之后,实现该接口即可。如下面的Calculator是一个简单的远程接口。
public interface Calculator extends Remote {
    String calculate(String expr) throws RemoteException;
}   


实现了远程接口的类的实例称为远程对象。创建出远程对象之后,需要把它注册到一个注册表之中。这是为了客户端能够找到该远程对象并调用。
 public class CalculatorServer implements Calculator {
    public String calculate(String expr) throws RemoteException {
        return expr;
    }
    public void start() throws RemoteException, AlreadyBoundException {
        Calculator stub = (Calculator) UnicastRemoteObject.exportObject(this, 0);
        Registry registry = LocateRegistry.getRegistry();
        registry.rebind("Calculator", stub);
    }
}


CalculatorServer是远程对象的Java类。在它的start方法中通过UnicastRemoteObjectexportObject把当前对象暴露出来,使得它可以接收来自客户端的调用请求。再通过Registryrebind方法进行注册,使得客户端可以查找到。

客户端的实现就是首先从注册表中查找到远程接口的实现对象,再调用相应的方法即可。实际的调用虽然是在服务器端完成的,但是在客户端看来,这个接口中的方法就好像是在当前JVM中一样。这就是RMI的强大之处。

 public class CalculatorClient {
    public void calculate(String expr) {
        try {
            Registry registry = LocateRegistry.getRegistry("localhost");
            Calculator calculator = (Calculator) registry.lookup("Calculator");
            String result = calculator.calculate(expr);
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
} 


在运行的时候,需要首先通过rmiregistry命令来启动RMI中用到的注册表服务器。

为了通过Java的序列化机制来进行传输,远程接口中的方法的参数和返回值,要么是Java的基本类型,要么是远程对象,要么是实现了 Serializable接口的Java类。当客户端通过RMI注册表找到一个远程接口的时候,所得到的其实是远程接口的一个动态代理对象。当客户端调用其中的方法的时候,方法的参数对象会在序列化之后,传输到服务器端。服务器端接收到之后,进行反序列化得到参数对象。并使用这些参数对象,在服务器端调用实际的方法。调用的返回值Java对象经过序列化之后,再发送回客户端。客户端再经过反序列化之后得到Java对象,返回给调用者。这中间的序列化过程对于使用者来说是透明的,由动态代理对象自动完成。除了序列化之外,RMI还使用了动态类加载技术。当需要进行反序列化的时候,如果该对象的类定义在当前JVM中没有找到,RMI会尝试从远端下载所需的类文件定义。可以在RMI程序启动的时候,通过JVM参数java.rmi.server.codebase来指定动态下载Java类文件的URL。 

参考资料
Java对象序列化规范
RMI规范




以上转载自:http://www.infoq.com/cn/articles/cf-java-object-serialization-rmi/
分享到:
评论

相关推荐

    java 对象的序列化与反序列化

    Java对象的序列化和反序列化是Java编程中一项重要的技术,主要用于将对象的状态转换为字节流,以便存储或在网络上传输。这一过程对于理解Java的IO操作、持久化数据以及实现分布式通信等场景非常关键。 首先,我们来...

    javaRMI反序列化漏洞验证工具

    Java RMI的反序列化漏洞主要源于不正确的类型检查和信任机制。当接收到未经验证的远程调用时,如果服务器没有正确地校验或限制输入数据,攻击者可以通过构造恶意序列化数据来操控远程对象的行为。这种漏洞可能允许...

    关于 Java 对象序列化您不知道的 5 件事

    Java对象序列化不仅用于持久化对象状态,还能在网络传输中扮演关键角色,例如在RMI(远程方法调用)中。此外,它还可以作为缓存和备份数据的一种手段。通过实现`java.io.Serializable`接口,一个类的实例就能被序列...

    JAVA对象的序列化与反序列化详细PPT课件.pptx

    前者代表对象输出流,具备`writeObject(Object obj)`方法,可以将对象序列化并写入到目标输出流;后者代表对象输入流,其`readObject()`方法能从输入流中读取字节流并反序列化为对象。 为了使一个Java对象能够被...

    Java对象序列化的秘密

    Java对象序列化是Java平台提供的一种机制,允许将对象的状态转换为字节流,以便存储在磁盘上、通过网络传输或在不同时间点恢复。这个过程涉及到将一个复杂的Java对象模型转换为简单的二进制表示,使得数据可以在不同...

    java反序列化工具

    Java反序列化是一种将已序列化的对象状态转换回对象的过程,它是Java平台中持久化数据的一种常见方式。在Java应用程序中,序列化用于保存对象的状态以便稍后恢复,或者在网络间传输对象。然而,这个过程也可能引入...

    介绍Java对象序列化使用基础

    序列化的过程就是对象写入字节流和从字节流中读取对象。...对象序列化功能非常简单、强大,在RMI、Socket、JMS、EJB都有应用。对象序列化问题在网络编程中并不是最激动人心的课题,但却相当重要,具有许多实用意义。

    Java对象的序列化与反序列化Java开发Java经验技巧

    Java对象的序列化与反序列化是Java编程中一项重要的技术,它允许我们将Java对象转换为字节流,便于存储、传输或者在网络间传递。这一过程对于数据持久化、跨进程通信(如RMI,Remote Method Invocation)以及分布式...

    java深度历险 InfoQ中文站

    java深度历险 InfoQ中文站 1、java字节码操作 2、java类的加载、链接和初始化 3、java线程 4、java垃圾回收机制与引用类型 5、java泛型 6、java注解 7、java反射与动态代理 8、javaI/O 9、java安全 10、java对象序列...

    java序列化(Serializable)的作用和反序列化.doc

    ### Java序列化(Serializable)的作用与反序列化详解 #### 一、序列化的概念 序列化是指将程序中的对象转换为一系列字节流的过程,主要用于保存对象的状态或在网络之间传输对象。序列化的主要目的是为了能够持久化...

    java_rmi.rar_RMI java_java.rmi

    6. **反序列化和序列化**:RMI使用Java的序列化机制来处理参数和返回值的网络传输。所有远程方法的参数和返回值都必须是可序列化的。 Java RMI还有许多高级特性,例如: - **多线程RMI**:RMI支持并发调用,多个...

    Java序列化

    Java序列化是Java平台中的一种标准机制,允许将对象的状态转换为字节流,以便存储在磁盘上、通过网络进行传输或者在某些时候恢复原来的对象状态。这一过程包括两个主要步骤:对象的序列化(将对象转换为字节流)和反...

    146-从Java RMI反序列化到内网沦陷.pdf

    CVE-2017-3241 漏洞存在于Java RMI框架中,当服务端对客户端发送的数据进行反序列化处理时,如果对数据来源没有严格检查,攻击者可以构造恶意序列化数据包,通过RMI服务在服务端执行任意代码,导致远程代码执行(RCE...

    java RMI实现代码

    Java RMI(Remote Method Invocation,远程方法调用)是Java平台提供的一种分布式计算技术,它允许在不同的Java虚拟机之间进行远程对象的调用。RMI使得开发者可以像调用本地对象一样调用网络上的对象,极大地简化了...

    关于Java对象序列化您不知道的5件事Java开发Java

    Java对象序列化是Java开发中的一个重要概念,它允许我们将Java对象转换为字节流,以便存储、传输或在不同时间点恢复。以下是五个可能不为人知的关于Java对象序列化的知识点,这些知识点对于深入理解Java开发至关重要...

    java rmi java rmi

    根据提供的文件信息,我们可以深入探讨Java RMI(Java Remote Method Invocation)的相关知识点,包括其概念、原理、体系结构以及一个具体的示例。 ### RMI的概念 RMI是一种Java技术,它允许开发者创建分布式应用...

    RMI.rar_Java RMI_java.rmi_java.rmi.Remot_remote

    Java RMI的实现基于Java的序列化机制,这意味着远程方法的参数和返回值都必须是可序列化的。Java RMI还提供了“Stub”和“Skeleton”的概念,Stub作为远程对象在客户端的代理,而Skeleton则在服务器端处理远程方法...

    07-Java序列化面试题(10题)-新增.pdf

    除了在持久化对象时会用到对象序列化之外,当使用 RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。Java 序列化 API 为处理对象序列化提供了一个标准机制,该 API 简单易用。 5. Serializable ...

Global site tag (gtag.js) - Google Analytics