java远程方法调用。即Java RMI(Java Remote Method Invocation),是Java编程语言里一种用于实现远程过程调用的应用程序编程接口,它使客户机上运行的程序可以调用远程服务器上的对象,远程方法调用特性使Java编程人员能够是网络环境中分布操作,RMI全部的宗旨就是尽可能地简化远程接口对象的使用。
Java RMI极大的依赖于接口,在需要创建一个远程对象时,程序员通过传递一个接口来隐藏底层的实现细节,客户端得到的远程对象句柄正好与本地的根代码连接,由后者负责透过网络通信,这样一来,程序员只需要关心如何通过自己的接口句柄发送消息。
RMI
在Spring中,同样提供了对RMI的支持,使得在Spring 下的RMI开发变得更加方便,同样,我们还是通过示例来快速的体验RMI所提供的功能。
使用示例
以下提供了Spring整合RMI的使用示例
- 建立RMI对外接口
public interface HelloRMIService { int getAdd(int a ,int b ); }
- 建立接口实现类
public class HelloRMIServiceImpl implements HelloRMIService { @Override public int getAdd(int a, int b) { System.out.println(" a = "+ a + " , b = "+ b + ",result = "+ (a + b )); return a + b ; } }
- 建立服务端配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!--服务端--> <bean id="helloRMI" class="com.spring_1_100.test_71_80.test76_spring_rmi.HelloRMIServiceImpl"></bean> <!--服务类--> <bean id="myRMI" class="org.springframework.remoting.rmi.RmiServiceExporter"> <!--服务类--> <property name="service" ref="helloRMI"></property> <!--服务名--> <property name="serviceName" value="helloRMI"></property> <!--服务接口--> <property name="serviceInterface" value="com.spring_1_100.test_71_80.test76_spring_rmi.HelloRMIService"></property> <!--服务端--> <property name="registryPort" value="9999"></property> <!--其他的属性自己查看,org.springframework.remoting.RMI.RMIServiceExporter 的类,就知道支持的属性了--> </bean> </beans>
4.建立服务端测试
public class ServerTest { public static void main(String[] args) { new ClassPathXmlApplicationContext("classpath:spring_1_100/config_71_80/spring76_rmi/spring76_service.xml"); } }
到这里,建立RMI服务端的步骤己经结束了,服务端发布了一个两数相加的对外接口供其他服务器调用,启动服务端测试类,其他机器或端口便可以通过RMI来连接本机了。
- 完成了服务端的配置后,还需要在测试端建立测试环境以及测试代码,首先测试端配置文件。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <bean id="myClient" class="org.springframework.remoting.rmi.RmiProxyFactoryBean"> <property name="serviceUrl" value="rmi://127.0.0.1:9999/helloRMI"></property> <property name="serviceInterface" value="com.spring_1_100.test_71_80.test76_spring_rmi.HelloRMIService"></property> </bean> </beans>
- 编写测试代码
public class ClientTest { public static void main(String[] args) { ApplicationContext ct = new ClassPathXmlApplicationContext("classpath:spring_1_100/config_71_80/spring76_rmi/spring76_client.xml"); HelloRMIService helloRMIService = ct.getBean("myClient",HelloRMIService.class); int c = helloRMIService.getAdd(1,2); System.out.println(c); } }
结果输出:
3
通过以上的步骤,实现了测试端的代码调用,你会看到测试端通过RMI进行远程连接,连接到了服务端,并使用对应的实现类HelloRMIServiceImpl中提供的方法getAdd来计算参数并返回结果,你会看到控制台输出了3,当然以上的测试用例是使用同一台不同的端口来模拟不同的机器的RMI连接,在企业应用中一般都是使用不同的机器来进行RMI服务的发布与访问的,你需要将接口打包,并放置在服务端工程中。
这是一个简单的方法展示 ,但是却很好的展示了Spring中使用RMI的流程以及步骤,如果抛出Spring而使用原始的RMI发布与连接,则会是一件很麻烦的事情,在后面我们来举一个例子,在Spring中使用的RMI非常简单,Spring帮我们做了大量的工作,这些工作都包括什么呢?接下来我们一起深入研究分析Spring中对RMI功能的实现原理。
服务端实现
首先我们从服务端发布的功能开始着手,同样,Spring中核心 还是配置文件,这是所有功能的基础,在服务端配置文件中我们可以看到,定义了两个bean,其中一个是对接口实现类的发布,而另一个则是对RMI服务的发布,使用org.springframework.remotin.RMI.RMIServiceExporter类进行封装,其中包括了服务类,服务名,服务接口,服务端口等若干属性,因此我们可以断定,org.springframework.remoting.RMI.RMIServiceExporter类应该是发布RMI的关键类,我们可以从此类入手进行分析。
根据前面展示的示例,启动Spring中的RMI服务并没有多余的操作,仅仅是开启Spring的环境,new ClassPathXmlApplicationContext(“classpath:spring_1_100/config_71_80/spring76_rmi/spring76_service.xml”) ,仅此一句,于是,我们分析我可能RMIServiceExportern在初始化的时候做了某些操作完成了端口的发布功能,那么这些操作的入口是在这个类的哪些方法里调用呢?这个类的层次结构
RMIServiceExporter实现了Spring中几个比较敏感的接口,BeanClassLoaderAware,DisposableBean,InitializingBean,其中DisposableBean接口保证在实现该接口的bean销毁时调用其destory方法,BeanClassLoaderAware接口保证在实现该接口的bean的初始化时调用setBeanClassLoader方法,而InitializingBean接口则是保证在实现该接口的bean初始化时调用afterPropertiesSet方法,所以我们推断RMIServiceExporter的初始化函数入口一定在其afterPropertiesSet或者setBeanClassLoader方法中,经过查看代码,确认afterPropertiesSet为RMIServiceExporter功能的初始化入口。
public void afterPropertiesSet() throws RemoteException { prepare(); } public void prepare() throws RemoteException { //如果没有配置service,则throw new IllegalArgumentException("Property 'service' is required"); checkService(); //如果serviceName为空,抛出IllegalArgumentException异常 if (this.serviceName == null) { throw new IllegalArgumentException("Property 'serviceName' is required"); } //如果用户在配置文件中配置了clientSocketFactory或者serverSocketFactory的处理 //如果配置文件中配置了clientSocketFactory同时又实现了RMIserverSocketFactory //接口,那么会忽略配置中的serverSocketFactory而使用clientSocketFactory代替 if (this.clientSocketFactory instanceof RMIServerSocketFactory) { this.serverSocketFactory = (RMIServerSocketFactory) this.clientSocketFactory; } //clientSocketFactory和serverSocketFactory要么同时出现,要么同时不出现 if ((this.clientSocketFactory != null && this.serverSocketFactory == null) || (this.clientSocketFactory == null && this.serverSocketFactory != null)) { throw new IllegalArgumentException( "Both RMIClientSocketFactory and RMIServerSocketFactory or none required"); } //如果配置了registryClientSocketFactory同时又实现了RMIServerSocketFactory接口, //那么会忽略配置中的registryClientSocketFactory而使用registryClientSocketFactory代替 if (this.registryClientSocketFactory instanceof RMIServerSocketFactory) { this.registryServerSocketFactory = (RMIServerSocketFactory) this.registryClientSocketFactory; } //registryClientSocketFactory和registryServerSocketFactory要么同时出现,要么同时不出现 if (this.registryClientSocketFactory == null && this.registryServerSocketFactory != null) { throw new IllegalArgumentException( "RMIServerSocketFactory without RMIClientSocketFactory for registry not supported"); } this.createdRegistry = false; //确定RMI registry if (this.registry == null) { this.registry = getRegistry(this.registryHost, this.registryPort, this.registryClientSocketFactory, this.registryServerSocketFactory); this.createdRegistry = true; } //初始化以及从缓存中导出Object //此时通常情况下使用RMIInvocationWrapper封装JDK代理类,切面为RemoteInvocationTraceInterceptor this.exportedObject = getObjectToExport(); if (logger.isInfoEnabled()) { logger.info("Binding service '" + this.serviceName + "' to RMI registry: " + this.registry); } if (this.clientSocketFactory != null) { //使用由给定的套接字工厂指定传递方式导出远程对象,以便能够接收传入的调用。 //clientSocketFactory:进行远程对象调用的客户端套接字工厂 //serverSocketFactory:接收远程调用的服务端套接字工厂 UnicastRemoteObject.exportObject( this.exportedObject, this.servicePort, this.clientSocketFactory, this.serverSocketFactory); } else { //导出remote object,以使它能够接收特定的端口调用 UnicastRemoteObject.exportObject(this.exportedObject, this.servicePort); } try { if (this.replaceExistingBinding) { this.registry.rebind(this.serviceName, this.exportedObject); } else { //绑定服务名称remote object ,外界调用serviceName的时候会被exportObject对象接收 this.registry.bind(this.serviceName, this.exportedObject); } } catch (AlreadyBoundException ex) { unexportObjectSilently(); throw new IllegalStateException( "Already an RMI object bound for name '" + this.serviceName + "': " + ex.toString()); } catch (RemoteException ex) { unexportObjectSilently(); throw ex; } }
果然,在afterPropertiesSet函数中将实现委托给了prepare,而在prepare方法中我们找到了RMI服务发布的功能实现,同时,我们也大致清楚了RMI服务发布的流程。
1.验证service.
此处的service对应的是配置中类型为RMIServiceExporter的service属性,它是实现类,并不是接口,尽管后期会对RMIServiceExporter做一系列的封装,但是最终还是会将逻辑引向至RMIServiceExporter来处理,所以,在发布之前需要进行验证。
2.处理用户自定义的socketFactory属性。
在RMIServiceExporter中提供了4个套接字工厂配置,分别是clientSocketFactory,serviceSocketFactory,registryClientSocketFactory,registryServiceSocketFactory,那么这两对配置又有什么区别或者说分别是应用在什么样的场景呢?
registryClientSocketFactory与registryServerSocketFactory用于主机与RMI服务器之间的创建,也就是当使用LocateRegistry.createRegistry(registryPort,clientSocketFactory,serverSocketFactory)方法创建registry实例时会在RMI主机使用serverSocketFactory创建套接字等待连接,而服务端与RMI主机通信时会使用clientSocketFactory创建连接套接字。
clientSocketFactory,serverSocketFactory同样是创建套接字,但是使用的位置不同,clientSocketFactory,serverSocketFactory用于导出远程对象,serverSocketFactory用于在服务端建立套接字等待客户端连接,而clientSocketFactory用于调用端建立套接字发起连接。
3. 根据配置参数获取Registry。
4. 构造对外发布的实例
构建对外发布的实例,当外界通过注册的服务名调用响应的方法时,RMI服务会将请求引入此类来处理。
5. 发布实例
在发布RMI服务的流程中,有几个步骤可能是我们比较关心的。
1. 获取registry
对RMI稍有了解就会知道,由于底层的封装,获取Registry实例是非常简单的事情,只需要使用一个函数LocateRegistry.createRegistry(…)创建Registry实例就可以了,但是Spring中并没有这么做,而是考虑得更多的事情,比如RMI注册主机与发布的服务并不是同一台机器上,那么需要使用LocateRegistry.getRegistry(registryHost,registryPort,clientSocketFactory)去远程获取Registry实例。
protected Registry getRegistry(String registryHost, int registryPort, RMIClientSocketFactory clientSocketFactory, RMIServerSocketFactory serverSocketFactory) throws RemoteException { if (registryHost != null) { // Host explicitly specified: only lookup possible. if (logger.isInfoEnabled()) { logger.info("Looking for RMI registry at port '" + registryPort + "' of host [" + registryHost + "]"); } //如果registryHost不为空则尝试获取对应的主机Registry Registry reg = LocateRegistry.getRegistry(registryHost, registryPort, clientSocketFactory); testRegistry(reg); return reg; } else { //获取本机的Registry return getRegistry(registryPort, clientSocketFactory, serverSocketFactory); } }
如果并不是从另外的服务器上获取Registry连接,那么就需要在本地创建RMI的registry实例了,当然这里有一个关键的参数alwaysCreateRegistry,如果此参数配置为true,那么获取Registry实例时会首先测试是否己经建立了对指定端口的连接,如果己经建立则利用己经创建的实例,否则重新创建。
当前,之前也提到过,创建Registry实例时可以使用自定义的连接工厂,而之前的判断也保证了clientSocketFactory与serverSocketFactory要么同时出现,要么同时不出现,所以这里只是对clientSocketFactory是否为空进行了判断。
如果创建Registry实例时不需要使用自定义的套接字工厂,那么就可以直接使用LocateRegistry.createRegistry(…)方法来创建了,当然复用检查还是必要的。
protected Registry getRegistry(int registryPort) throws RemoteException { if (this.alwaysCreateRegistry) { logger.info("Creating new RMI registry"); return LocateRegistry.createRegistry(registryPort); } if (logger.isInfoEnabled()) { logger.info("Looking for RMI registry at port '" + registryPort + "'"); } synchronized (LocateRegistry.class) { try { //查看对应的当前registryPort的Registry是否己经创建,如果己经他创建,则直接使用 Registry reg = LocateRegistry.getRegistry(registryPort); //测试是否可用,如果不可用,则抛出异常 testRegistry(reg); return reg; } catch (RemoteException ex) { logger.debug("RMI registry access threw exception", ex); logger.info("Could not detect RMI registry - creating new one"); //根据端口创建Registry return LocateRegistry.createRegistry(registryPort); } } }
2. 初始化将要导出的实体对象
之前有提到过,当请求某个RMI服务的时候,RMI会根据注册的服务名称,将请求引导至远程对象处理类中,这个处理类便是getObjectToExport()进行创建。
protected Remote getObjectToExport() { //如果配置的service属性对应的类实现了Remote接口且没有配置serviceInterface属性 if (getService() instanceof Remote && (getServiceInterface() == null || Remote.class.isAssignableFrom(getServiceInterface()))) { // conventional RMI service return (Remote) getService(); } else { //对service进行封装 if (logger.isDebugEnabled()) { logger.debug("RMI service [" + getService() + "] is an RMI invoker"); } return new RmiInvocationWrapper(getProxyForService(), this); } }
请求处理类的初始化主要处理规则为,如果配置的service属性对应的类实现了Remote接口且没有配置serviceInterface属性,那么直接使用service作为中处理类,否则,使用RMIInvocationWrapper对service的代理类和当前类也就是RMIServiceExporter进行封装。
经过这样的封装,客户端与服务端便可以达成一致的协义,当客户端检测到是RMIInvocationWrapper类型stub的时候便会直接调用其invoke方法,使得调用端与服务端很好的连接了一起,而RMIInvocationWrapper封装了用于处理请求的代理类,在invoke中便会使用代理类进行进一步的处理。
之前的逻辑己经非常清楚了,当请求的RMI时会由注册表Registry实例将请求转向前注册的处理类去处理,也就是之前封装的RMIInvocationWrapper,然后由RMIInvocationWrapper中的invoke方法进行处理,那么为什么不是在invoke方法中直接使用service,而是通过代理再次将service封装呢?
这其中一个关键的点是,在创建代理时添加一个增强拦截器RemoteInvocationTranceInterceptor目的是为了对方法调用进行打印跟踪,但是如果直接在invoke方法中硬编码这些日志,会使代码看起来不是很优雅,而且耦合度很高,使用代理的方式就会解决这样的问题,而且会有很高的扩展性。
protected Object getProxyForService() { //验证service checkService(); //验证serviceInterface checkServiceInterface(); //使用JDK方式创建代理 ProxyFactory proxyFactory = new ProxyFactory(); //添加代理接口 proxyFactory.addInterface(getServiceInterface()); if (this.registerTraceInterceptor != null ? this.registerTraceInterceptor.booleanValue() : this.interceptors == null) { //加入代理的横切面RemoteInvocattiontraceInterceptor并记录Export名称 proxyFactory.addAdvice(new RemoteInvocationTraceInterceptor(getExporterName())); } if (this.interceptors != null) { AdvisorAdapterRegistry adapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); for (int i = 0; i < this.interceptors.length; i++) { proxyFactory.addAdvisor(adapterRegistry.wrap(this.interceptors[i])); } } //设置要代理的目标类 proxyFactory.setTarget(getService()); proxyFactory.setOpaque(true); //创建代理 return proxyFactory.getProxy(getBeanClassLoader()); }
3. RMI服务激活调用
之前反复提到过,由于在之前bean初始化的时候做了服务名称绑定this.registryBind(this.serviceName,this.exportedObject),其中exportedObject其实是被RMIInvocationWrapper进行封装的,也就是说当其他的服务器调用serviceName的RMI服务时,Java会为我们封装其内部操作,而直接会将代码转向RMIInvocationWrapper的Invoke方法中。
public Object invoke(RemoteInvocation invocation) throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { return this.rmiExporter.invoke(invocation, this.wrappedObject); }
而此时this.RMIExporter为之前初始化的RMIServiceExporter,invocation为包含着需要激活的方法参数,而wrappedObject则是之前封装的代理类。
protected Object invoke(RemoteInvocation invocation, Object targetObject) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { return super.invoke(invocation, targetObject); } protected Object invoke(RemoteInvocation invocation, Object targetObject) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { if (logger.isTraceEnabled()) { logger.trace("Executing " + invocation); } try { return getRemoteInvocationExecutor().invoke(invocation, targetObject); } catch (NoSuchMethodException ex) { if (logger.isDebugEnabled()) { logger.warn("Could not find target method for " + invocation, ex); } throw ex; } catch (IllegalAccessException ex) { if (logger.isDebugEnabled()) { logger.warn("Could not access target method for " + invocation, ex); } throw ex; } catch (InvocationTargetException ex) { if (logger.isDebugEnabled()) { logger.debug("Target method failed for " + invocation, ex.getTargetException()); } throw ex; } } public Object invoke(RemoteInvocation invocation, Object targetObject) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException{ Assert.notNull(invocation, "RemoteInvocation must not be null"); Assert.notNull(targetObject, "Target object must not be null"); //通过反射激活方法 return invocation.invoke(targetObject); } public Object invoke(Object targetObject) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { //根据方法名称获取代理中对应的方法 Method method = targetObject.getClass().getMethod(this.methodName, this.parameterTypes); //执行代理中的方法 return method.invoke(targetObject, this.arguments); }
客户端实现
根据客户端配置文件,锁定入口类为RMIProxyFactoryBean,同样根据类的层次结构查找入口函数,如下:
根据层次关系及之前的分析,我们提取出该类实现比较重要的接口InitializingBean,BeanClassLoaderAware以及MethodInterceptor。
其中实现了InitializingBean,则Spring会确保在此初始化bean时调用afterPropertiesSet进行逻辑的初始化。
public void afterPropertiesSet() { super.afterPropertiesSet(); if (getServiceInterface() == null) { throw new IllegalArgumentException("Property 'serviceInterface' is required"); } //根据设置的接口创建代理,并使用当前类this作为增强器 this.serviceProxy = new ProxyFactory(getServiceInterface(), this).getProxy(getBeanClassLoader()); }
上述加粗一行代码的主要目的是什么呢,为什么要通过接口生成一个代理对象呢?我们跟进代码。
public ProxyFactory(Class<?> proxyInterface, Interceptor interceptor) { addInterface(proxyInterface); addAdvice(interceptor); } public void addAdvice(Advice advice) throws AopConfigException { int pos = this.advisors.size(); addAdvice(pos, advice); } public void addAdvice(int pos, Advice advice) throws AopConfigException { Assert.notNull(advice, "Advice must not be null"); if (advice instanceof IntroductionInfo) { addAdvisor(pos, new DefaultIntroductionAdvisor(advice, (IntroductionInfo) advice)); } else if (advice instanceof DynamicIntroductionAdvice) { throw new AopConfigException("DynamicIntroductionAdvice may only be added as part of IntroductionAdvisor"); } else { addAdvisor(pos, new DefaultPointcutAdvisor(advice)); } }
代码跟进到这里,好像一无获,只看到将RmiProxyFactoryBean被封装成DefaultPointcutAdvisor的point属性加入到advisor中。带着疑问,我们继续看下面代码。
突然发现,RMIProxyFactoryBean又实现了FactoryBean接口,那么当获取Bean时并不是直接获取bean的,而是调用该bean的getObject方法。
public Object getObject() { return this.serviceProxy; }
因此,在Spring获取HelloRMIService时,实际上获取到的是serviceProxy JDK代理对象。这样的操作,是不是有一种似曾相识的感觉,细心的读者肯定会想到,MyBatis的Mapper不就是这种实现方式嘛。在容器中真正获取到的是Mapper的代理对象,解析xml,连接数据库,结果集的封装都在代理类中进行。
这样,我们似乎己经形成一个大致的轮廓,当获取该bean时,首先通过afterPropertiesSet创建代理类,并使用当前类作为增强方法,而在该bean时其实返回的是代理类,既然调用的是代理类,那么又会使用当前bean作为增强器进行增强,也就是说会调用RMIProxyFactoryBean的父类RMIClientInterceptor的invoke方法。
我们首先从afterPropertiesSet中的super.afterPropertiesSet()方法开始分析。
public void afterPropertiesSet() { super.afterPropertiesSet(); prepare(); }
继续追踪代码,发现父类的父类,也就是RULBasedRemoteAccessor中的afterPropertiesSet方法只完成了对serviceUrl属性的验证。
public void afterPropertiesSet() { if (getServiceUrl() == null) { throw new IllegalArgumentException("Property 'serviceUrl' is required"); } }
所以推断所有的客户端都应该在prepare方法中实现,继续查看prepare()。
- 通过代理拦截并获取stub
在父类afterPropertiesSet方法中完成了对serviceUrl的验证,那么prepare函数中完成了什么样的功能呢?
public void prepare() throws RemoteLookupFailureException { //如果配置了lookupStubOnStartup属性便会在启动时寻找stub if (this.lookupStubOnStartup) { Remote remoteObj = lookupStub(); if (logger.isDebugEnabled()) { if (remoteObj instanceof RmiInvocationHandler) { logger.debug("RMI stub [" + getServiceUrl() + "] is an RMI invoker"); } else if (getServiceInterface() != null) { boolean isImpl = getServiceInterface().isInstance(remoteObj); logger.debug("Using service interface [" + getServiceInterface().getName() + "] for RMI stub [" + getServiceUrl() + "] - " + (!isImpl ? "not " : "") + "directly implemented"); } } if (this.cacheStub) { //如果cacheStub为true,则将stub缓存 this.cachedStub = remoteObj; } } }
从上面的代码中,我们了解到了一个很重要的属性lookupStubOnStartup,如果此属性设置为true,那么获取stub的工作就会在系统启动时被执行缓存,从而提高使用的时候响应时间。
获取stub是RMI应用中的关键步骤,当你可以使用两种方式进行。
- 使用自定义的套接字工厂,如果使用这种方式,你需要在构造Registry实例时将自定义套接字工厂传入,并使用Registry中提供的lookup方法来获取对应的sub。
- 直接使用RMI提供的标准方法,Naming.lookup(getServiceUrl())。
protected Remote lookupStub() throws RemoteLookupFailureException { try { Remote stub = null; if (this.registryClientSocketFactory != null) { URL url = new URL(null, getServiceUrl(), new DummyURLStreamHandler()); //验证传输协义 String protocol = url.getProtocol(); if (protocol != null && !"rmi".equals(protocol)) { throw new MalformedURLException("Invalid URL scheme '" + protocol + "'"); } //主机 String host = url.getHost(); //端口 int port = url.getPort(); //服务名 String name = url.getPath(); if (name != null && name.startsWith("/")) { name = name.substring(1); } Registry registry = LocateRegistry.getRegistry(host, port, this.registryClientSocketFactory); stub = registry.lookup(name); } else { // Can proceed with standard RMI lookup API... stub = Naming.lookup(getServiceUrl()); } if (logger.isDebugEnabled()) { logger.debug("Located RMI stub with URL [" + getServiceUrl() + "]"); } return stub; } catch (MalformedURLException ex) { throw new RemoteLookupFailureException("Service URL [" + getServiceUrl() + "] is invalid", ex); } catch (NotBoundException ex) { throw new RemoteLookupFailureException( "Could not find RMI service [" + getServiceUrl() + "] in RMI registry", ex); } catch (RemoteException ex) { throw new RemoteLookupFailureException("Lookup of RMI stub failed", ex); } }
为了使用registryClientSocketFactory,代码量比使用RMI标准获取stub方法多出 很多,那么registryClientSocket到底是做什么的呢?
与之前服务端的套接字工厂类似,这里的registryClientSocketFactory用来连接RMI服务器,用户通过实现RMIClientSocketFactory接口来控制用于连接socket的各种参数 。
- 增强器进行远程连接
之前分析了类型为RMIProxyFactoryBean的bean的初始化完成逻辑操作,在初始化时,创建代理并将本身作为增强器加入到代理中(RMIProxyFactoryBean间接实现了MethodInterceptor),这样一来,当客户端调用代理接口中的某个方法时,就会首先执行RMIProxyFactoryBean中的invoke方法进行增强。
public Object invoke(MethodInvocation invocation) throws Throwable { //获取服务器中对应注册的remote对象,通过序列化传输 Remote stub = getStub(); try { return doInvoke(invocation, stub); } catch (RemoteConnectFailureException ex) { return handleRemoteConnectFailure(invocation, ex); } catch (RemoteException ex) { if (isConnectFailure(ex)) { return handleRemoteConnectFailure(invocation, ex); } else { throw ex; } } }
众所周知,当客户端使用接口进行方法调用时是通过RMI获取stub的,然后再通过stub封装的信息进行服务器的调用,这个stub就是在构建服务器时发布的对象,那么,客户端调用最关键的一步就是进行stub的获取了。
protected Remote getStub() throws RemoteLookupFailureException { //如果有缓存,则直接从缓存中获取 if (!this.cacheStub || (this.lookupStubOnStartup && !this.refreshStubOnConnectFailure)) { return (this.cachedStub != null ? this.cachedStub : lookupStub()); } else { synchronized (this.stubMonitor) { if (this.cachedStub == null) { //获取stub this.cachedStub = lookupStub(); } return this.cachedStub; } } }
默认情况下cacheStub为true,lookupStubOnStartup为true,refreshStubOnConnectFailure为false,当项目启动时,会将创建好的Remote对象缓存到cachedStub中,因此上述代码最终都会走加粗的这一行,其他的代码不是多此一举吗?
- 第一种场景,假如项目中配置cacheStub=false,项目启动时不会将Remote对象缓存到cachedStub中,因此每次获取stub时,都会调用lookupStub(),创建一个新的Remote对象并返回。而在项目运行中,通过程序修改cacheStub=true,此时会走synchronized内部的代码块,而cacheStub==null,因此第一次会调用lookupStub()方法创建对象并缓存到cachedStub中,以后每次从缓存中读取Remote对象。
- 第二种场景,项目启动时使用默认的配置,当程序运行过程中修改cacheStub为false,此时会进入到return (this.cachedStub != null ? this.cachedStub : lookupStub());这一行代码中,因为cachedStub不为空,因此还是从缓存中获取Remote对象,所以修改无效。
上述过程中,只为获取Remote对象,为了方便程序员配置,实现的逻辑不简单,同时还做了并发层面的考虑。
当获取到stub后便可以远程方法调用了,Spring中对于远程方法调用其实是分为两种情况的。
- 获取的stub是RMIInvocationHandler类型的,从服务端获取的stub是RMIInvocationHandler,就意味着服务端也同样使用了Spring去构建,那么自然会使用Spring中作的约定,进行客户端调用的处理,Spring中处理方式被委托给了doInvoke方法。
- 当获取的stub不是RMIInvocationHandler类型,那么服务端构建RMI服务可能是通过普通的方式或者借助于Spring外的第三方插件,那么处理方式自然会按照RMI中普通 的处理方式进行的,而这种普通的处理方式无非就是通过反射,因为在invocation中包含了所需要的调用的方法和各种信息,包括方法名称以及参数等,而调用实体正是stub,那么通过反射方法完全可以激活stub中的远程调用。
protected Object doInvoke(MethodInvocation invocation, Remote stub) throws Throwable { if (stub instanceof RmiInvocationHandler) { //stub从服务器传回且经过Spring的封装 try { return doInvoke(invocation, (RmiInvocationHandler) stub); } catch (RemoteException ex) { throw RmiClientInterceptorUtils.convertRmiAccessException( invocation.getMethod(), ex, isConnectFailure(ex), getServiceUrl()); } catch (InvocationTargetException ex) { Throwable exToThrow = ex.getTargetException(); RemoteInvocationUtils.fillInClientStackTraceIfPossible(exToThrow); throw exToThrow; } catch (Throwable ex) { throw new RemoteInvocationFailureException("Invocation of method [" + invocation.getMethod() + "] failed in RMI service [" + getServiceUrl() + "]", ex); } } else { //直接通过反射方法继续激活 try { return RmiClientInterceptorUtils.invokeRemoteMethod(invocation, stub); } catch (InvocationTargetException ex) { Throwable targetEx = ex.getTargetException(); if (targetEx instanceof RemoteException) { RemoteException rex = (RemoteException) targetEx; throw RmiClientInterceptorUtils.convertRmiAccessException( invocation.getMethod(), rex, isConnectFailure(rex), getServiceUrl()); } else { throw targetEx; } } } }
之前反复提到了Spring中客户端处理RMI的方式,其实在分析服务端发布RMI的方式时,我们己经了解到了,Spring将RMI的导出Object封装成RMIInvocationHandler类型进行发布,那么当客户端获取stub的时候是包含了远程连接信息代理类的RMIInvocationHandler,也就是说当调用RMIInvocationHandler中的方法时会使用RMI中提供的代理进行远程连接,而此时,Spring中要做的就是代码引向RMIInvocationHandler接口的invoke方法的调用。
protected Object doInvoke(MethodInvocation methodInvocation, RmiInvocationHandler invocationHandler) throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { if (AopUtils.isToStringMethod(methodInvocation.getMethod())) { return "RMI invoker proxy for service URL [" + getServiceUrl() + "]"; } //将methodInvation中的方法名及参数等信息重新封装到RemoteInvocation,并通过远程代理方法直接调用 return invocationHandler.invoke(createRemoteInvocation(methodInvocation)); }
public class DefaultRemoteInvocationFactory implements RemoteInvocationFactory { @Override public RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) { return new RemoteInvocation(methodInvocation); } } public RemoteInvocation(MethodInvocation methodInvocation) { this.methodName = methodInvocation.getMethod().getName(); this.parameterTypes = methodInvocation.getMethod().getParameterTypes(); this.arguments = methodInvocation.getArguments(); }
当代码跟踪到这里,发现调用远程对象时仅仅将方法名称,方法参数类型数组,以及调用参数传递到远程,因此,客户端的service名称和服务端不一样,也不影响。来测试一下。
结果正如 我们所料,在远程调用时,只和方法名称,方法参数类型有关。
而对于没有实现RmiInvocationHandler接口的Remote对象,直接反射调用。
public static Object invokeRemoteMethod(MethodInvocation invocation, Object stub) throws InvocationTargetException { Method method = invocation.getMethod(); try { if (method.getDeclaringClass().isInstance(stub)) { return method.invoke(stub, invocation.getArguments()); } else { Method stubMethod = stub.getClass().getMethod(method.getName(), method.getParameterTypes()); return stubMethod.invoke(stub, invocation.getArguments()); } } catch (InvocationTargetException ex) { throw ex; } catch (NoSuchMethodException ex) { throw new RemoteProxyFailureException("No matching RMI stub method found for: " + method, ex); } catch (Throwable ex) { throw new RemoteProxyFailureException("Invocation of RMI stub method failed: " + method, ex); } }
我们学习了那么多,觉得Spring帮我们考虑了好多的事情,假如我们不用Spring,自己单独用rmi,那怎样写测试用例呢?通过上面的学习,我相信很多的读者己经有抽丝剥茧的能力。在不用Spring的情况下,如何写一个RMI服务调用。
1.前期准备工作
public class MyRemoteInvocation implements Serializable { private static final long serialVersionUID = 6876024250231820554L; private String methodName; private Class<?>[] parameterTypes; private Object[] arguments; ...省略get set 方法 } public interface MyRmiInvocationHandler extends Remote { Object invoke(MyRemoteInvocation invocation) throws Exception; } public class MyRmiInvocationWrapper implements MyRmiInvocationHandler { private final Object wrappedObject; public MyRmiInvocationWrapper(Object wrappedObject) { this.wrappedObject = wrappedObject; } @Override public Object invoke(MyRemoteInvocation invocation) throws Exception { Method method = this.wrappedObject.getClass().getMethod(invocation.getMethodName(), invocation.getParameterTypes()); return method.invoke(this.wrappedObject, invocation.getArguments()); } }
当获取的stub不是RMIIncationHandler类型,那么服务端的构建 RMI服务可能是通过普通方法,那么处理方式自然会按照RMI中的普通处理方式进行的,而这种普通的处理方式无非是通过反射,因此invocation中包含了所需要调用的方法和各种信息,包含方法名称,方法参数类型,参数值等,因此RemoteInvocation作为方法各种信息的承载体,必不可少,而真正调用的实体正是RMIIncationHandler,因此RMIIncationHandler作为服务端和客户端的约定,也是必不可少的。
之前反复提到过,由于在之前bean的初始化的时候做了服务名称绑定this.registry.bind(serviceName,exportedObject),其中exportedObject其实是被RMIInvocationWrapper进行封装的,也就是说,当其他服务器调用serviceName的RMI服务时,Java会为我们封装其内部操作,而直接会将代码转向RMIInvocationWrapper的invoke方法。因此MyRmiInvocationWrapper不能省略,只要其实现约定MyRmiInvocationHandler的invoke方法即可。
2.服务端代码编写
public class ServerTest1 { public static void main(String[] args) throws Exception { int registryPort = 9999; Registry reg = LocateRegistry.createRegistry(registryPort); Remote exportedObject = new MyRmiInvocationWrapper(new HelloRMIServiceImpl()); UnicastRemoteObject.exportObject(exportedObject, 0); reg.bind("helloRMI", exportedObject); } }
3.客户端代码编写
public class ClientTest1 { public static void main(String[] args) throws Exception { Remote remoteObj = Naming.lookup("rmi://127.0.0.1:9999/helloRMI"); MyRemoteInvocation invocation = new MyRemoteInvocation(); invocation.setArguments(new Object[]{1, 2}); invocation.setMethodName("getAdd"); invocation.setParameterTypes(new Class[]{int.class, int.class}); Object object = ((MyRmiInvocationHandler) remoteObj).invoke(invocation); System.out.println(object); } }
结果输出:
3
扩展
扩展1
网上关于registryClientSocketFactory的使用例子少之又少,我在https://blog.csdn.net/oooyooo/article/details/38705641这篇博客中找到的如何使用的方法
服务端其他的代码不变,在客户端修改myClient 的配置文件,如下
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <bean id="rmiSocketFactory" class="net.sf.ehcache.distribution.ConfigurableRMIClientSocketFactory"> <constructor-arg index="0"><value>10000</value></constructor-arg> </bean> <bean id="myClient" class="org.springframework.remoting.rmi.RmiProxyFactoryBean"> <property name="serviceUrl" value="rmi://127.0.0.1:9999/helloRMI"></property> <property name="serviceInterface" value="com.spring_1_100.test_71_80.test76_spring_rmi.HelloRMIService"></property> <property name="lookupStubOnStartup" value="false"/> <property name="refreshStubOnConnectFailure" value="true"/> <property name="registryClientSocketFactory" ref="rmiSocketFactory" /> </bean> </beans>
测试结果
3
扩展2
同样在https://blog.csdn.net/oooyooo/article/details/38705641这篇博客中,提供了另外一种方案,负载均衡,由于作者只提供了思路,具体的实现并没有完整,因此在这里,我来简单的实现一下吧。
- 创建服务端1
public class ServerTest { public static void main(String[] args) { new ClassPathXmlApplicationContext("classpath:spring_1_100/config_71_80/spring76_rmi/spring76_service.xml"); } } <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!--服务端--> <bean id="helloRMI" class="com.spring_1_100.test_71_80.test76_spring_rmi.HelloRMIServiceImpl"></bean> <!--服务类--> <bean id="myRMI" class="org.springframework.remoting.rmi.RmiServiceExporter"> <!--服务类--> <property name="service" ref="helloRMI"></property> <!--服务名--> <property name="serviceName" value="helloRMI"></property> <!--服务接口--> <property name="serviceInterface" value="com.spring_1_100.test_71_80.test76_spring_rmi.HelloRMIService"></property> <!--服务端--> <property name="registryPort" value="9999"></property> <!--其他的属性自己查看,org.springframework.remoting.RMI.RMIServiceExporter 的类,就知道支持的属性了--> </bean> </beans>
- 创建服务端2
public class ServerTest { public static void main(String[] args) { new ClassPathXmlApplicationContext("classpath:spring_1_100/config_71_80/spring76_rmi/spring76_service1.xml"); } } <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!--服务端--> <bean id="helloRMI" class="com.spring_1_100.test_71_80.test76_spring_rmi.HelloRMIServiceImpl"></bean> <!--服务类--> <bean id="myRMI" class="org.springframework.remoting.rmi.RmiServiceExporter"> <!--服务类--> <property name="service" ref="helloRMI"></property> <!--服务名--> <property name="serviceName" value="helloRMI"></property> <!--服务接口--> <property name="serviceInterface" value="com.spring_1_100.test_71_80.test76_spring_rmi.HelloRMIService"></property> <!--服务端--> <property name="registryPort" value="8866"></property> <!--其他的属性自己查看,org.springframework.remoting.RMI.RMIServiceExporter 的类,就知道支持的属性了--> </bean> </beans>
细心的读者肯定会发现服务端1和服务端2的唯一区别就是端口不同,其他的都相同。
- 创建客户端,首先创建配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="myClient" class="com.spring_1_100.test_71_80.test76_spring_rmi.HelloRMIServiceRemoteInterfaceSelectorImpl"> <property name="roundRobinStrategy" ref="roundRobinStrategy"></property> </bean> <bean id="roundRobinStrategy" class="com.spring_1_100.test_71_80.test76_spring_rmi.RoundRobinStrategy"> <constructor-arg> <map key-type="java.lang.Integer" value-type="java.lang.String"> <entry key="500" value="myClient1"/> <entry key="1000" value="myClient2"/> </map> </constructor-arg> </bean> <bean id="myClient1" class="org.springframework.remoting.rmi.RmiProxyFactoryBean"> <property name="serviceUrl" value="rmi://127.0.0.1:9999/helloRMI"></property> <property name="serviceInterface" value="com.spring_1_100.test_71_80.test76_spring_rmi.HelloRMIService"></property> </bean> <bean id="myClient2" class="org.springframework.remoting.rmi.RmiProxyFactoryBean"> <property name="serviceUrl" value="rmi://127.0.0.1:8866/helloRMI"></property> <property name="serviceInterface" value="com.spring_1_100.test_71_80.test76_spring_rmi.HelloRMIService"></property> </bean> </beans>
上述需要注意的是roundRobinStrategy策略类,策略类中配置了权重范围,权重算法是如果随机数落在0-500范围之内,取myClient1的代理对象,如果随机数落在500-1000范围,则取myClient2代理对象。
public class HelloRMIServiceRemoteInterfaceSelectorImpl implements FactoryBean<Object> { private RoundRobinStrategy roundRobinStrategy; @Override public Object getObject() throws Exception { return roundRobinStrategy.getService(); } @Override public Class<?> getObjectType() { return HelloService.class; } @Override public boolean isSingleton() { return true; } public RoundRobinStrategy getRoundRobinStrategy() { return roundRobinStrategy; } public void setRoundRobinStrategy(RoundRobinStrategy roundRobinStrategy) { this.roundRobinStrategy = roundRobinStrategy; } }
HelloRMIServiceRemoteInterfaceSelectorImpl类需要注意的一点是实现了FactoryBean接口,因此HelloRMIServiceRemoteInterfaceSelectorImpl实际上是HelloService bean工厂,因此在容器中getBean(“myClient”)方法,实际上是调用了HelloRMIServiceRemoteInterfaceSelectorImpl的getObject()方法获取对象。
public class RoundRobinStrategy implements ApplicationContextAware { private Map<Integer, String> weights; private static ApplicationContext applicationContext; public RoundRobinStrategy(Map<Integer, String> weights) { this.weights = weights; } public Map<Integer, String> getWeights() { return weights; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public HelloRMIService getService() { Random random = new Random(); int a = random.nextInt(1000); String serviceName = null; for (Map.Entry<Integer, String> weight : weights.entrySet()) { if (a < weight.getKey()) { serviceName = weight.getValue(); break; } } return applicationContext.getBean(serviceName, HelloRMIService.class); } }
RoundRobinStrategy实现了ApplicationContextAware接口,因此在容器启动过程中会注入applicationContext,拿到applicationContenxt 一切都好办了,自定义负载均衡算法,创建一个1-1000的随机数,如果值落在0-500范围内,则取myClient1的代理对象返回,如果随机数落在500-1000范围内,则取myClient2的代理对象返回,因此就实现了简单的负载均衡了。
- 开始测试
public class ClientTest2 { public static void main(String[] args) { ApplicationContext ct = new ClassPathXmlApplicationContext("classpath:spring_1_100/config_71_80/spring76_rmi/spring76_client2.xml"); HelloRMIService helloRMIService = ct.getBean("myClient",HelloRMIService.class); int c = helloRMIService.getAdd(1,2); System.out.println(c); } }
测试结果:
总结 :
关于RMI的使用及Spring中源码解析,到里就告一段落了,有不懂的或者还有其他问题的小伙伴,可以给我提问,我看到了尽量去解决。下一下篇,我们来解读HttpInvoker的使用及Spring 源码解析。
本文用到的github项目地址
https://github.com/quyixiao/rmiclien.git