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 {@Overridepublic 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 registryif (this.registry == null) {this.registry = getRegistry(this.registryHost, this.registryPort,this.registryClientSocketFactory, this.registryServerSocketFactory);this.createdRegistry = true;}//初始化以及从缓存中导出Object//此时通常情况下使用RMIInvocationWrapper封装JDK代理类,切面为RemoteInvocationTraceInterceptorthis.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不为空则尝试获取对应的主机RegistryRegistry reg = LocateRegistry.getRegistry(registryHost, registryPort, clientSocketFactory);testRegistry(reg);return reg;}else {//获取本机的Registryreturn 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");//根据端口创建Registryreturn 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 servicereturn (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() {//验证servicecheckService();//验证serviceInterfacecheckServiceInterface();//使用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属性便会在启动时寻找stubif (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) {//获取stubthis.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 {@Overridepublic 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;}@Overridepublic 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;@Overridepublic Object getObject() throws Exception {return roundRobinStrategy.getService();}@Overridepublic Class<?> getObjectType() {return HelloService.class;}@Overridepublic 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;}@Overridepublic 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

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

  1. Spring源码深度解析(郝佳)-学习-源码解析-基于注解bean定义(一)

    我们在之前的博客 Spring源码深度解析(郝佳)-学习-ASM 类字节码解析 简单的对字节码结构进行了分析,今天我们站在前面的基础上对Spring中类注解的读取,并创建BeanDefinition做 ...

  2. Spring源码深度解析(郝佳)-学习-源码解析-创建AOP静态代理实现(八)

    继上一篇博客,我们继续来分析下面示例的 Spring 静态代理源码实现. 静态 AOP使用示例 加载时织入(Load -Time WEaving,LTW) 指的是在虚拟机载入字节码时动态织入 Aspe ...

  3. Spring源码深度解析(郝佳)-学习-源码解析-基于注解切面解析(一)

    我们知道,使用面积对象编程(OOP) 有一些弊端,当需要为多个不具有继承关系的对象引入同一个公共的行为时,例如日志,安全检测等,我们只有在每个对象引用公共的行为,这样程序中能产生大量的重复代码,程序就 ...

  4. Spring源码深度解析(郝佳)-学习-源码解析-Spring MVC(三)-Controller 解析

    在之前的博客中Spring源码深度解析(郝佳)-学习-源码解析-Spring MVC(一),己经对 Spring MVC 的框架做了详细的分析,但是有一个问题,发现举的例子不常用,因为我们在实际开发项 ...

  5. Spring源码深度解析(郝佳)-学习-源码解析-基于注解注入(二)

    在Spring源码深度解析(郝佳)-学习-源码解析-基于注解bean解析(一)博客中,己经对有注解的类进行了解析,得到了BeanDefinition,但是我们看到属性并没有封装到BeanDefinit ...

  6. Spring源码深度解析(郝佳)-学习-源码解析-Spring整合MyBatis

    了解了MyBatis的单独使用过程之后,我们再来看看它也Spring整合的使用方式,比对之前的示例来找出Spring究竟为我们做了什么操作,哪些操作简化了程序开发. 准备spring71.xml &l ...

  7. Spring源码深度解析(郝佳)-学习-源码解析-创建AOP静态代理(七)

    加载时织入(Load-Time Weaving ,LTW) 指的是在虚拟机加载入字节码文件时动态织入Aspect切面,Spring框架的值添加为 AspectJ LTW在动态织入过程中提供了更细粒度的 ...

  8. Spring源码深度解析(郝佳)-学习-构造器注入

    本文主要是Spring源码有一定基础的小伙伴而言的,因为这里我只想讲一下,Spring对于构造器的注入参数是如何解析,不同参数个数构造器. 相同参数个数,不同参数类型. Spring是如何选择的. 1 ...

  9. Spring源码深度解析(郝佳)-学习-源码解析-factory-method

    本文要解析的是Spring factory-method是如何来实现的,话不多说,示例先上来. Stu.java public class Stu {public String stuId;publi ...

最新文章

  1. .NET程序设计之四书五经
  2. android mapbox 添加多个点,使用Android Mapbox SDK显示多个标记的自定义infoWindow
  3. android使用的图片压缩格式,Android 之使用libjpeg压缩图片
  4. [机器学习] XGBoost 自定义损失函数-FocalLoss
  5. 【转】一步一步教你远程调用EJB
  6. OpenCV2.4.5在13-04的配置过程
  7. 基础知识(九)boost+vs2015安装配置
  8. django--cookie与session
  9. Java内部类(转)
  10. html辅助方法实现原理,前端每日实战:苦练 CSS 基本功——图解辅助线的原理和画法...
  11. 三维空间中无人机路径规划的改进型蝙蝠算法
  12. 计算机变成英语,win10系统下计算器界面变成英文界面了怎么办
  13. 家庭用计算机音响,7.1声道THX家庭影院音箱摆位计算器
  14. 蓝牙芯片解决方案市场规模
  15. Android自定义Lint检查-CustomLint
  16. 网络服务器带宽Mbps、Mb/s、MB/s有什么区别?10M、100M到底是什么概念?
  17. PDF Shaper Professional v11.3 全能PDF工具箱单文件版
  18. 互联网日报 | 吉利汽车完成科创板上市辅导;华为开发者大会9月10日举行;贵州茅台整治“年份酒”乱象...
  19. 音频社交的变声,应用了哪些算法?
  20. 颜色空间内容讲解与图像分割应用

热门文章

  1. 基于STM32F1的手势识别PJ7602和测温报警MLX90614
  2. ios12后获取iOS设备WiFi名字和mac地址
  3. MP3 PQF模块 matlab实现
  4. 我的第一张MCSE 2012证书
  5. 基于IIC总线的温湿度传感器,你用过吗
  6. 阿里云服务器ECS年终特惠,云服务器报价出炉
  7. 微软的中国MVP名录
  8. 生活网络常识—CAT1与CAT4的区别
  9. 华云大咖说 | 对象存储在金融行业的应用
  10. ADS42LB69使用记录