Spring源码深度解析(郝佳)-学习-RMI使用及Spring源码解读

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

RMI

        在Spring中,同样提供了对RMI的支持,使得在Spring 下的RMI开发变得更加方便,同样,我们还是通过示例来快速的体验RMI所提供的功能。

使用示例

        以下提供了Spring整合RMI的使用示例

  1. 建立RMI对外接口
public interface HelloRMIService {
    int getAdd(int a ,int b );
}
  1. 建立接口实现类
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 ;
    }
}
  1. 建立服务端配置文件
<?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来连接本机了。

  1. 完成了服务端的配置后,还需要在测试端建立测试环境以及测试代码,首先测试端配置文件。
<?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>
  1. 编写测试代码
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()。

  1. 通过代理拦截并获取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的各种参数 。

  1. 增强器进行远程连接

  之前分析了类型为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. 创建服务端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>
  1. 创建服务端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的唯一区别就是端口不同,其他的都相同。

  1. 创建客户端,首先创建配置文件
<?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的代理对象返回,因此就实现了简单的负载均衡了。

  1. 开始测试
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

https://github.com/quyixiao/spring_tiny/tree/master/src/main/java/com/spring_1_100/test_71_80/test76_spring_rmi

https://github.com/quyixiao/mybatis_plugin

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值