远程调用是客户端应用和服务端应用之间的会话。在客户端,它所需要的一些功能并不在该应用的实现范围之内,所以应用要想能提供这些功能的·其他系统寻求帮助。而远程应用通过远程服务暴露这些功能。
RPC(remote-procedure call. RPC)远程过程调用:
就是执行流从一个应用传递给另一个应用,理论上另一个应用部署在跨网络的一台远程机器上。
Spring支持多种不同的RPC模型,暴露RMI,Caucho的Hessian和Burlap以及Spring自带的HTTP invoker。
RPC模型 | 使用场景 |
---|---|
远程方法调用(RMI) | 不考虑网络限制时(例如防火墙),访问/发布基于Java的服务 |
Hessian或Burlap | 考虑网络限制时,通过HTTP访问/发布基于Java的服务。Hessian是二进制协议.而Burlap是基于XML的 |
HTTP invoker | 考虑网络限制,并希望使用基于XML或专有的序列化机制实现Java序列化时,访问/发布基于Spring的服务 |
JAX-RPC和JAX-WS | 访问/fabu1平台独立的、基于SOAP的Web服务 |
不管选择哪种远程调用模型,Spring都提供了风格一致的支持。
在所有的模型中,服务都作为Spring所管理的bean配置到我们的应用中。这是通过一个代理工厂bean实现的,这个bean能够把远程服务像本地对象一样装配到其他bean的属性中去。
客户端向代理发起调用,就像代理提供了这些服务一样。代理代表客户端与远程服务进行通信,由它负责处理连接的细节并向远程服务发起调用。
要记住一点,任何传递给远程调用的bean或从远程调用返回的bean可能需要实现java.io.Serializable接口。
如果你曾经创建过RMI服务,应该会知道这会这几如下几个步骤:
spring提供了更简单的方式来发布RMI服务,不用再编写那些需要抛出RemoteException异常的特定RMI类,只需要简单地编写实现服务功能的POJO就可以了,Spring会处理剩余的事项。
下面是将要发布的服务接口:
import java.util.List;
import com.jpa.domin.User;
public interface RPCService {
List findAllQQEmailUser();
}
实现类:
@Service
public class RPCServiceImpl implements RPCService {
@Autowired
private ISpringDataJpaRepository sdjr;
public List findAllQQEmailUser() {
return sdjr.findAllQQEmailUser();
}
}
将它的实现类RPCServiceImpl发布为RMI服务:
@Bean
@Autowired
public RmiServiceExporter rmiServiceExporter(RPCService rpcService){
RmiServiceExporter exporter = new RmiServiceExporter();
exporter.setService(rpcService);
exporter.setServiceName("RPCService");//命名RMI服务
exporter.setServiceInterface(RPCService.class);//指定此服务所实现的接口
return exporter;
}
RmiServiceExporter 可以把任意Spring管理的bean发布为RMI服务。RmiServiceExporter 把bean包装在一个适配器类中,然后适配器类被绑定到RMI注册表中,并且代理到服务类的请求——在本例中服务类也就是RPCServiceImpl。
默认情况下,RmiServiceExporter 会尝试绑定到本地机器1099端口上的RMI注册表。如果在这个端口没有发现RMI注册表,RmiServiceExporter 将会启动一个注册表。可以通过registryHost和registryPort属性来自定义:
@Bean
@Autowired
public RmiServiceExporter rmiServiceExporter(RPCService rpcService){
RmiServiceExporter exporter = new RmiServiceExporter();
exporter.setService(rpcService);
exporter.setServiceName("RPCService");//命名RMI服务
exporter.setServiceInterface(RPCService.class);//指定此服务所实现的接口
exporter.setRegistryHost("rmi.test.com");//绑定到rmi.test.com主机
exporter.setRegistryPort(1199);//绑定到1199端口上的注册表
return exporter;
}
传统上,RMI客户端必须使用RMI API的Naming类从RMI注册表中查找服务。
而Spring则不需要这么麻烦,Spring的RmiProxyFactoryBean是一个工厂bean,该bean可以为RMI服务创建代理。使用RmiProxyFactoryBean引用RPCService的RMI服务是非常简单的:
@Bean
public RmiProxyFactoryBean rPCService(){
RmiProxyFactoryBean rmiProxy = new RmiProxyFactoryBean();
rmiProxy.setServiceUrl("rmi://localhost/RPCService");//RPCService为RMI服务名
rmiProxy.setServiceInterface(RPCService.class);
return rmiProxy;
}
现在已经把RMI服务声明为Spring管理的bean,我们就可以把它作为依赖装配进另一个bean中,就像任意非远程的bean那样使用了:
@Autowired
private RPCService rPCService;
@RequestMapping("/me")
public String me(){
List users = rPCService.findAllQQEmailUser();
for(User u : users){
System.out.println(u.getUsername());
}
return "me";
}
需要注意的是,在这里的操作中,服务端和客户端都用到了同一个实体User,不仅要确保两个应用中的User一样,而且都实现了序列化,还要确保他们所在的包路径要一直,这是因为在编译class文件的时候,这个class文件内部已经包含了package com.cn;这样的内容,如果在部署的时候,不严格按照这个目录部署的话,服务器就会找不到,就会报no security manager: RMI class loader disabled异常。
RMI是一种实现远程服务交互的好办法,但是它存在某些限制。首先,RMI很难穿越防火墙,这是因为RMI使用任意端口来交互——这是防火墙通常所不允许的。其次,RMI是基于Java的,这意味这客户端和服务端必须都是用Java开发的。因为RMI使用Java的序列化机制,所以通过网络传输的对象类型必需要保证在调用两端的Java运行时中是完全相同的版本。
Hessian和Burlap是Caucho Technology提供的两种基于HTTP的轻量级远程服务解决方案。借助于尽可能简单的API和通信协议,它们都致力于简化web服务。
Hessian,像RMI一样,使用二进制消息进行客户端和服务端的交互。但与其他二进制远程调用技术不同的是,它的二进制消息可以移植到其他非Java的语言中,包括PHP、Python、C++和C#。
Burlap是一种基于XML的远程调用技术,这使得它可以自然而然地移植到任何能够解析XML的语言上。正因为它基于XML,所以相比Hessian的二进制格式而言,Burlap可读性更强。但是和其他基于XML的远程技术不同,Burlap的消息结构尽可能的简单,不需要额外的外部定义语言。
他们之间的唯一区别在于Hessian的消息是二进制的,而Burlap的消息是XML。由于Hessian的消息是二进制的,所以它在带宽上更具优势。但是如果我们更注重可读性或者我们的应用需要与没有Hessian实现的语言交互,那么Burlap的XML消息会是更好的选择。
导出Hessian服务:
同RMI一样,这次只需要把RmiServiceExporter换成HessianServiceExporter
@Bean
@Autowired
public HessianServiceExporter hessianServiceExporter(RPCService rpcService){
HessianServiceExporter serviceExporter = new HessianServiceExporter();
serviceExporter.setService(rpcService);
serviceExporter.setServiceInterface(RPCService.class);
return serviceExporter;
}
这里没有设置serviceName属性,在RMI中,该属性用来在RMI注册表中注册一个服务。而Hessian没有注册表,因此也就没有必要为Hessian服务进行命名。
下图是HessianServiceExporter 将POJO的public方法发布为Hessian服务的实现过程:
配置Hessian控制器:
由于Hessian是基于HTTP的,所以HessianServiceExporter 实现为一个Spring MVC控制器。这就意味这为了使用导出的Hessian服务,我们需要执行两个额外的配置步骤:
这里我们使用Java配置:
public class MVCInitializer extends AbstractAnnotationConfigDispatcherServletInitializer{
@Override
protected String[] getServletMappings() {
// TODO Auto-generated method stub
return new String[]{"/", "*.service"};//在原有的基础上加上*.service,是DispatcherServlet能够拦截后缀为*.service的URL
}
}
@Bean
public HandlerMapping hessianMapping(){
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
Properties mappings = new Properties();
mappings.setProperty("/rpc.service", "hessianServiceExporter");
mapping.setMappings(mappings);
return mapping;
}
hessianServiceExporter就是HessianServiceExporter 的bean name。
和RMI一样,客户端我们只需要配置一个代理就可以了:
@Bean
public HessianProxyFactoryBean rPCService(){
HessianProxyFactoryBean proxy = new HessianProxyFactoryBean();
proxy.setServiceUrl("http://localhost:8080/spring_jpa/rpc.service");
proxy.setServiceInterface(RPCService.class);
return proxy;
}
如果使用Burlap,对应的服务端为:
@SuppressWarnings("deprecation")
@Bean
@Autowired
public BurlapServiceExporter burlapServiceExporter(RPCService rpcService){
BurlapServiceExporter serviceExporter = new BurlapServiceExporter();
serviceExporter.setService(rpcService);
serviceExporter.setServiceInterface(RPCService.class);
return serviceExporter;
}
客户端:
@Bean
public BurlapProxyFactoryBean rPCService(){
BurlapProxyFactoryBean proxy = new BurlapProxyFactoryBean();
proxy.setServiceUrl("http://localhost:8080/spring_jpa/rpc.service");
proxy.setServiceInterface(RPCService.class);
return proxy;
}
因为Hessian和Burlap都是基于HTTP的,它们都解决了RMI所头疼的防火墙渗透问题。但是当传递过来的RPC消息中包含序列化对象时,RMI就完胜Hessian和Burlap了。因为Hessian和Burlap都采用了私有的序列化机制,而RMI使用的是Java本身的序列化机制。如果我们的数据模型非常复杂,Hessian/Burlap的序列化模型就可能无法胜任了。
还有一个两全其美的方案,那就是Spring HTTP invoke,它基于HTTP(像Hessian和Burlap一样)提供了RPC,同时有使用了Java的对象序列化机制(像RMI一样)。
HTTP invoker是一个新的远程调用模型,作为Spring 框架的一部分,能够执行基于HTTP的远程调用(让防火墙不为难),并使用Java的序列化机制(让开发者也乐观其变)。
配置HTTP invoker和配置Hessian/Burlap过程是一样的,唯一的区别在于两个bean不一样,一个是导出bean(服务端),一个是代理bean(客户端)。
服务端:
@Bean
@Autowired
public HttpInvokerServiceExporter httpInvokerServiceExporter(RPCService rpcService){
HttpInvokerServiceExporter serviceExporter = new HttpInvokerServiceExporter();
serviceExporter.setService(rpcService);
serviceExporter.setServiceInterface(RPCService.class);
return serviceExporter;
}
客户端:
@Bean
public HttpInvokerProxyFactoryBean rPCService(){
HttpInvokerProxyFactoryBean proxy = new HttpInvokerProxyFactoryBean();
proxy.setServiceUrl("http://localhost:8080/spring_jpa/rpc.service");
proxy.setServiceInterface(RPCService.class);
return proxy;
}
其他的比如配置DispatcherServlet的拦截请求,以及URL映射都是一样的。只需要把给定的URL映射到httpInvokerServiceExporter bean即可。
@Override
protected String[] getServletMappings() {
// TODO Auto-generated method stub
return new String[]{"/", "*.service"};//在原有的基础上加上*.service,是DispatcherServlet能够拦截后缀为*.service的URL
}
@Bean
public HandlerMapping hessianMapping(){
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
Properties mappings = new Properties();
mappings.setProperty("/rpc.service", "httpInvokerServiceExporter");
mapping.setMappings(mappings);
return mapping;
}
需要记住的是HTTP Invoker有一个重大的限制:它只是一个Spring框架所提供的远程调用解决方案。者意味着客户端和服务端必须都是Spring应用。并且,至少目前而言,也隐含表明客户端和服务端必须是基于Java的。另外,因为使用了Java的序列化机制,客户端和服务端必须使用相同版本的类(与RMI类似)。