编者注:本文为历史博文归档;涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效,阅读时请注意时效性。

引言

日前在论坛精华区查阅到 Robbin 关于 EJB 调用原理的分析文章,受益匪浅。不过原文主要以文字阐述为主,直观性稍显不足,且对 RMI(Remote Method Invocation,远程方法调用)的讲解篇幅有限。鉴于 EJB 底层基于 RMI 实现,本文将在原帖基础上,结合个人体会补充一些图解与代码示例,供大家参考。

RMI 工作原理

既然 EJB 基于 RMI,我们首先需理清 RMI 的工作机制。简而言之,RMI 的本质是实现不同 JVM(Java Virtual Machine,Java 虚拟机)之间的方法调用。其工作原理如下图所示:

实现的核心在于分别在两个 JVM 中创建 Stub(客户端桩)和 Skeleton(服务端骨架),二者通过 Socket 通信来完成参数与返回值的传递。

代码示例

网上关于 RMI 的示例代码大多通过继承 java.rmi.Remote 接口实现,封装较为完善,但也容易让人产生“雾里看花”的感觉,难以窥见底层细节。以下示例参考自《Enterprise JavaBeans》一书,虽然实现较为粗糙,但胜在直观,有助于快速理解其工作原理。

1. 定义业务接口

首先定义一个 Person 接口,包含两个业务方法:getAge()getName()

public interface Person {
    public int getAge() throws Throwable;
    public String getName() throws Throwable;
}

2. 服务端实现

编写 Person 接口的服务端实现类 PersonServer

public class PersonServer implements Person {
    int age;
    String name;

    public PersonServer(String name, int age) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

3. 客户端 Stub 实现

若要在 Client 端调用上述业务方法,需编写相应的 Stub(客户端)和 Skeleton(服务端)程序。以下是 Stub 的实现逻辑:

import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.net.Socket;

public class Person_Stub implements Person {
    Socket socket;

    public Person_Stub() throws Throwable {
        // 连接 Skeleton
        socket = new Socket("computer_name", 9000);
    }

    public int getAge() throws Throwable {
        // 向 Skeleton 传递方法名
        ObjectOutputStream outStream =
            new ObjectOutputStream(socket.getOutputStream());
        outStream.writeObject("age");
        outStream.flush();

        ObjectInputStream inStream =
            new ObjectInputStream(socket.getInputStream());
        return inStream.readInt();
    }

    public String getName() throws Throwable {
        // 向 Skeleton 传递方法名
        ObjectOutputStream outStream =
            new ObjectOutputStream(socket.getOutputStream());
        outStream.writeObject("name");
        outStream.flush();

        ObjectInputStream inStream =
            new ObjectInputStream(socket.getInputStream());
        return (String) inStream.readObject();
    }
}

注意,Person_StubPersonServer 一样,都实现了 Person 接口。区别在于:PersonServer 是真实的业务逻辑实现,而 Person_Stub 负责建立 Socket 连接,向 Skeleton 发送请求,并通过 Skeleton 调用 PersonServer 的方法,最后接收返回结果。

4. 服务端 Skeleton 实现

Skeleton 类继承自 Thread,长驻后台运行,随时接收 Client 发来的请求,并根据请求键值调用相应的业务方法。

import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.net.Socket;
import java.net.ServerSocket;

public class Person_Skeleton extends Thread {
    PersonServer myServer;

    public Person_Skeleton(PersonServer server) {
        // 获取服务对象引用
        this.myServer = server;
    }

    public void run() {
        try {
            // 在 9000 端口新建 Socket
            ServerSocket serverSocket = new ServerSocket(9000);
            // 接受 Stub 的请求
            Socket socket = serverSocket.accept();

            while (socket != null) {
                // 获取 Stub 的请求
                ObjectInputStream inStream =
                    new ObjectInputStream(socket.getInputStream());
                String method = (String) inStream.readObject();

                // 检查方法名
                if (method.equals("age")) {
                    // 执行服务端的业务方法
                    int age = myServer.getAge();
                    ObjectOutputStream outStream =
                        new ObjectOutputStream(socket.getOutputStream());

                    // 返回结果给 Stub
                    outStream.writeInt(age);
                    outStream.flush();
                }

                if (method.equals("name")) {
                    // 执行服务端的业务方法
                    String name = myServer.getName();
                    ObjectOutputStream outStream =
                        new ObjectOutputStream(socket.getOutputStream());

                    // 返回结果给 Stub
                    outStream.writeObject(name);
                    outStream.flush();
                }
            }
        } catch (Throwable t) {
            t.printStackTrace();
            System.exit(0);
        }
    }

    public static void main(String[] args) {
        // 新建服务对象
        PersonServer person = new PersonServer("Richard", 34);

        Person_Skeleton skel = new Person_Skeleton(person);
        skel.start();
    }
}

5. 客户端调用实现

最后是实现 Client 端的调用逻辑。

public class PersonClient {
    public static void main(String[] args) {
        try {
            Person person = new Person_Stub();
            int age = person.getAge();
            String name = person.getName();
            System.out.println(name + " is " + age + " years old");
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

Client 的本质是知晓 Person 接口的定义,并实例化一个 Person_Stub,通过 Stub 调用业务方法。至于 Stub 如何与 Server 沟通,Client 无需关心。

请注意实例化的写法:

Person person = new Person_Stub();

而不是:

Person_Stub person = new Person_Stub();

原因在于遵循面向接口编程的原则。

结语

感谢您耐心阅读至此。关于 RMI 的核心机制,本文就先介绍到这里。原本计划接着深入探讨 EJB 的具体工作原理,但篇幅所限,今日先到这里,后续再继续补充。

说明:本文内容基于早期 JDK 版本(JDK 1.4/5 时代)及 EJB 2.x 规范编写。现代 JDK(Java 5+)已不再需要手动生成 Stub/Skeleton 代码,RMI 机制已高度自动化;同时 EJB 3.0 及以上版本已采用注解驱动,与文中涉及的底层机制有较大差异。阅读时请注意技术栈的时效性。