Java远程方法调用,即Java RMI(Java Remote Method Invocation),是Java编程语言里一种用于实现远程过程调用的应用程序编程接口。它是客户机上运行的程序能够在网络环境中分布操作。RMI全部的宗旨就是尽可能的简化远程接口对象的使用。
Java RMI极大的依赖于接口,在需要创建一个远程对象时,程序员通过传递一个接口来隐藏底层的实现细节。客户端得到的远程对象句柄正好与本地的根代码连接,由后者负责透过网络通信。这样依赖,程序员只需要关心如何通过自己的接口句柄发送消息。

12.1 RMI

在Spring中,同样提供了对RMI的支持,使得在Spring下的RMI开发变得更方便。实现方法如下:

1. 建立RMI对外接口;
2. 建立接口实现类;
3. 建立服务端配置文件;
4. 建立服务端测试;
5. 完成服务端配置后,还需要在测试端建立测试环境以及测试代码;
6. 编写测试代码。

12.1.1 服务端实现

Spring中的核心还是配置文件,这是所有功能的基础。在服务端的配置文件中我们可以看到,定义了两个bean,其中一个是对接口实现类的发布,而另一个是对RMI服务的发布,使用RMIServiceExporter类进行封装,其中包括了服务类,服务名,服务接口,服务端口等若干属性,因此RMIServiceExporter类应该是发布RMI的关键类。

根据RMIServiceExporter源码分析,该类实现了Spring中几个比较敏感的接口:
1. BeanClassLoaderAware:保证在实现该接口的bean的初始化时调用其setBeanClassLoader方法;
2. DisposableBean:保证在实现该接口的bean销毁时调用其destroy方法;
3. InitializingBean:保证实现该接口的bean初始化时调用其afterPropertiesSet方法
所以RMIServiceExporter的初始化入口一定在其setBeanClassLoader或者afterPropertiesSet方法中。

由源码可知afterPropertiesSet为RMIServiceExporter功能的初始化入口。在afterPropertiesSet函数中将实现委托给了prepare,而在prepare方法中我们找到了RMI服务发布的功能实现,同时也大致清楚了RMI服务发布的流程。

1. 验证service:此处的service对应的配置文件中类型为RMIServiceExporter的service属性,它是实现类,并不是接口,尽管后期会对RMIServiceExporter进行一系列的封装,但是无论怎么封装,最终还是会将逻辑引向RMIServiceExporter来处理,所以发布之前需要进行验证。
2. 处理用户自定义SocketFactory属性:在RMIServiceExporter中提供了4中套接字工厂配置,分别是clientSocketFactory、serverSocketFactory(用于创建服务端与RMI连接,serverSocketFactory用于在服务端建立套接字等待客户端连接;clientSocketFactory用于调用端建立套接字发起连接)和registerClientSocketFactory、registerServerSocketFactory(用于主机和RMI服务器之间连接的创建)
3. 根据配置参数获取Register。
4. 构建对外发布的实例,构建对外发布的实例,当外界通过注册的服务名调用响应的方法时,RMI服务会将请求因如此类来处理。
5. 发布实例。
12.1.1.1 获取registry

由于底层的封装,获取Rgistry实例非常简单,只需要使用LocateRegistry.createRegistry(…)创建Registry实例就可以了。但是,Spring没有这么做而是考虑的更多,比如,RMI注册主机和发布的服务并不在一台机器上,那么就需要LocateRegistry.getRegistry(…)去远程获取Registry实例。

如果并不是从另外的服务器上获取Registry连接,那么就需要在本地创建RMI的Registry实例了。当然这里有一个关键参数alwaysCreateRegistry,如果此参数配置为true,如果此参数设置为true,那么在获取Registry实例时首先会检测是否已建立对指定端口的连接,如果已经建立则复用已创建的实例,否则重新创建。

当然,之前也提到过,常见Registry实例时可以使用自定义连接工厂,而之前的判断也保证了clientSocketFactory和serverSocketFactory要么同时出现,要么同时不出现,所以这只对clientSocketFactory是否为空进行了判断。

如果创建Registry实例时不需要使用自定义套接字工厂时,那么就可以直接使用LocateRegistry.createRegistry(…)方法来创建,当然复用的检测还是必要的。

12.1.1.2 初始化导出的实体对象

之前提到过,当请求某个RMI服务的时候,RMI会根据注册的服务名称,将请求引导至远程对象处理类中,这个处理类便是使用getObjectToExport()进行创建。

请求处理类的初始化主要处理规则为:如果配置的service属性对应的类实现了Remote接口且没有配置serviceInterface属性,那么直接使用service作为处理类;否则使用RMIInvocationWrapper对service的代理类和当前类也就是RMIServiceExporter进行封装。

经过这样的处理,客户端和服务端便可以达成一致协议,当客户端检测到是RMIInvocationWrapper类型的stub的时候便直接调用其invoke方法,使调用端与服务端很好的连接在一起。而RMIInvocationWrapper封装了用于处理请求的代理类,在invoke中便会使用代理类进行进一步处理。

之前的逻辑已经非常清楚了,当请求RMI服务时会由注册表Registry实例将请求转向之前注册的处理类去处理,也就是之前封装的RMIInvocationWrapper,然后有RMIInvocationWrapper中的invoke方法进行处理。 那么为什么不是在invoke方法中直接使用service,而是通过代理再次将service封装呢?

这其中的一个关键点是,在创建代理时添加了一个增强拦截器RemoteInvocationTraceIntercepter,目的是为了对方法调用进行打印跟踪,如果直接在invoke方法中硬编码这些日志,会使代码看起来不优雅,而且耦合度很高,使用代理的方法就会解决这样的问题,而且会有很高的扩展性。

12.1.1.3 RMI服务激活调用

之前反复提到过,由于在之前bean初始化的时候做了服务名称绑定this.Registry.bind(this.serviceName, this.exportObject),其中exportObject其实是被RMIInvocationWrapper进行过封装的,也就是说当其他服务器调用serviceName的RMI服务时,Java会为我们封装其内部操作,而直接会将代码转向RMIInvocationWrapper的invoke方法中。

RMIExporter.invoke方法的实现原理:
1. 根据方法名获取代理中对应的方法;
2. 执行代理中的方法。

12.1.2 客户端实现

根据客户端配置文件,锁定入口类为RMIProxyFactoryBean,根据层次关系以及之前的分析,该类实现类比较重要的BeanClassLoaderAware,MethodIntercepter, InitializingBean。

其中实现了InitializingBean,则Spring会确保在此初始化bean时调用其afterPropertiesSet方法进行逻辑的初始化。

这样就形成了一个大致的轮廓,当获取该bean时,首先会通过afterPropertiesSet创建代理类,既然调用代理类,并使用当前类座位增强方法,而在调用该bean时其实返回的是代理类,既然调用的是代理类,那么又会使当前bean作为增强器增强,也就是说会调用RMIProxyFactoryBean的父类RMIClientIntercepter的invoke方法。

afterPropertiesSet中的super.afterPropertiesSet()方法只完成了对serviceUrl属性的验证。所以推断所有的客户端都应该在prepare中实现,继续查看prepare()。

12.1.2.1 通过代理拦截并获取stub

lookupStubOnStartup,如果将此属性设置为true,那么stub的工作会在系统启动时被执行并缓存,从而提高使用时候的响应时间。

获取stub是RMI应用中的关键步骤,可以使用两种方式进行。

1. 使用自定义套接字工厂。使用这种方式,你需要在勾走Registry实例时将自定义套接字工厂传入,并使用Registry提供的lookup方法来获取对应的stub。
2. 直接使用RMI提供的标准方法:Naming.lookup(getServiceUrl()).
12.1.2.2 增强器进行远程连接

之前分析了类型为RMIProxyFactoryBean的bean的初始化中完成逻辑操作。在初始化时,创建了代理并将本身作为增强器加入了代理中(RMIProxyFactoryBean间接实现了MethodIntercepter),那么这样一来,当在客户端代理的接口中的某个方法时,就会首先执行RMIProxyFactoryBean中的invoke方法进行增强。

众所周知,当客户端使用接口进行方法调用时是通过RMI获取stub的,再通过stub中封装的信息进行服务器调用,这个stub就是在构建服务器时发布的对象,那么,客户端抵用是最关键的一步就是进行stub的获取了。

当获取到stub或便可以进行远程方法调用,Spring中对远程方法的调用其实是两种情况考虑的:

1. 获取的stub是RMIInvocationHandler,就意味着服务端也是同样使用Spring构建的,那么自然会使用Spring中的约定,进行客户端调用处理。Spring中的处理方式被委托给doInvoke方法。
2. 获取的stub不是RMIInvocationHandler类型,那么服务端构建RMI服务可能是通过普通的方法或者借助Spring以外的第三方插件,那么处理方式自然会按照RMI中普通的处理方式进行,而这种普通的处理方式无法是反射。因为在invocation中包含了所需要调用的方法的各种信息,包含方法名称以及参数等,而调用的实体正式stub,那么通过放射方法完全可以激活stub中的远程调用。

之前反复提到了Spring中的客户端处理RMI的方式。其实,在分析服务端发布RMI的方式时,Spring将RMI导出Object封装成RMIInvocationHandler类型进行发布,那么当客户端获取stub的时候是包含了远程连接信息代理类的RMIInvocationHandler,也就是说当调用RMIInvocationHandler中的方法是会使用RMI中提供的代理进行远程连接,而在此时,Spring中要做的就是将代买引向RMIInvocationHandler接口的invoke方法的调用。

12.2 HttpInvoker

一方面,RMI使用Java标准的对象序列化,但是很难穿越防火墙;另一方面,Hessian/Burlap能很好地穿越防火墙工作,但使用自己私有的一套对象序列化机制。

就这样,Spring的HttpInvoker应运而生。HttpInvoker是一个新的远程调用模型,作为Spring框架的一部分,来执行基于Http的远程调用,并使用Java的序列化机制。

12.2.1 使用示例
1. 创建对外接口
2. 创建接口实现类
3. 创建服务端配置文件
4. 在WEB-INF下创建remote-servlet.xml
5. 创建测试端配置client.xml
6. 创建测试类
12.2.2 服务端实现

根据remote-servlet.xml中的配置,我们分析入口为HttpInvokerServiceExporter,该类实现了InitializingBean接口以及HttpRequestHandler接口,当某个bean继承了InitializingBean接口时,Spring会确保这个bean在初始化是调用其afterPropertiesSet,而对于HttpRequestHandler接口,因为我们在配置中已经将此接口配置成Web服务,那么当有相应请求的时候,Spring的Web服务就会将程序引导至HttpRequestHandler的handle方法中。

12.2.2.1 创建代理

prepare() -> getProxyForService()(JDK方式创建代理) -> new RemoteInvocationTraceInterceptor(…)

通过上面3个方法串联,初始化过程中实现的逻辑主要是创建一个代理,代理中封装了对于特定请求的处理方法以及接口等信息,而这个代理的最关键的目的是加入了RemoteInvocationTraceInterceptor。RemoteInvocationTraceInterceptor中的增强主要是对增强的目标方法进行一些相关信息的日志打印,并没有在此基础上进行任何功能性的增强。

12.2.2.2 处理客户端的request

当有Web请求时,根据配置中的规则会把配置中的规则会把路径匹配的访问直接引入对应的HttpRequestHandler中。

在handlerRequest函数中,HttpInvoker处理的大致框架将请求的方法,也就是RemoteInvocation对象,从客户端序列化并通过Web请求出入服务器,服务端再对传过来的序列化对象进行反序列化还原RemoteInvocation实例,然后通过实例中的相关信息进行相关方法的调用,并将执行结果再次返回给客户端。

1. 从request中读取序列化对象readRemoteInvocation(...):提取HttpServletRequest中的RemoteInvocation对象的序列化信息以及反序列化的过程。这里完全是按照标准的方式进行操作,包括创建ObjectInputStream以及从ObjectInputStream中提取对象实例。
2. 执行调用invokeAndCreateResult(...):根据反序列化方式得到的RemoteInvocation对象中的信息,进行方法调用。测试调用的实体并不是服务接口或者服务类,而是之前在初始化构造的封装了服务接口以及服务类的代理。完成RemoteInvocation实例的提取,也就意味着可以通过RemoteInvocation实例中提供的信息进行方法调用了。
3. 将结果的序列化对象写入输出流。
12.2.3 客户端实现

客户端比较重要任务就是构建RemoteInvocation实例,并传送到服务端,从层次结构中可以看出,HttpInvokerProxyFactoryBean类同样实现了InitializingBean以及MethodIntercepter。

HttpInvokerProxyFactoryBean类型bean在初始化过程中创建了封装服务接口的代理,并使自身作为增强拦截器,然后又因为实现MethodIntercepter接口,所以获取Bean的时候返回的其实是创建的代理。

所有的逻辑实现都在HttpInvokerProxyFactoryBean类的invoke方法,在调用invoke方法之前该方法所提供的主要功能是将调用信息封装在RemoteInvocation中,发送给客户端并等待返回结果。

函数主要有3个步骤

1. 构建RemoteInvocation实例:因为是代理中增强方法的调用,调用的方法及参数信息会在代理中封装至MethodInvocation实例中,并在增强其中进行传递,那么首先,要做的就是将MethodInvocation中的信息提取并构建RemoteInvocation实例。
2. 远程执行方法(doExecuteRequest(...)):Spring引入Apache第三方JAR。
3. 提取结果,考虑到序列化的问题,在Spring中约定使用HttpInvoker方式进行远程方法调用时,结果使用RemoteInvocationResult进行封装,那么提取结果后还需要从封装的结果中提取对应的结果。