本文基于《Spring实战(第4版)》所写。
我们有多种可以使用的远程调用技术,包括:
- 远程方法调用(Remote Method Invocation, RMI);
- Caucho的Hessian和Burlap;
- Spring基于HTTP的远程服务;
- 使用JAX-RPC和JAX-WS的Web Service。
Spring远程调用概览
远程调用是客户端应用和服务端之间的会话。在客户端,它所需要的一些共功能并不在该应用的实现范围之内,所以应用要向能提供这些功能的其他系统寻求帮助。而远程应用通过远程服务暴露这些功能。
下表概述了每个一个Spring支持的RPC模型,并简要讨论了它们所适用的不同场景。
RPC模型 | 适用场景 |
---|---|
远程方法调用(RMI) | 不考虑网络限制时(例如防火墙),访问/发布基于Java的服务 |
Hessian或Burlap | 考虑网络限制时,通过HTTP访问/发布基于Java的服务。Hessian是二进制协议,而Burlap是基于XML的(Spring 5.0不支持Burlap了) |
HTTP invoker | 考虑网络限制,并希望使用基于XML或专有的序列化机制实现Java序列化时,访问/发布基于Spring的服务 |
JAX-PRC和JAX-WS | 访问/发布平台独立的、基于SOAP的Web服务 |
在所有的模型中,服务都作为Spring所管理的bean配置到我们的应用中。这是通过一个代理工厂bean实现的,这个bean能够把远程服务像本地对象一样装配到其他bean的属性中去。下图展示了它是如何工作的。
客户端向代理发起作用,就像代理提供了这些服务一样。代理代表客户端与远程服务进行通信,由它负责处理连接的细节并向远程服务发起调用。
更重要的是,如果调用远程服务时发生java.rmi.RemoteException异常,代理会处理此异常并重新抛出非检查型异常RemoteAccessException。远程异常通常预示着系统发生了无法优雅恢复的问题,如网络或配置问题。既然客户端通常无法从远程异常中恢复,那么重新抛出RemoteAccessException异常就能让客户端决定是否处理此异常。
在服务器端,我们可以使用上表所列出的任意一种模型将Spring管理的bean发布为远程服务。下图展示了远程导出器(remote exporter)如何将bean方法发布为远程服务。
无论我们开发的是使用远程服务的代码,还是实现这些服务的代码,或者两者兼而有之,在Spring中,使用远程服务纯粹是一个配置问题。我们不需要编写任何Java代码就可以支持远程调用。我们的服务bean也不需要关心他们是否参与了一个RPC(当然,任何传递给远程调用的bean或从远程调用返回的bean可能需要实现java.io.Serializable接口)。
使用RMI
RMI涉及到好几个步骤,包括程序的和手工的。Spring简化了RMI模型,它提供了一个代理工厂bean,能让我们把RMI服务像本地JavaBean那样装配到我们的Spring应用中。Spring还提供了一个远程导出器,用来简化把Spring管理的bean转换为RMI服务的工作。
导出RMI服务
创建RMI服务,会涉及如下几个步骤:
- 编写一个服务实现类,类中的方法必须抛出java.rmi.RemoteException异常;
- 创建一个继承于java.rmi.Remote的服务接口;
- 运行RMI编译器(rmic),创建客户端stub类和服务端skeleton类;
- 启动一个RMI注册表,以便持有这些服务;
- 在RMI注册表中注册服务。
在Spring中配置RMI服务
Spring只需简单地编写实现服务功能的POJO就可以了,Spring会处理剩余的其他事项。
我们将要创建RMI服务需要发布SpitterService接口中的方法,如下的程序清单展现了该接口定义
package spittr.web;
import spittr.model.Spitter;
import spittr.model.Spittle;
import java.util.List;
public interface SpitterService {
List getRecentSpittles(int count);
void saveSpittle(Spittle spittle);
void saveSpitter(Spitter spitter);
Spitter getSpitter(long id);
void startFollowing(Spitter follower, Spitter followee);
List getSpittlesForSpitter(Spitter spitter);
List getSpittlesForSpitter(String username);
Spitter getSpitter(String username);
Spittle getSpittleById(long id);
void deleteSpittle(long id);
List getAllSpitters();
}
如果使用传统的RMI来发布服务,SpitterService和SpitterServiceImpl中的所有方法都需要抛出java.rmi.RemoteException。但是如果使用Spring的RmiServiceExporter把该类转变为RMI服务,那现有的实现不需要做任何改变。
RmiServiceExporter可以把任意Spring管理的bean发布为RMI服务。如下图所示,RmiServiceExporter把bean包装在一个适配器类中,然后适配器类被绑定到RMI注册表中,并且代理到服务类的请求—在本例中服务类也就是SpitterServiceImpl。
使用RmiServiceExporter将SpitterServiceImpl发布为RMI服务的最简单方式是在Spring中使用如下的@Bean方法进行配置:
@Bean
public RmiServiceExporter rmiServiceExporter(SpitterService spitterService){
RmiServiceExporter rmiExporter = new RmiServiceExporter();
rmiExporter.setService(spitterService);
rmiExporter.setServiceName("SpitterService");
rmiExporter.setServiceInterface(SpitterService.class);
// rmiExporter.setRegistryHost("rmi.spitter.com"); // 可以不用,直接用本机地址
// rmiExporter.setRegistryPort(1199); // 可以不用,默认1099
return rmiExporter;
}
这就是使用Spring把某个bean转变为RMI服务所需要做的全部工作。现在Spitter服务已经导出为RMI服务,我们可以为Spittr应用创建其他的用户界面或邀请第三方使用此RMI服务创建新的客户端。如果使用Spring,客户端开发者访问Spitter的RMI服务会非常容易。
也可以使用XML配置
需要注意的是,由于rmi服务一般来讲使用jar包直接启动,所以我们还在工程中建立一个主函数
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
public static void main(String[] args) throws InterruptedException {
// System.setProperty("java.rmi.server.hostname", "192.168.68.115");
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("app.xml");
}
}
然后打包为jar,并在Linux中执行,命令请看在linux下发布jar包
如果使用jar启服务时,提示没有“没有主清单属性”,请看maven生成jar,提示没有“没有主清单属性”
启动服务后,如果提示
[root@VM_0_17_centos ftpUser]# java -jar SpittrServer-1.0-SNAPSHOT.jar
四月 19, 2018 10:52:45 上午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@22d8cfe0: startup date [Thu Apr 19 10:52:45 CST 2018]; root of context hierarchy
四月 19, 2018 10:52:45 上午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [app.xml]
四月 19, 2018 10:52:46 上午 org.springframework.remoting.rmi.RmiServiceExporter getRegistry
信息: Looking for RMI registry at port '1099'
四月 19, 2018 10:52:46 上午 org.springframework.remoting.rmi.RmiServiceExporter getRegistry
信息: Could not detect RMI registry - creating new one
四月 19, 2018 10:52:46 上午 org.springframework.remoting.rmi.RmiServiceExporter prepare
信息: Binding service 'SpitterServer' to RMI registry: RegistryImpl[UnicastServerRef [liveRef: [endpoint:[10.45.***.***:1099](local),objID:[0:0:0, 0]]]]
则表示服务请用成功。如果报Connection Refused,或者endpoint:后面的IP为127.0.0.1都表明服务启用不成功。
解决方法有两种
- 在代码中,启用服务前添加如下语句
System.setProperty("java.rmi.server.hostname", "192.168.68.115");
"192.168.68.115"表示当前IP,可以是局域网地址,也可以是外网地址
推荐本地调用时使用
- 如果是Linux系统,先 vim /etc/hosts ,文件内容如下:
127.0.0.1 localhost localhost.localdomain VM_0_17_centos
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
"VM_0_17_centos"是机器名
修改为
140.143.234.154 VM_0_17_centos localhost localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
下面转换一下视角,来看看如何编写Spitter RMI服务的客户端。
装配RMI服务
传统上,RMI客户端必须使用RMI API的Naming类从RMI注册表中查找服务。例如,下面的代码片段演示了如何获取Spitter的RMI服务:
try{
String serviceUrl = "rmi:/spitter/SpitterService";
SpitterService spitterService = (SpitterService) Naming.lookup(serviceUrl);
...
}
catch (RemoteException e) { ... }
catch (NotBoundException e) { ... }
catch (MalformedURLException e) { ... }
虽然这段代码可以获取Spitter的RMI服务的引用,但是它存在两个问题:
- 传统的RMI查找可能会导致3种检查型异常的任意一种(RemoteException、NotBoundException和MalformedURLException),这些异常必须被捕获或重新抛出;
- 需要Spitter服务的任何代码都必须自己负责获取该服务。这属于样板代码,与客户端的功能并没有直接关系。
Spring的RmiProxyFactoryBean是一个工厂bean,该bean可以为RMI服务创建代理。使用RmiProxyFactoryBean引用SpitterService的RMI服务是非常简单的,只需要在客户端的Spring配置中增加如下的@Bean方法:
@Bean
public RmiProxyFactoryBean spitterService() {
RmiProxyFactoryBean rmiProxy = new RmiProxyFactoryBean();
rmiProxy.setServiceUrl("rmi://192.168.68.115:1099/SpitterService");
rmiProxy.setServiceInterface(SpitterService.class);
return rmiProxy;
}
也可以使用XML
服务的URL是通过RmiProxyFactoryBean的serviceUrl属性来设置的,在这里,服务名被设置为SpitterService,并且声明服务是在本地机器上的;同时服务提供的接口由serviceInterface属性来指定。下图展示了客户端和RMI代理和交互。
现在已经把RMI服务声明为Spring管理的bean,我们就可以把它作为依赖装配进另一个bean中,就像任意非远程的bean的那样。例如,假设客户端需要使用Spitter服务为指定的用户获取Spittle列表,我们可以使用@Autowired注解把服务代理装配进客户端中:
@Autowired
SpitterService spitterService;
我们还可以像本地bean一样调用它的方法:
public List getSpittle(String userName) {
Spitter spitter = spitterService.getSpitter(userName);
return spitterService.getSpittlesForSpitter(spitter);
}
此外,代理捕获了这个服务所有可能抛出的RemoteException异常,并把它包装为运行期异常重新抛出,这样我们就可以放心地忽略这些异常。
如果用于调试可创建一个主函数调用
import client.ClientInvoke;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("app.xml");
ClientInvoke clientInvoke = (ClientInvoke) context.getBean("clientInvoke");
clientInvoke.test();
context.close();
}
}
提醒一下,本例中调用了两次服务,都会受网络延迟的影响,进而可能会影响到客户端的性能。
RMI是一种实现远程服务交互的好办法,但是它存在某些限制。首先,RMI很难穿越防火墙,这是因为RMI使用任意端口来交互—这是防火墙通常所不允许的。
另外一件需要考虑的事情是RMI是基于Java的。这意味这客户端和服务端必须都是用Java开发的。因为RMI使用了Java的序列化机制,所以通过网络传输的对象类型必须要保证在调用两端的Java运行时中是完全相同的版本。
使用Hessian发布远程服务
Hessian和Burlap是Caucho Technology提供的两种基于HTTP的轻量级远程服务解决方案。
- Hessian,基于二进制消息进行交互。可在Java、PHP、Python、C++和C#。由于二进制交互,带宽上更具优势。
- Burlap,基于XML的远程调用技术。它的消息结构尽可能的简单,不需要额外的外部定义语句(例如WSDL或IDL),Spring 5.0不支持Burlap了。
使用Hessian导出bean的功能
像之前一样,把SpitterServiceImpl类的功能发布为远程服务—这次是一个Hessian服务。我们只需要编写一个继承com.caucho.hessian.server.HessianServlet的类,并确保所有的服务方法是public的(在Hessian里,所有public方法被视为服务方法)。
和Spring一起使用时,可利用Spring的AOP来为Hessian服务提供系统级服务,例如声明式事务。
导出Hessian服务
为了把Spitter服务bean发布为Hessian服务,我们需要配置另一个导出bean,只不过这次是HessianServiceExporter。
它把POJO的public方法发布成Hessian服务的方法。不过正如下图所示,其实现过程与RmiServiceExporter将POJO发布为RMI服务是不同的。
HessianServiceExporter是一个Spring MVC控制器,它接收Hessian请求,并把这些请求转换成对被POJO的调用从而将POJO导出为一个Hessian服务。在如下Spring的声明中,HessianServiceExporter会把spitterService bean导出为Hessian服务:
@Bean
public HessianServiceExporter hessianExportedSpitterService(SpitterService service) {
HessianServiceExporter exporter = new HessianServiceExporter();
exporter.setService(service);
exporter.setServiceInterface(SpitterService.class);
return exporter;
}
与RmiServiceExporter不同的是,我们不需要设置serviceName属性。在RMI中,serviceName属性用来在RMI注册表中注册一个服务。而Hessian没有注册表,因此也就没有必要为Hessian服务进行命名。
配置Hessian控制器
由于Hessian是基于HTTP的,所以HessianServiceExporter实现为一个Spring MVC控制器。这意味着为了使用导出的Hessian服务,我们需要执行两个额外的配置步骤:
- 在web.xml中配置Spring的DispatcherServlet,并把我们的应用部署为Web应用;
- 在Spring的配置文件中配置一个URL处理器,把Hessian服务的URL分发给对应的Hessian服务bean。
首先,我们需要一个DispatcherServlet。这个我们已经在Spittr应用的web.xml文件中配置了。但是为了处理Hessian服务,DispatcherServlet还需要配置一个Servlet映射来拦截后缀为“*.service”的URL:
spitter
*.service
如果在 Java中通过实现WebApplicationInitializer来配置DispatcherServlet的话,那么需要将URL模式作为映射添加到ServletRegistration.Dynamic中,在将DispatcherServlet添加到容器中的时候,我们能够得到ServletRegistration.Dynamic对象:
ServletRegistration.Dynamic dispatcher = container.addServlet("appServlet", new DispatcherServlet(dispatcherServletContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
dispatcher.addMapping("*.service");
或者,如果你通过扩展AbstractDispatcherServletInitializer或AbstractAnnotationConfigDispatcherServletInitializer的方式来配置DispatcherServlet,那么在重载getServletMappings() 的时候,需要包含该映射:
@Override
protected String[] getServletMappings() {
return new String[] {"/" , "*.service"};
}
这样配置后,任何以“.service”结束的URL请求都将由DispatcherServlet处理,它会把请求传递给匹配这个URL的控制器。因此“/spitter.service”的请求最终将被hessianSpitterService bean所处理(它实际上仅仅是一个SpitterServiceImpl的代理)。
我们还需要配置一个URL映射来确保DispatcherServlet把请求转给hessianSpitterService。如下的SimpleUrlHandlerMapping bean可以做到这一点:
@Bean
public HandlerMapping hessianMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
Properties mappings = new Properties();
mappings.setProperty("/spitter.service",
"hessianExportedSpitterService");
mapping.setMappings(mappings);
return mapping;
}
访问Hessian服务
与RMI的客户端的代码类似,客户端调用基于Hessian的Spitter服务可以用如下的配置声明:
@Bean
public HessianProxyFactoryBean spitterService() {
HessianProxyFactoryBean proxy = new HessianProxyFactoryBean();
proxy.setServiceUrl("http://localhost:8080/Spitter/spitter.service");
proxy.setServiceInterface(SpitterService.class);
return proxy;
}
如果想用xml配置,如下:
serviceInterface属性指定了这个服务实现的接口。并且,serviceUrl标识了这个服务的URL。既然Hessian是基于HTTP的,当然在这里要设置一个HTTP URL(URL是由我们先前定义的URL映射所决定的)。下图展示了客户端以及由HessianProxyFactoryBean所生成的代理之间是如何交互的。
Hessian和Burlap
优点:基于HTTP的,解决了防火墙渗透问题。服务端和客户端基本上支持常用语言。传输速度快。
缺点:Hessian和Burlap采用了私有的序列化机制
RMI
优点:RMI使用的是Java本身的序列化机制
缺点:由于不是HTTP协议,会有防火墙渗透问题。而且服务端和客户端都必须用Java语言。传输速度慢。
让我们看以下Spring的HTTP invoker, 它基于HTTP提供了RPC(像Hessian/Burlap一样),同时又使用了Java的对象序列化机制(像RMI一样)。
使用Spring的HttpInvoker
Spring的HttpInvoker 是一个新的远程调用模型,作为Spring框架的一部分,能够执行基于HTTP的远程调用(让防火墙不为难),并使用Java的序列化机制。
将bean导出为HTTP服务
为了把Spitter服务导出为一个基于HTTP invoker的服务,我们需要像下面的配置一样声明一个HttpInvokerServiceExporter bean:
@Bean
public HttpInvokerServiceExporter httpExportedSpitterService (SpitterService service) {
HttpInvokerServiceExporter exporter = new HttpInvokerServiceExporter();
exporter.setService(service);
exporter.setServiceInterface(SpitterService.class);
return exporter;
}
如下图所示,HttpInvokerServiceExporter的工作方式与HessianServiceExporter很相似,它也是一个Spring的MVC控制器,它通过DispatcherServlet接收来自于客户端的请求,并将这些请求转换成对实现服务的POJO的方法调用。
因为HttpInvokerServiceExporter是一个Spring MVC控制器,我们需要建立一个URL处理器,映射HTTP URL到对应的服务上,就像Hessian导出器所做的一样:
@Bean
public HandlerMapping httpInvokerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
Properties mappings = new Properties();
mappings.setProperty("/spitter.service",
"httpExportedSpitterService");
mapping.setMappings(mappings);
return mapping;
}
同样,我们需要确保匹配了DispatcherServlet,这样才能处理对“*.service”扩展的请求。
通过HTTP访问服务
如下图所示,HttpInvokerProxyFactoryBean填充了相同的位置。
为了把基于HTTP invoker的远程服务装配进我们的客户端Spring应用上下文中,我们必须将HttpInvokerProxyFactoryBean配置为一个bean来代理它,如下所示:
@Bean
public HttpInvokerProxyFactoryBean spitterService(){
HttpInvokerProxyFactoryBean proxy = new HttpInvokerProxyFactoryBean();
proxy.setServiceUrl("http://localhost:8080/Spitter/spitter.service");
proxy.setServiceInterface(SpitterService.class);
return proxy;
}
serviceInterface属性用来标识Spitter服务所实现的接口,而serviceUrl属性用来标识远程服务的位置。
要记住HTTP invoker有一个重大的限制:客户端和服务端必须都是Spring应用,并且都要基于java。另外,因为使用Java的序列化机制,客户端与服务端必须使用相同版本的类(与RMI类似)。
发布和使用Web服务
SOA(面向服务架构)的核心理念是,应用程序可以并且应该被设计成依赖于一组公共的核心服务,而不是为每个应用都重新实现相同的功能。
Spring为使用Java API for XML Web Service(JAX-WS)来发布和使用SOAP Web服务提供了大力支持。
创建基于Spring的JAX-WS端点
Spring提供了JAX-WS服务导出器,SimpleJaxWsServiceExporter。但它并不一定是所有场景下的最好选择。SimpleJaxWsServiceExporter要求JAX-WS运行时支持将端点发布到指定地址上。Sun JDK 1.6自带的JAX-WS可以符合要求,但是其他的JAX-WS实现,包括JAX-WS的参考实现,可能并不能满足此需求。
如果我们将要部署的JAX-WS运行时不支持将其发布到指定地址上,那我们就要以更为传统的方式来编写JAX-WS端点。这意味着端点的生命周期由JAX-WS运行时来进行管理,而不是Spring。但这并不意味着它们不能装配Spring上下文的bean。
在Spring中自动装配JAX-WS端点
JAX-WS编程模型使用注解将类和类的方法声明为Web服务的操作。使用@WebService注解所标注的类被认为Web服务的端点,而使用@WebMethod注解所标注的方法被认为是操作。
装配JAX-WD端点的秘密在于继承SpringBeanAutowiringSupport。通过继承SpringBeanAutowiringSupport,我们可以使用@Autowired注解标注端点的属性,依赖就会自动注入了。(此方法未验证)
导出独立的JAX-WS
SpringSimpleJaxWsServiceExporter的工作方式很类似于其他的服务导出器。它把Spring管理的bean发布为JAX-WS运行时中的服务端点。与其他服务导出器不同的是,SpringSimpleJaxWsServiceExporter不需要为它指定一个被导出bean的引用,它会将使用JAX-WS注解所标注的所有bean发布为JAX-WS服务。
SpringSimpleJaxWsServiceExporter可以使用如下的@Bean方法来配置:
@Bean
public SimpleJaxWsServiceExporter jaxWsServiceExporter(){
SimpleJaxWsServiceExporter exporter = new SimpleJaxWsServiceExporter();
exporter.setBaseAddress("http://localhost:8092/services/");
return exporter;
}
当启动的时候,它会搜索Spring应用上下文来查找所有使用@WebService注解的bean。当找到符合的bean时,SimpleJaxWsServiceExporter使用“http://localhost:8092/services/”地址将bean发布为JAX-WS端点的基本地址(也可不设置,默认为“http://localhost:8080”)。SpitterServiceEndpoint就是其中一个被查找到的bean
package spittr.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import spittr.model.Spittle;
import javax.jws.WebMethod;
import javax.jws.WebService;
@Service // 必须标注,否则扫描不到
@WebService(serviceName = "spitterService")
public class SpitterServiceEndpoint { //启动自动配置
@Autowired
SpitterService spitterService; // 自动装配SpitterService
@WebMethod
public void addSpittle(Spittle spittle){
spitterService.addSpittle(spittle); // 委托给spitterService
}
@WebMethod
public String deleteSpittle(long spittleId) {
return spitterService.deleteSpittle(spittleId); // 委托给spitterService
}
}
我们注意到SpitterServiceEndpoint完全就是一个Spring bean,因此它不需要继承任何特殊的支持类就可以实现自动装配。
需要注意的是,它只能用在支持将端点发布到指定地址的JAX-WS运行时中。这包含了sun 1.6 JDK自带的JAX-WS运行时。
在客户端代理JAX-WS服务
使用JaxWsProxyFactoryBean,我们可以在Spring中装配Spitter Web服务,与任意一个其他的bean一样。JaxWsProxyFactoryBean是Spring工厂bean,它能生成一个知道如何与SOAP Web服务交互的代理。所创建的代理实现了服务接口,如下图。
我们可以像下面这样配置JaxWsPortProxyFactoryBean来引用Spitter服务:
@Bean
public JaxWsPortProxyFactoryBean spitterService() throws MalformedURLException {
JaxWsPortProxyFactoryBean proxy = new JaxWsPortProxyFactoryBean();
proxy.setWsdlDocumentUrl(new URL("http://localhost:8092/services/spitterService?wsdl"));
proxy.setServiceName("spitterService");
proxy.setPortName("SpitterServiceEndpointPort");
proxy.setServiceInterface(SpitterService.class);
proxy.setNamespaceUri("http://web.spittr/");
return proxy;
}
wdslDocumentUrl属性标识了远程Web服务定义文件的位置。JaxWsPortProxyFactoryBean将使用这个位置上可用的WSDL来为服务创建代理。由JaxWsPortProxyFactoryBean所生成的代理实现了serviceInterface属性所指定的SpitterService接口。
剩下的三个属性的值通常可以通过查看服务的WSDL来确定。如下所示:
....