本文引用 : https://yinwj.blog.csdn.net/article/details/49120813
RMI : Remote Method Invocation,远程方法调用
RPC : Remote Procedure Call Protocol, 远程过程调用协议
ESB : Enterprise Service Bus, 企业服务总线
SOA : Service-Oriented Architecture, 面向服务的架构
1. 概述
在这个章节我将通过对RMI的详细介绍,引出一个重要的系统间通信的管理规范RPC,并且继续讨论一些RPC的实现;再通过分析PRC的技术特点,引出另一种系统间通信的管理规范ESB,并介绍ESB的一些具体实现。最后我们介绍SOA:面向服务的软件架构。
2. RMI基本使用
RMI(Remote Method Invocation,远程方法调用),是JAVA早在JDK 1.1中提供的JVM与JVM之间进行 对象方法调用的技术框架的实现(在JDK的后续版本中,又进行了改进)。通过RMI技术,某一个本地的JVM可以调用存在于另外一个JVM中的对象方法,就好像它仅仅是在调用本地JVM中某个对象方法一样。例如RMI客户端中的如下调用:
List< UserInfo > users = remoteServiceInterface.queryAllUserinfo();
看似remoteServiceInterface对象和普通的对象没有区别,但实际上remoteServiceInterface对象的具体方法实现却不在本地的JVM中,而是在某个远程的JVM中(这个远程的JVM可以是RMI客户端同属于一台物理机,也可以属于不同的物理机)
1.1 RMI使用场景
RMI是基于JAVA语言的,也就是说在RMI技术框架的描述中,只有Server端使用的是JAVA语言并且Client端也是用的JAVA语言,才能使用RMI技术(目前在codeproject.com中有一个开源项目名字叫做“RMI for C++”,可以实现JAVA To C++的RMI调用。但是这是一个第三方的实现,并不是java的标准RMI框架定义,所以并不在我们的讨论范围中)。
RMI适用于两个系统都主要使用JAVA语言进行构造,不需要考虑跨语言支持的情况。并且对两个JAVA系统的通讯速度有要求的情况。
RMI 是一个良好的、特殊的RPC实现:使用JRMP协议承载数据描述,可以使用BIO和NIO两种IO通信模型。
1.2 RMI框架的基本组成
虽然RMI早在JDK.1.1版本中就开放了。但是在JDK1.5的版本中RMI又进行改进。所以我们后续的代码示例和原理讲解都基于最新的RMI框架特性。
要定义和使用一套基于RMI框架工作的系统,您至少需要做一下几个工作:
- 定义RMI Remote接口
- 实现这个RMI Remote接口
- 生成Stub(桩)和Skeleton(骨架)。这一步的具体操作视不同的JDK版本而有所不同(例如JDK1.5后,Skeleton不需要手动);“RMI注册表”的工作方式也会影响“Stub是否需要命令行生成”这个问题。
- 向“RMI注册表”注册在第2步我们实现的RMI Remote接口。
- 创建一个Remote客户端,通过java“命名服务”在“RMI注册表”所在的IP:PORT寻找注册好的RMI服务。
- Remote客户端向调用存在于本地JVM中对象那样,调用存在于远程JVM上的RMI接口。
下图描述了上述几个概念名称间的关系,呈现了JDK.5中RMI框架其中一种运行方式(注意,是其中一种工作方式。也就是说RMI框架不一定都是这种运行方式,后文中我们还将描述另外一种RMI的工作方式):
1.3 RMI示例代码
在这个代码中,我们将使用“本地RMI注册表”(LocateRegistry),让RMI服务的具体提供者和RMI注册表工作在同一个JVM上,向您介绍最基本的RMI服务的定义、编写、注册和调用过程:
首先我们必须定义RMI 服务接口,代码如下:
package testRMI;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
import testRMI.entity.UserInfo;
public interface RemoteServiceInterface extends Remote {
/**
* 这个RMI接口负责查询目前已经注册的所有用户信息
*/
public List<UserInfo> queryAllUserinfo() throws RemoteException;
}
很简单的代码,应该不用多解释什么了。这个定义的接口方法如果放在某个业务系统A中,您可以理解是查询这个系统A中所有可用的用户资料。注意这个接口所继承的java.rmi.Remote接口,是“RMI服务接口”定义的特点。
那么有接口定义了,自然就要实现这个接口:
package testRMI;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.List;
import testRMI.entity.UserInfo;
/**
* RMI 服务接口RemoteServiceInterface的具体实现<br>
* 请注意这里继承的是UnicastRemoteObject父类。
* 继承于这个父类,表示这个Remote Object是“存在于本地”的RMI服务实现
* (这句话后文会解释)
* @author yinwenjie
*
*/
public class RemoteUnicastServiceImpl extends UnicastRemoteObject implements RemoteServiceInterface {
/**
* 注意Remote Object没有默认构造函数
* @throws RemoteException
*/
protected RemoteUnicastServiceImpl() throws RemoteException {
super();
}
private static final long serialVersionUID = 6797720945876437472L;
/* (non-Javadoc)
* @see testRMI.RemoteServiceInterface#queryAllUserinfo()
*/
@Override
public List<UserInfo> queryAllUserinfo() throws RemoteException {
List<UserInfo> users = new ArrayList<UserInfo>();
UserInfo user1 = new UserInfo();
user1.setUserAge(21);
user1.setUserDesc("userDesc1");
user1.setUserName("userName1");
user1.setUserSex(true);
users.add(user1);
UserInfo user2 = new UserInfo();
user2.setUserAge(21);
user2.setUserDesc("userDesc2");
user2.setUserName("userName2");
user2.setUserSex(false);
users.add(user2);
return users;
}
}
还有我们定义的Userinfo信息,就是一个普通的POJO对象:
package testRMI.entity;
import java.io.Serializable;
import java.rmi.RemoteException;
public class UserInfo implements Serializable {
/**
*
*/
private static final long serialVersionUID = -377525163661420263L;
private String userName;
private String userDesc;
private Integer userAge;
private Boolean userSex;
public UserInfo() throws RemoteException {
}
/**
* @return the userName
*/
public String getUserName() {
return userName;
}
/**
* @param userName the userName to set
*/
public void setUserName(String userName) {
this.userName = userName;
}
/**
* @return the userDesc
*/
public String getUserDesc() {
return userDesc;
}
/**
* @param userDesc the userDesc to set
*/
public void setUserDesc(String userDesc) {
this.userDesc = userDesc;
}
/**
* @return the userAge
*/
public Integer getUserAge() {
return userAge;
}
/**
* @param userAge the userAge to set
*/
public void setUserAge(Integer userAge) {
this.userAge = userAge;
}
/**
* @return the userSex
*/
public Boolean getUserSex() {
return userSex;
}
/**
* @param userSex the userSex to set
*/
public void setUserSex(Boolean userSex) {
this.userSex = userSex;
}
}
RMI Server 的接口定义和RMI Server的实现都有了,那么编写代码的最后一步是**将这个RMI Server注册到“RMI 注册表”中运行。这样 RMI的客户端就可以调用这个 RMI Server了。**下面的代码是将RMI Server注册到“本地RMI 注册表”中:
package testRMI;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RemoteUnicastMain {
public static void main(String[] args) throws Exception {
/*
* Locate registry,您可以理解成RMI服务注册表,或者是RMI服务位置仓库。
* 主要的作用是维护一个“可以正常提供RMI具体服务的所在位置”。
* 每一个具体的RMI服务提供者,都会讲自己的Stub注册到Locate registry中,以表示自己“可以提供服务”
*
* 有两种方式可以管理Locate registry,一种是通过操作系统的命令行启动注册表;
* 另一种是在代码中使用LocateRegistry类。
*
* LocateRegistry类中有一个createRegistry方法,可以在这台物理机上创建一个“本地RMI注册表”
* */
LocateRegistry.createRegistry(1099);
// 以下是向LocateRegistry注册(绑定/重绑定)RMI Server实现。
RemoteUnicastServiceImpl remoteService = new RemoteUnicastServiceImpl();
// 通过java 名字服务技术,可以讲具体的RMI Server实现绑定一个访问路径。注册到LocateRegistry中
Naming.rebind("rmi://127.0.0.1:1099/queryAllUserinfo", remoteService);
/*
* 在“已经拥有某个可访问的远程RMI注册表”的情况下。
* 下面这句代码就是向远程注册表注册RMI Server,
* 当然远程RMI注册表的JVM-classpath中一定要有这个Server的Stub存在
*
* (运行在另外一个JVM上的RMI注册表,可能是同一台物理机也可能不是同一台物理机)
* Naming.rebind("rmi://192.168.61.1:1099/queryAllUserinfo", remoteService);
* */
}
}
这样我们后续编写的Client端就可以调用这个RMI Server了。下面的代码是RMI Client的代码:
package testRMI;
import java.rmi.Naming;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.BasicConfigurator;
import testRMI.entity.UserInfo;
/**
* 客户端调用RMI测试
* @author yinwenjie
*
*/
public class RemoteClient {
static {
BasicConfigurator.configure();
}
/**
* 日志
*/
private static final Log LOGGER = LogFactory.getLog(RemoteClient.class);
public static void main(String[] args) throws Exception {
// 您看,这里使用的是java名称服务技术进行的RMI接口查找。
RemoteServiceInterface remoteServiceInterface = (RemoteServiceInterface)Naming.lookup("rmi://192.168.61.1/queryAllUserinfo");
List<UserInfo> users = remoteServiceInterface.queryAllUserinfo();
RemoteClient.LOGGER.info("users.size() = " +users.size());
}
}
那么怎么来运行这段代码呢?如果您使用的是eclipse编写了您第一个RMI Server和RMI Client,并且您使用的是“本地RMI 注册表”。那么您不需要做任何的配置、脚本指定等工作(包括不需要专门设置JRE权限、不需要专门指定classpath、不需要专门生成Stub和Skeleton),就可以看到RMI的运行和调用效果了:
下图为RemoteUnicastMain的效果RMI 服务注册和执行效果:
可以看到,RemoteUnicastMain中的代码执行完成后整个应用程序没有退出。如下图:
这是因为这个应用程序要承担“真实的RMI Server实现”的服务调用。如果它退出,RMI 注册表就无法请求真实的服务实现了
我们再来看下图,RemoteClient调用RMI 服务的效果:
很明显控制台将返回:
0 [main] INFO testRMI.RemoteClient - users.size() = 2
3. JAVA RMI 原理
通过上面的两组代码,我们大概知道了RMI框架是如何使用的。下面我们来讲解一下RMI的基本原理。
3.1 Registry和Stub、Skeleton的关系
-
一定要说明,在RMI Client实施正式的RMI调用前,它必须通过LocateRegistry或者Naming方式到RMI注册表寻找要调用的RMI注册信息。找到RMI事务注册信息后,Client会从RMI注册表获取这个RMI Remote Service的Stub信息。这个过程成功后,RMI Client才能开始正式的调用过程。
-
另外要说明的是RMI Client正式调用过程,也不是由RMI Client直接访问Remote Service,而是由客户端获取的Stub作为RMI Client的代理访问Remote Service的代理Skeleton,如上图所示的顺序。也就是说真实的请求调用是在Stub-Skeleton之间进行的。
-
Registry并不参与具体的Stub-Skeleton的调用过程,只负责记录“哪个服务名”使用哪一个Stub,并在Remote Client询问它时将这个Stub拿给Client
3.2 Remote-Service线程管理
在上文中的演示我们看到了RemoteRegistryUnicastMain处理请求时,使用了线程池。这是JDK1.5到JDK1.6+版本中RMI框架的做的一个改进。包括JDK1.5在内,之前的版本都采用新建线程的方式来处理请求;在JDK1.6版本之后,改用了线程池,并且线程池的大小是可以调整的:
-
sun.rmi.transport.tcp.maxConnectionThreads:连接池的大小,默认为无限制。无限的大小肯定是有问题,按照Linux单进程可打开的最大文件数限制,建议的设置值为65535(生产环境)。如果同一时间连接池中的线程数量达到了最大值,那么后续的Client请求将会报错。测试环境/开发环境是否设置这个值,就没有那么重要了。
-
sun.rmi.transport.tcp.threadKeepAliveTime:如果当线程池中有闲置的线程资源的话,那么这个闲置线程资源多久被注销(单位毫秒),默认的设置是1分钟。
如果您使用的是linux或者window的命令控制台执行的话,您可以通过类似如下语句进行参数设置:
java -Dsun.rmi.transport.tcp.maxConnectionThreads=2 -Dsun.rmi.transport.tcp.threadKeepAliveTime=1000 testRMI.RemoteRegistryUnicastMain
3.3 Registry和Naming
Registry和Naming都可以进行RMI服务的bind/rebind/unbind,都可以用lookup方法查询RMI服务。Naming实际上是对Registry的封装。使用完整的URL方式对已注册的服务名进行查找。
3.4 UnicastRemoteObject和Activatable
在JDK1.2版本中,由Ann Wollrath执笔加入了一种新的RMI工作方式。即通过RMI“活化”模式,将Remote Service的真实提供者移植到RMI Registry注册表所在的JVM上。要使用这种工作模式的Remote Service实现不再继承UnicastRemoteObject类,而需要继承Activatable类(其他的业务代码不需要改变)
4. RMI:一种特殊的RPC服务实现
之所以介绍RMI,是因为要通过介绍RMI引出一种重要的系统间通讯管理框架RPC.