java序列化实现RMI

来源:互联网 发布:2017通货膨胀 知乎 编辑:程序博客网 时间:2024/06/05 23:59

RMI(Remote Method Invocation)是Java中的远程过程调用(Remote Procedure Call,RPC)实现,是一种分布式Java应用的实现方式。它的目的在于对开发人员屏蔽横跨不同JVM和网络连接等细节,使得分布在不同JVM上的对象像是存在于一个统一的JVM中一样,可以很方便的互相通讯。通讯就涉及到了数据的编码和解码,对于一般的数据类型我们不需要这么做,但是涉及到比较复杂的数据类型,例如对象,RMI利用到了序列化,使得数据的编码与解码对于开发人员透明起来,我们不需要关注数据如何传输,只需要实现相关方法就能做到远程调用。

一、序列化

对象的序列化主要是使用JAVA中的ObjectOutputStream的writeObject(Object obj)来序列化和ObjectInputStream的readObject()来反序列化。
1、在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。当然目前大多数项目存储会员状态信息都会用到Memcached。
2、当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。此处RMI就利用了这点来帮我们编排对象数据信息。

总结就有如下优点:

  • 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中。
  • 便于在网络上传送对象的字节序列。
package rmiTest;import java.io.Serializable;public class Person implements Serializable{    private static final long serialVersionUID = -6379887343250921447L;    private int id;    private String name;    private String sex;    public Person(){}    public Person(int id, String name, String sex) {        super();        this.id = id;        this.name = name;        this.sex = sex;    }    //getXx();    //setXx();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
package serializeTest;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import rmiTest.Person;public class TestObjSerializeAndDeserialize {    public static void main(String[] args) {        Person person = new Person(1, "丁烁", "man");        try {            serialize(person, "E:/ds.txt");            deserialize("E:/ds.txt");        } catch (Exception e) {            e.printStackTrace();        }    }    //序列化    public static void serialize(Object object, String path) throws Exception{        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(path));        outputStream.writeObject(object);        outputStream.flush();        outputStream.close();        System.out.println("序列化成功!");    }    //反序列化    public static void deserialize(String path) throws Exception{        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(path));        Person person =(Person) inputStream.readObject();        inputStream.close();        System.out.println("反序列化成功!");        System.out.println("id:"+person.getId());        System.out.println("name:"+person.getName());        System.out.println("sex:"+person.getSex());    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

输出结果:
结果输出
文件生成:
生成的文件

注意事项:实体类上必须实现序列化接口,并且设定一个serialVersionUID。如果没有添加该字段,序列化对象之后,修改实体类再进行反序列化就会报错。java编译器会自动给这个class进行一个摘要算法来生成serialVersionUID,只要这个文件多一个空格,得到的serialVersionUID就会截然不同。所以修改之后java编译器又给我们生成了一个serialVersionUID,反序列化时两个版本号不一致就会报错。

二、jar命令

jar(Java archive file),这里为了更加真实的模拟远程接口调用,我们使用一个简单的面向接口编程,包含一个客户端和一个服务端。服务端包含了接口、实现以及实体类,客户端引入接口以及实体类的jar包,通过远程接口调用来调用接口的实现类。所以简要的梳理一下如何使用jar命令来进行打包相关类文件。当然在实际的项目中会使用maven来构建jar工程。

举例:
jar cvf d:\rmitest.jar D:\workspace\tjfae-v2300-20160410\appTest\bin\rmiTest
该命令会将rmiTest下的所有文件打包成jar并命名为rmitest.jar存放与d盘下。当然也可以指定多个目录、多个类文件。但是这样有一个弊端,我们打出的jar并不是按照我们所想的package目录打出来的。我们反编译一个jar如下图:
这里写图片描述
jar里面的类文件路径是按照我们打包时的路径所写,这样会导致我们在引用这个jar包时,报路径错误。明白了这点,我们可以将dos下的当前目录调整到与package对应位置,再执行打包命令。先将dos路径调整到D:\workspace\tjfae-v2300-20160410\appTest\bin下,然后执行如下命令:
jar cvf d:\rmitest.jar rmiTest
这里写图片描述

此时就已经打出了我们想要的jar包,为后面的远程接口调用做准备。

三、RMI远程调用实例

前面介绍了序列化和如何利用jar命令打包,下面的例子分为客户端和服务器端,服务器端包含了接口和接口的实现,并且将实现注册到一个服务地址上。然后用jar命令将服务器端的接口和实体类打包,在客户端引用,这样客户端只有接口,而不会看到具体实现。这里为了方便,将接口和实现放在同一个工程。但是更好方式是:

  1. 将接口写在一个工程A;
  2. 将工程A打成jar包;
  3. 在工程B中引入工程A的jar包,实现这些接口并注册服务,工程B即服务端;
  4. 在工程C中引入工程A的jar包,获取服务并注入给相应接口,工程C即客户端;
  5. 客户端远程调用。

本例中涉及到的类文件如下:
服务端:
Person:实体类,实现Serializable进行序列化;
PersonService:服务接口,继承Remote;
PersonServiceImp:服务实现,继承UnicastRemoteObject,并且实现PersonService;
ServerTest:注册PersonService这个服务;
客户端:
引入事先打好的jar包(Person.class、PersonService.class)
ClientTest:调用远程服务。

Person上面已经贴出。
PersonService包含一个getPerson的抽象方法。

package rmiTest;import java.rmi.Remote;import java.rmi.RemoteException;public interface PersonService extends Remote {    public Person getPerson() throws RemoteException;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

PersonServiceImp必须要继承UnicastRemoteObject,如果不继承这个类,那么客户端每次lookup出来的都是不一样的对象,可以通过在服务端添加一个成员变量,每调用一次自增一下来进行测试,这里不做多余讲解。

package rmiTest;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;public class PersonServiceImp extends UnicastRemoteObject implements PersonService {    private static final long serialVersionUID = 1L;    protected PersonServiceImp() throws RemoteException {        super();    }    @Override    public Person getPerson() throws RemoteException {        Person person = new Person();        person.setId(1);        person.setName("ds");        person.setSex("M");        return person;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

ServerTest服务端注册这个服务

package rmiTest;import java.rmi.Naming;import java.rmi.registry.LocateRegistry;public class ServerTest {    public static void main(String[] arg){        try {            PersonService personService = new PersonServiceImp();            LocateRegistry.createRegistry(6600);            Naming.bind("rmi://127.0.0.1:6600/PersonService", personService);        } catch (Exception e) {            e.printStackTrace();        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

ClientTest客户端进行远程调用,客户端要先引入事先打好的jar包

import java.rmi.Naming;import rmiTest.Person;import rmiTest.PersonService;public class ClientTest {    public static void main(String[] args) {        try {            PersonService personService = (PersonService) Naming.lookup("rmi://127.0.0.1:6600/PersonService");            Person person = personService.getPerson();            System.out.println("ID:"+person.getId());            System.out.println("Name:"+person.getName());            System.out.println("Sex:"+person.getSex());        } catch (Exception e) {            e.printStackTrace();        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
(function () {('pre.prettyprint code').each(function () { var lines = (this).text().split(\n).length;varnumbering = $('
    ').addClass('pre-numbering').hide(); (this).addClass(hasnumbering).parent().append(numbering); for (i = 1; i