• ## 说在前面

入门了反序列化之后对RMI、JNDI、LDAP、JRMP、JMX、JMS这些都不了解,所以打算一个问题一个问题的解决它们,这这篇专注于RMI的学习,从RPC到RMI的反序列化再到JEP290都过了一遍。参考了很多很多师傅的文章,如果有写的不对的地方还望师傅们不吝赐教。

RMI 基础

RPC

RPC(Remote Procedure Call)远程过程调用,就是要像调用本地的函数一样去调远程函数。它并不是某一个具体的框架,而是实现了远程过程调用的都可以称之为RPC。比如RMI(Remote Method Invoke 远程方法调用)就是一个实现了RPC的JAVA框架。

RPC的演化过程可以看这个视频进行了解:https://www.bilibili.com/video/BV1zE41147Zq

对于视频里面实现RPC的方式我画了一个简单的流程图来理解:

Client如果想要远程调用一个方法,就需要通过一个Stub类传递类名、方法名与参数信息给Server端,Server端获取到这些信息后会从本地服务器注册表中找到具体的类,再通过反射获取到一个具体的方法并执行然后返回结果。

JAVA 代理

代理模式

代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

ProxyClient直接调用DoAction()中间加了一层处理,正是这层处理扩展了对象的功能。

静态代理

这种代理方式需要代理对象和目标对象实现一样的接口。

例子如下:

  • 接口类:IUserDao.java
package proxy1;

public interface IUserDao {
    public void save();
}
  • 目标对象:UserDao.java
package proxy1;
// 实现IUserDao接口
public class UserDao implements IUserDao{
    @Override
    public void save() {
        System.out.println("保存数据");
    }
}
  • 静态代理对象:UserDapProxy.java
package proxy1;
// 也需要实现IUserDao接口
public class UserDapProxy implements IUserDao{
    private IUserDao target;

    public UserDapProxy(IUserDao target) {
        this.target = target;
    }

    @Override
    public void save() { // 重写方法
        System.out.println("doSomething before"); // 执行前可以加的操作
        target.save(); // 实际上需要调用的方法
        System.out.println("doSomething after"); // 执行后可以加的操作
    }
}
  • 测试类:TestProxy.java
package proxy1;

public class TestProxy {
    public static void main(String[] args) {
        // 目标对象
        IUserDao target = new UserDao();
        // 代理对象
        UserDapProxy proxy = new UserDapProxy(target);
        // 通过代理调用方法
        proxy.save();
    }
}

可以看到,在不修改原来对象功能的前提下,在调用方法前后增加了功能。但是这种代理模式有很一些缺点:

  1. 冗余。由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。
  2. 不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改。

动态代理

动态代理利用JAVA中的反射,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。

  • 动态代理对象:UserProxyFactory.java
package proxy1;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class UserProxyFactory {
    private Object target;

    public UserProxyFactory(Object target) {
        this.target = target;
    }

    public Object getProxyInstance() {
        // 返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 指定当前目标对象使用类加载器
                target.getClass().getInterfaces(), // 目标对象实现的接口的类型
                new InvocationHandler() { // 事件处理器
                    @Override // 重写InvocationHandler累类的invoke方法,通过反射调用方法
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("doSomething before");
                        Object returnValue = method.invoke(target, args);
                        System.out.println("doSomething after");
                        return null;
                    }
                }
        );
    }
}
  • 测试类:TestDynamicProxy.java
package proxy1;

public class TestDynamicProxy {
    public static void main(String[] args) {
        IUserDao taget = new UserDao();
        System.out.println(taget.getClass()); // 获取目标对象信息
        IUserDao proxy = (IUserDao) new UserProxyFactory(taget).getProxyInstance(); // 获取代理类 
        System.out.println(proxy.getClass()); // 获取代理对象信息
        proxy.save(); // 执行代理方法
    }
}

参考文章

JAVA RMI

定义:

RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

Java RMI:Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。

JAVA中RMI的简单例子:

Server端

定义一个远程接口:User.java

package eval_rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface User extends Remote {
    String name(String name) throws RemoteException;
    void say(String say) throws RemoteException;
    void dowork(Object work) throws RemoteException;
}

在Java中,只要一个类extends了java.rmi.Remote接口,即可成为存在于服务器端的远程对象。其他接口中的方法若是声明抛出了RemoteException异常,则表明该方法可被客户端远程访问调用。

JavaDoc描述:Remote 接口用于标识其方法可以从非本地虚拟机上调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口” (扩展 java.rmi.Remote 的接口)中指定的这些方法才可被远程调用。

远程接口实现类:UserImpl.java

package eval_rmi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

// java.rmi.server.UnicastRemoteObject构造函数中将生成stub和skeleton
public class UserImpl extends UnicastRemoteObject implements User{
    // 必须有一个显式的构造函数,并且要抛出一个RemoteException异常
    public UserImpl() throws RemoteException{
        super();
    }
    @Override
    public String name(String name) throws RemoteException{
        return name;
    }
    @Override
    public void say(String say) throws  RemoteException{
        System.out.println("you speak" + say);
    }
    @Override
    public void dowork(Object work) throws  RemoteException{
        System.out.println("your work is " + work);
    }
}

远程对象必须继承java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”,而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理(Stub),用于与服务器端的通信,而骨架也可认为是服务器端的一个代理(skeleton),用于接收客户端的请求之后调用远程方法来响应客户端的请求。

这个Stub和RPC同理,Skeleton可以理解为是服务端的Stub。

服务端实现类:UserServer.java

package eval_rmi;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class UserServer {
    public static void main(String[] args) throws Exception{
        String url = "rmi://127.0.0.1:3333/User";
        User user = new UserImpl(); // 生成stub和skeleton,并返回stub代理引用
        LocateRegistry.createRegistry(3333); // 本地创建并启动RMI Service,被创建的Registry服务将在指定的端口上监听并接受请求
        Naming.bind(url, user); // 将stub代理绑定到Registry服务的URL上
        System.out.println("the rmi is running : " + url);
    }
}

这个类的作用就是注册远程对象,向客户端提供远程对象服务。将远程对象注册到RMI Service之后,客户端就可以通过RMI Service请求到该远程服务对象的stub了,利用stub代理就可以访问远程服务对象了。

Naming类的介绍:

/** Naming 类提供在对象注册表中存储和获得远程对远程对象引用的方法 
 *  Naming 类的每个方法都可将某个名称作为其一个参数, 
 *  该名称是使用以下形式的 URL 格式(没有 scheme 组件)的 java.lang.String: 
 *  //host:port/name 
 *  host:注册表所在的主机(远程或本地),省略则默认为本地主机 
 *  port:是注册表接受调用的端口号,省略则默认为1099,RMI注册表registry使用的著名端口 
 *  name:是未经注册表解释的简单字符串 
 */  
//Naming.bind("//host:port/name", h);

Client端

UserClient.java

package eval_rmi;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class UserClient {
    public static void main(String[] args) throws Exception{
        String url = "rmi://127.0.0.1:3333/User";
        User userClient = (User)Naming.lookup(url); // 从RMI Registry中请求stub
        System.out.println(userClient.name("test")); // 通过stub调用远程接口实现
        userClient.say("world"); // 在客户端中调用,在服务端输出
    }
}

RMI测试

先启动UserServer.java,再启动UserClient.java

同时在服务端:

直接使用Registry实现的RMI

除了使用Naming的方式注册RMI之外,还可以直接使用Registry实现。代码如下:

Server端:

package eval_rmi;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class UserServer {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(3333); // 本地主机上的远程对象注册表Registry的实例
        User user = new UserImpl(); // 创建一个远程对象
        registry.rebind("HelloRegistry", user); // 把远程对象注册到RMI注册服务器上,并命名为HelloRegistr
        System.out.println("rmi start at 3333");
    }
}

Client端:

package eval_rmi;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class UserClient {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.getRegistry(3333); // 获取注册表
        User userClient = (User) registry.lookup("HelloRegistry"); // 获取命名为HelloRegistr的远程对象的stub
        System.out.println(userClient.name("test")); 
        userClient.say("world");
    }
}

总结

根据RMI的整个过程画出一个的流程图如下:

JRMP

Java远程方法协议(英语:Java Remote Method Protocol,JRMP)是特定于Java技术的、用于查找和引用远程对象的协议。这是运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议(英语:Wire protocol)。

简单理解就是:JRMP是一个协议,是用于Java RMI过程中的协议,只有使用这个协议,方法调用双方才能正常的进行数据交流。

文章在Bypass JEP290部分有提到JRMP端,是指实现了JRMP接收、处理和发送请求过程的服务。

点击收藏 | 5 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖