Spring远程服务

远程过程调用(RPC)

  • 概念解释:
  1. RMI(RemoteMethodInvocation):远程方法调用,顾名思义,通过远程的方式调用非本地对象的方法并返回结果。使用远程调用通常解决本地计算瓶颈问题,例如分布式记算,最近很火的阿尔法狗人机大战,据说运算使用上千个CPU。
  2. JRMP(java remote method protocol):java远程方法协议,这是完成java到java远程调用的协议,基于TCP协议。
  3. stub与skeleton:这两个概念下面会用到,这里解释下,skeleton是放在服务端的代理,它知道真正的对象在哪。stub是放在客户端的代理,它记录了查找和调用skeleton信息。理解成远程对象引用也成.
  • 容易混淆的概念:

  远程方法调用与远程过程调用的区别:远程方法调用是java独有的,基于JRMP对象流协议实现,支持传输java序列化对象。远程过程调用是基于socket技术实现的,不能传输java对象,socket套接字协议支持多种语言。它们都是基于TCP协议传输。远程方法调用传输的是java序列化对象和基本数据类型,而远程过程调用不支持传输对象。

几种RPC模型:

  • RMI:不考虑网络限制时(例如防火墙),访问/发布基于 java的服务.
  • Hessian/Burlap:考虑网络限制时,通过HTTP访问/发布基于java的服务.
  • HTTP Invoker:考虑网络限制,并希望使用基于XML或专有的Java序列化机制时,访问/发布基于Spring的服务.
  • JAX-RPC和 JAX-WS:访问/发布平台中立的,基于SOAP的web服务.

远程方法调用(RMI)

发布一个RMI服务

  1. 发布一个服务实现类,类中的方法必须抛出 java.rmi.RemoteException.
  2. 创建一个继承于java .rmi.Remote的服务接口.
  3. 运行rmi编译器(rmic).创建客户端
    stub类和服务端skeleton类.
  4. 启动一个rmi注册表,以便驻留这些服务.
  5. 在rmi注册表中注册服务.

    发布一个rmi服务需要如此多的步骤.spring又是如何简化处理的呢?

在spring中发布一个rmi服务

spring为我们做了许多处理,只需要编写基本的是实现服务功能的POJO,剩余的事情就交给spring来处理吧.
RmiServiceExporter可以将任何一个Spring管理的bean发布为rmi服务.RmiServiceExporter将bean封装到一个适配器中,然后适配器类被绑定到注册表中,并且将请求代理给服务器.
例如一个服务SpitterServiceImpl,将其发布为一个rmi服务最简单的方式就是:

1
2
3
4
<bean class="org.springframework.remoting.rmi.RmiServiceExporter"
p:service-ref="spitterService"
p:serviceName="spitterService"
p:serviceInterface="com.aicai.spitter.service.SptterService" />

这里会把spitterService bean装配进service-ref属性中来表明RmiServiceExporter要将该Bean发布为一个Rmi服务.serviceName属性命名了RMI服务,serviceInterface属性指定了此服务所实现的接口.

默认情况下RmiServiceExporter会尝试将一个注册表绑定到本机的1099端口上,如果这个端口没有发现rmi注册表,RmiServiceExporter将重新启动一个注册表.如果希望将注册表绑定到不同的端口或主机,我们可以通过registryPort和registryHost属性来指定.
例如,下面的RmiServiceExporter将尝试把rmi注册表绑定到rmi.spitter.com的1199端口上.

1
2
3
4
5
6
<bean class="org.springframework.remoting.rmi.RmiServiceExporter"
p:service-ref="spitterService"
p:serviceName="spitterService"
p:serviceInterface="com.aicai.spitter.service.SptterService"
p:registryHost="rmi.spitter.com"
p:registryPort="1199"/>

装配rmi服务

1
2
3
<bean id="spitterService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean"
p:serviceUrl="rmi://localhost/spitterService"
P:serviceInterface="com.aicai.spitter.service.SptterService" />

服务的Url是通过RmiProxyFactoryBean的ServiceUrl属性设置的,在这里服务被命名为spitterService,并且驻留在本地机器上.同时,服务提供的接口由serviceInterface属性来指定.

这样就可以在服务端调用了,不用做什么Naming.lookup(serviceUrl)之类的操作,远程调用变得透明。

1
2
3
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
RemoteService service = (RemoteService)context.getBean("spitterService");
System.out.println(service.welcome());

或者直接使用@Autowired注入需要的服务.就像服务在本地一样,不用关心它来自何处.

1
2
@autowired
SpitterService spitterService ;

RMI虽然简单高效,但使用RMI会存在一些问题,比如java序列化的版本问题或者防火墙问题(RMI不是基于HTTP的)。

使用Hessian / Burlap发布远程服务

Hessian 是二进制的,Burlap是基于xml的.它们都是Caucho提供的,
Hessian 因为是二进制可以和任何非java语言通信,更具带宽优势.Burlap基于xml,可读性更强.
除此之外,2者并没有什么不同.

导出一个Hessian服务

1
2
3
<bean id="hessianSpitterService" class="org.springframework.remoting.caucho.HessianServiceExporter"
p:service-ref="spitterService"
p:serviceInterface="com.aicai.spitter.service.SptterService" />

配置Hessian控制器

HessianServiceExporter 和rmiServiceExporter的另一个主要区别是,因为Hessian是基于HTTP的,所以HessianServiceExporter 实现为一个Spring MVC控制器.这意味着,未了使用导出的hessian服务,需要2个额外的配置:

  • 在web.xml中配置Spring的DispatcherServlet,并把我们的应用部署为web应用.
  • 在Spring的配置文件中,配置一个URL处理器.将Hessian服务的url分发给对应的hessian 服务Bean.

之前已经配置了DispatcherServlet,需要配置下面的servlet-mapping.来拦截后缀为.service的url.

1
2
3
4
<servlet-mapping>
<servlet-name>spitter</servlet-name>
<url-pattern>*.service</url-pattern>
</servlet-mapping>

这样配置了之后,任何以.service结尾的url请求都会由DispatcherServlet处理,它会将请求传递给匹配这个url的控制器,所以spitter.service的请求最终由hessianSpitterService Bean处理(它实际上仅仅是SpitterServiceImpl的代理).
那我们是如何知道这个请求会转给hessianSpitterService 代理呢?
所以我们还需要配置一个url映射来确保DispatcherServlet把请求转给
hessianSpitterService .
如下的SimpleUrlHandlerMapping可以做到这一点.

1
2
3
4
5
6
7
8
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"
<property name="mappings">
<value>
/spitter.service=hessianSpitterService
</value>
</property>
/>

访问Hessian/Burlap服务

客户端基于Hessian的Spitter服务可以使用如下配置声明:

1
2
3
4
5
<bean id="spitterService"
class="org.springframework.remoting.caucho.HessianProxyFactoryBean"
p:serviceUrl="http://lcoalhost:8080/Spitter/spitter.service"
p:serviceInterface="com.aicai.spitter.service.SptterService"
/>

而Burlap和Hessian的唯一区别就是HessianProxyFactoryBean换成BurlapProxyFactoryBean.

因为Burlap和Hessian都是基于http的,它们都解决了RMI所头疼的防火墙渗透的问题.

但是当传递过来的RPC消息中包含序列化对象时,RMI就完胜Burlap和Hessian了.因为Burlap和Hessian都使用了私有的序列化机制,而rmi使用的是java本身的序列化机制.如果数据模型非常复杂,Burlap和Hessian的序列化模型可能就无法胜任了.

使用Spring的HttpInvoker

Spring的HttpInvoker基于http的调用,让防火墙可以接收,并使用java的序列化机制.
使用基于HttpInvoker的服务和使用Hessian/Burlap的服务非常相似.

把Bean发布为Http服务

为了将Bean发布为一个Http invoker服务,我们需要像下面的配置一样声明一个HttpInvokerServiceExporter Bean.

1
2
3
4
<bean class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter"
p:service-ref="spitterService"
p:serviceInterface="com.aicai.spitter.service.SptterService"
/>

HttpInvokerServiceExporter 跟Hessian和Burlap一样,也是一个Spring MVC控制器,它通过DispatcherServlet接受来自客户端的请求,并将这些请求转换成对实现服务的POJO的方法的调用.

同样我们需要建立一个url处理器,映射http url到对应的服务上:

1
2
3
4
5
6
7
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"
<property name="mappings">
<value> /spitter.service=httpInvokerSpitterService
</value>
</property>
/>

同样,像之前一样,我们需要确认web.xml中声明了DispatcherServlet,并配置了如下的<servlet-mapping>:

1
2
3
4
<servlet-mapping>
<servlet-name>spitter</servlet-name>
<url-pattern>*.service</url-pattern>
</servlet-mapping>

通过这样方式的配置,Spitter服务就可以通过’/spitter.service’正常访问.

通过http访问服务

同样,我们使用下面的配置,将远程服务装配进客户端的spring应用上下文中.

1
2
3
4
5
<bean id="spitterService"
class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean"
p:serviceUrl="http://lcoalhost:8080/Spitter/spitter.service"
p:serviceInterface="com.aicai.spitter.service.SptterService"
/>

要记住Http invoker有一个重要限制:它只是一个spring框架提供的远程调用解决方案.这意味着客户端和服务端都必须是spring应用.并且,至少目前而言,也隐含表明客户端和服务端都必须是基于Java的.此外,因为使用了java的序列化机制,客户端和服务器端必须使用相同版本的类(与RMI类似).

发布和使用WEB服务

SOA:面向服务的架构,SOA的核心理念是,应用程序应该可以并且应该被设计成依赖于一组公共的核心服务,而不是没每个应用都重新实现相同的功能.

JAX-WS :(Java API for XML Web Services)规范是一组XML web services的JAVA API,JAX-WS允许开发者可以选择RPC-oriented或者message-oriented 来实现自己的web services。在 JAX-WS中,一个远程调用可以转换为一个基于XML的协议例如SOAP,在使用JAX-WS过程中,开发者不需要编写任何生成和处理SOAP消息的代码。JAX-WS的运行时实现会将这些API的调用转换成为对应的SOAP消息。
在服务器端,用户只需要通过Java语言定义远程调用所需要实现的接口SEI(service endpoint interface),并提供相关的实现,通过调用JAX-WS的服务发布接口就可以将其发布为WebService接口。
在客户端,用户可以通过JAX-WS的API创建一个代理(用本地对象来替代远程的服务)来实现对于远程服务器端的调用。
当然 JAX-WS 也提供了一组针对底层消息进行操作的API调用,你可以通过Dispatch 直接使用SOAP消息或XML消息发送请求或者使用Provider处理SOAP或XML消息。
通过web service所提供的互操作环境,我们可以用JAX-WS轻松实现JAVA平台与其他编程环境(.net等)的互操作。
JAX-WS与JAX-RPC之间的关系
Sun最开始的web services的实现是JAX-RPC 1.1 (JSR 101)。这个实现是基于Java的RPC,并不完全支持schema规范,同时没有对Binding和Parsing定义标准的实现。
JAX-WS2.0 (JSR 224)是Sun新的web services协议栈,是一个完全基于标准的实现。在binding层,使用的是the Java Architecture for XML Binding (JAXB, JSR 222),在parsing层,使用的是the Streaming API for XML (StAX, JSR 173),同时它还完全支持schema规范.

创建JAX-WS端点

在Spring中自动装配JAX-WS端点

JAX-WS编程模型使用注解将类和类的方法声明为Web服务的操作,使用@WebService注解标注的类被认为Web服务的端点,而使用@WebMethod注解所标注的方法被认为是操作.
装配JAX-WS端点的秘密在于继承SpringBeanAutowiringSupport通过继承SpringBeanAutowiringSupport,我们可以使用@Autowired注解标注端点的属性,依赖就会自动注入了.SpitterServiceEndPoint展示了它是如何工作的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import javax.jws.WebMethod;
import javax.jws.WebService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
/**
* 在Spring中自动装配JAX-WS端点
* @author jing.ming
*
*/
@WebService(serviceName="SpitterService")
public class SpitterServiceEndPoint extends SpringBeanAutowiringSupport{
@Autowired
private SpitterService spitterService ;
@WebMethod
public void addSpittle(long spitterId){
spitterService.deleteSpittle(spitterId) ;
}
}

导出独立的JAX-WS端点

当对象的生命周期不是由Spring管理的,而对象的属性又需要注入Spring管理的的bean时,SpringBeanAutowiringSupport很有用.但是在合适的场景下,它还可以把Spring管理的Bean导出为JAX-WS端点.

不像其它的服务导出器,SimpleJaxWsServiceExporter不需要为它指定一个被导出Bean的引用,而是将使用JAX-WS注解所标注的所有Bean导出为JAX-WS服务.

SimpleJaxWsServiceExporter可以使用如下的Bean声明来配置:

1
<bean class="org.springframework.remoting.jaxws.SimpleJaxWsServiceExporter"/>

正如我们所看到的,SimpleJaxWsServiceExporter不需要再做其他的事情就可以完成所有的工作,当开始工作时,他会查找Spring应用上下文来查找所有使用@WebService注解所标注的Bean.当找到符合的Bean时,SimpleJaxWsServiceExporter使用http://lcoalhost:8080/的地址将Bean发布为JAX-WS端点.

SpitterServiceEndPoint 就是其中一个被查找的Bean.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import javax.jws.WebMethod;
import javax.jws.WebService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 在Spring中自动装配JAX-WS端点
* @author jing.ming
*
*/
@WebService(serviceName="SpitterService")
public class SpitterServiceEndPoint{
@Autowired
private SpitterService spitterService ;
@WebMethod
public void addSpittle(long spitterId){
spitterService.deleteSpittle(spitterId) ;
}
}

我们注意到SpitterServiceEndPoint的新实现不需要继承SpringBeanAutowiringSupport,因为他是一个合格的Spring bean.他不需要继承任何的支持类就可以实现自动装配.
因为SimpleJaxWsServiceEndPoint的基址默认为http://localhost:8080/,而SpitterServiceEndPoint使用了@WebService(serviceName="SpitterService")
注解所标注,所以这两个Bean所形成的Web服务地址为http://localhost:8080/SpitterService.但是我们可以完全控制服务的URL,可以调整基址,例如,下面的配置,把相同的服务端点发布到http://localhost:8888/services/SpitterService

1
2
<bean class="org.springframework.remoting.jaxws.SimpleJaxWsServiceExporter"
p:baseAddress="http://localhost:8888/services/"/>

在客户端代理JAX-WS服务

在客户端可以像下面这样配置来引用Spitter服务:

1
2
3
4
5
6
<bean id="spitterService" class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean"
p:wsdlDocumentUrl="http://localhost:8080/service/SpitterService?wsdl"
p:serviceName="spitterService"
p:portName="spitterServiceHttpPort"
p:serviceInterface="com.aicai.spitter.service.SptterService"
P:namespaceUri="http://spitter.com" />

wsdlDocumentUrl属性标识了远程Web服务定义文件的位置,JaxWsPortProxyFactoryBean将使用这个位置上的有效的WSDL来为服务创建代理.由JaxWsPortProxyFactoryBean所生成的代理实现了serviceInterface属性所指定的SpitterService端口.

热评文章