《Spring3实战》摘要(10)使用远程服务

看完发现这一章只是为了引出下一章的REST,难怪觉得这里写个接口服务这么麻烦

第十章 使用远程服务

作为一个 Java 开发者,我们有几种可以选择的远程调用技术,包括:

  • 远程方法调用(RMI);
  • Caucho 的 Hessian 和 Burlap;
  • Spring 自己基于 HTTP 的远程服务;
  • 使用 JAX-RPC 和 JAX-WS 的 Web 服务。

不管我们选择哪种远程调用技术,Spring 为使用这几种不同的技术访问和创建远程服务提供了广泛的支持。

10.1 Spring 远程调用概览

远程调用时客户端应用和服务端之间的会话。在客户端,它所需要的一些功能并不在该应用的实现范围之内,所以应用向能提供这些功能的其他系统寻求帮助。而远程应用通过远程服务发布这些女工功能。

远程过程调用(RPC),从表面上看,RPC类似于调用本地对象的某个方法。这两者都是同步操作,会阻塞调用代码的执行,知道被调用的过程执行完毕。

Spring 通过几种远程调用技术支持 RPC

RPC 模型适用场景
远程方法调用(RMI)不考虑网络限制时(例如防火墙),访问基于 Java 的服务
Hessian 或 Burlap考虑网络限制时,通过 HTTP 访问、发布基于 Java 的服务
HTTP invoker考虑网络限制,并希望使用基于 XML 或专有的 Java 序列化机制时,访问/发布基于 Spring 的服务
JAX-RPC 和 JAX-WS访问/发布平台中立的、基于 SOAP 的 Web 服务

无论选择哪一种 RPC 模型,我们都会发现 Spring 对每一种模型都提供了风格一致的支持。

在所有的模型中,服务都作为 Spring 所管理的 Bean 配置到我们的应用中。这是采用一个代理工厂 Bean 实现的。这个 Bean 能够像本地对象一样将远程服务装配到其他 Bean 的属性中去。

这里写图片描述

客户端向代理发起调用,就像代理提供了这些服务。代理代表客户端与远程服务进行通信,并由它负责处理连接细节及向远程服务发起远程调用。

更重要的是,如果调用远程服务时发生 java.rmi.RemoteException 异常,代理会处理此异常并重新抛出非检查型异常 RemoteAccessException。

在服务端,我们可以使用上表所列出的任意一种 RPC 模型将任意一个 Spring Bean 的功能发布为一个远程服务。

这里写图片描述

10.2 使用 RMI

远程方法调用(RMI),是Java最初的远程调用技术,RMI 首先在 JDK 1.1 被引入到 Java 平台中,它提供给 Java 开发者一种强大的方法来实现 Java 程序间的交互。

但是开发和访问 RMI 服务是非常沉闷的,涉及到好几个步骤,包括程序的和手工的。Spring 提供了一个代理工厂 Bean,能让我们把 RMI 服务像本地 JavaBean 那样装配到我们的 Spring 应用中,极大简化了 RMI 模型。Spring 还提供了一个远程导出器,用于简化将应用管理的 Bean 转换为 RMI 服务的工作。

10.2.1 发布一个 RMI 服务

如果你曾经创建过 RMI 服务,就会知道这涉及了如下几个步骤。

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

10.2.1.1 在 Spring 中配置 RMI 服务

Spring 提供了比较简单地发布 RMI 服务的方式,只需要编写实现服务功能的 POJO,Spring 就会为我们处理剩余的其他事项。

// POJO 类的示例代码
public interface SpitterService{
    List<Spittle> getRecentSpittles(int count);
    void saveSpittle(Spittle spittle);
    void saveSpitter(Spitter spitter);
    Spitter getSpitter(long id);
    void startFollowing(Spitter follower, Spitter followee);
    // ...

}

如果使用传统的 RMI 来发布此服务,在 SpitterServiceImpl 类中所实现 SpitterService 接口的所有方法都需要抛出 java.rmi.RemoteException。但是如果使用 Spring 的 RmiServiceExporter 将该类转变为 RMI 服务,那现有的实现不需要做任何变化。

RmiServiceExporter 将 Bean 包装在一个适配器类中,然后适配器类被绑定到 RMI 注册表中,并且将请求代理给服务类。

使用 RmiServiceExporter 将 SpitterServiceImpl 发布为 RMI 服务的最简单方式是,在 Spring 中使用如下的 XML 进行配置:

<!--  
    p:service-ref 属性表明要将指定的 Bean 发布为一个 RMI 服务
    p:serviceName 属性命名了 RMI 服务
    p:serviceInterface 属性指定了此服务所实现的接口
-->
<bean class="org.springframework.remoting.rmi.RmiServiceExporter"
    p:service-ref="spitterService"
    p:serviceName="SpitterService"
    p:serviceInterface="com.xxx.xxx.SpitterService" />

默认情况下,RmiServiceExporter 会尝试将一个 RMI 注册表绑定到本地机器的 1099 端口。如果在这个端口没有发现 RMI 注册表, RmiServiceExporter 将重新启动一个注册表。如果希望将某个 RMI 注册表绑定到不同的端口或主机,我们可以通过 registryPort 和 registryHost 属性来指定。

<!-- 设置 RmiServiceExporter 把 RMI 注册表绑定到 rmi.spitter.com 的 1199 端口上:-->
<bean class="org.springframework.remoting.rmi.RmiServiceExporter"
    p:service-ref="spitterService"
    p:serviceName="SpitterService"
    p:serviceInterface="com.xxx.xxx.SpitterService"
    p:registryHost="rmi.spitter.com"
    p:registryPort="1199" />

这就是我们使用 Spring 把某个 Bean 转变为 RMI 服务所需要做的全部工作。

10.2.2 装配 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){ ... }

Spring 的 RmiProxyFactoryBean 是一个工厂 Bean,该 Bean 可以为 RMI 服务创建代理。使用 RmiProxyFactoryBean 引用一个 SpitterService 的 RMI 服务是非常简单的,只需要在客户端的 Spring 配置文件中增加如下的 Bean 声明:

<bean id="spitterService" 
    class="org.springframework.remoting.rmi.RmiFactoryBean"
    p:serviceUrl="rmi://localhost/SpitterService"
    p:serviceInterface="com.xxx.xxx.SpitterService" />

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

这里写图片描述

现在已经将 RMI 服务声明为 Spring 管理的 Bean,我们就可以把它装配进另一个 Bean 中,就像对任意非远程 Bean 所做的那样。

RMI 是一种实现与远程服务交互的非常好的方式,但是它存在某些限制。首先 RMI 很难穿越防火墙,这是因为 RMI 使用任意端口来交互—-这是防火墙通常所不允许的。即使 RMI 提供了对 HTTP 的通道的支持(通常防火墙都允许),但是建立这个通道也不是件容易的事情。

另外一件需要考虑的事情是 RMI 是基于 Java 的。这意味着客户端和服务端必须都采用 Java 开发的。因为 RMI 使用了 Java 的序列化机制,所以通过网络传输的对象类型必须保证在调用的两端是相同的版本。

10.3 使用 Hessian 和 Burlap 发布远程服务

Hessian 和 Burlap 是 Caucho Technology 提供的两种基于 HTTP 的轻量级远程服务解决方案。它们都致力于借助于尽可能简单的 API 和通信协议来简化 Web 服务。

Hessian,像 RMI 一样,使用二进制消息进行客户端和服务端的交互。但与其他二进制远程调用技术(例如 RMI)不同的是,它的二进制消息可以移植到其他非 Java 的语言中,包括 PHP、Python、C++ 和 C#。

Burlap 是一种基于 XML 的远程调用技术,这使得它可以自然而然地一直到任何能够解析 XML 的语言上。正是因为它是基于 XML 的,所以相比起 Hessian 的二进制格式而言,Burlap 可读性更强。但是与其他基于 XML 的远程技术(例如 SOAP 或 XML-RPC)不同,Burlap 的消息结构尽可能的简单,不需要额外的外部定义语言(例如 WSDL 或 IDL)。

在很大程度上,Hessian 和 Burlap 是一样的,唯一的区别在于 Hessian 的消息是二进制的,而 Burlap 的消息是 XML。所以 Hessian 在带宽上更具有优势,但是如果我们更注重可读性或者我们的应用需要与没有 Hessian 实现的语言交互,那么 Burlap 的 XML 消息会是更好的选择。

10.3.1 使用 Hessian 和 Burlap 发布 Bean 的功能

编写一个 Hessian 服务是相当容易的。我们只需要编写一个继承 com.caucho.hessian.server.HessianServlet 的类,并确保所有的服务方法是 public 的(在 Hessian 里,所有 public 方法被视为服务方法)。

因为 Hessian 服务很容易实现,Spring 并没有进一步做更多简化 Hessian 模型的工作。但是与 Spring 一起使用时,Hessian 服务可以在各方面利用 Spring 框架的优势,这是纯 Hessian 服务所不具备的。

10.3.1.1 导出一个 Hessian 服务
<!-- 示例代码: -->
<!--  
    p:service-ref 属性表明要将指定的 Bean 发布为一个 Hessian 服务
    p:serviceInterface 属性指定了此服务所实现的接口
    Hessian 没有注册表,因此不需要 serviceName 属性为 Hessian 服务进行命名。
-->
<bean id="hessianSpitterService"
    class="org.springframework.remoting.caucho.HessianServiceExporter"
    p:service-ref="spitterService"
    p:serviceInterface="com.xxx.xxx.SpitterService" />
10.3.1.2 配置 Hessian 控制器

RmiServiceExporter 和 HessianServiceExporter 另外一个主要区别是,因为 Hessian 是基于 HTTP 的,所以 HessianServiceExporter 的实现为一个 Spring MVC 控制器。这意味着为了使用导出的 Hessian 服务,我们需要执行两个配置步骤:

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

第七章介绍了如何配置 Spring 的 DispatcherServlet 和 URL 处理器。

<!-- 示例:在 web.xml 中配置 Spring MVC 的 DispatcherServlet 拦截后缀为 *.service 的 URL -->
<servlet>
    <servlet-name>spitter</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatchServlet
    </servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>spitter</servlet-name>
    <url-pattern>*.service</url-pattern>
</servlet-mapping>

<!-- 示例:在 Spring 配置文件中,配置URL映射 -->
<bean id="urlMapping" 
    class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
        <value>
            /spitter.service=hessianSpitterService
        </value>
    </property>
</bean>
10.3.1.3 导出一个 Burlap 服务
<!-- 示例:导出一个 Burlap 服务 -->
<bean id="burlapSpitterService" 
    class="org.springframework.remoting.caucho.BurlapServiceExporter"
    p:service-ref="spitterService"
    p:serviceInterface="com.xxx.xxx.SpitterService" />

配置 Burlap 服务和配置 Hessian 服务是相同的,也需要配置一个 URL 处理器和一个 DispatcherServlet。

10.3.2 访问 Hessian/Burlap 服务

基于 RMI 服务的客户端代码与基于 Hessian 服务的客户端代码唯一的不同就是,我们使用 Spring 的 HessianProxyFactoryBean 来替换 RmiProxyFactoryBean。

<!-- 示例: -->
<bean id="spitterService"
    class="org.springframework.remoting.caucho.HessianProxyFactoryBean"
    p:serviceUrl="http://localhost:8080/Spitter/spitter.service"
    p:serviceInterface="com.xxx.xxx.SpitterService" />

把 Burlap 服务装配进客户端使用 BurlapProxyFactoryBean 来代替 HessianProxyFactoryBean。

Hessian 和 Burlap 都是基于 HTTP 的,它们都解决了 RMI 所头疼的防火墙渗透问题。但是当传递过来的 RPC 消息中包含序列化对象时,RMI 就完胜 Hessian 和 Burlap 了。因为 Hessian 和 Burlap 都采用了私有的序列化机制,而 RMI 使用的是 Java 本身的序列化机制。如果数据模型非常的复杂,那么 Hessian/Burlap 的序列化模型可能就无法胜任了。

10.4 使用 Spring 的 HttpInvoker

Spring 的 HTTP invoker 是一个新的远程调用模型,作为 Spring框架的一部分,来执行基于 HTTP 的远程调用(让防火墙可以接受),并使用 Java 的序列化机制。

10.4.1 把 Bean 发布为 HTTP 服务

导出 HTTP invoker 服务,我们需要使用 HttpInvokerServiceExporter。

<!-- 示例: -->
<bean
class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter"
p:service-ref="spitterService"
p:serviceInterface="com.habuma.spitter.service.SpitterService" />

HttpInvokerServiceExporter 的工作方式与 Hessian-ServiceExporter 和 BurlapServiceExporter 很相似。HttpInvokerServiceExporter 也是一个 Spring 的 MVC 控制器,它通过 DispatcherServlet 接收来自于客户端的请求,并将这些请求转换成对实现服务的POJO的方法调用。

因为 HttpInvokerServiceExporter 是一个 Spring MVC 控制器,所以我们需要建立一个 URL 处理器,映射 HTTP URL 到对应的服务上,就像 Hessian 和 Burlap 导出器一样:

<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
        <value>
            /spitter.service=httpInvokerSpitterService
        </value>
    </property>
</bean>

同样,我们需要确保在 web.xml 中声明了 DispatcherServlet ,并配置了如下的 <servlet-mapping>

<servlet-mapping>
    <servlet-name>spitter</servlet-name>
    <url-pattern>*.service</url-pattern>
</servlet-mapping>

10.4.2 通过 HTTP 访问服务

为了将基于 HTTP invoker 的远程服务装配进我们的客户端 Spring 应用上下文中,我们必须使用 HttpInvokerProxyFactoryBean 来胚子一个 Bean 来代理它,如下所示:

<bean id="spitterService"
class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean"
p:serviceUrl="http://localhost:8080/Spitter/spitter.service"
p:serviceInterface="com.habuma.spitter.service.SpitterService" />

Spring 的 HTTP invoker 作为一个两全其美的远程调用解决方案而出现的,将 HTTP 的简单性和 Java 内置的对象序列化机制融合在一起。

HTTP invoker 有一个严重限制:客户端和服务端必须都是 Spring 应用。并且,因为使用了 Java 序列化机制,客户端和服务端必须使用相同版本的类(与 RMI 类似)。

10.5 发布和使用 Web 服务

RMI、Hessian、Burlap 和 HTTP invoker 都是远程调用的可选解决方案。但是当面临无所不在的远程调用时,Web 服务是势不可挡的。

近几年,最流行的一个 TLA (Three Letter Acronym,三个字母缩写)就是 SOA (面向服务的架构)。SOA 对不同的人意味着不同的意义。但 SOA 的核心理念是,应用程序可以并且应该被设计成依赖于一组公共的核心服务,而不是为每个应用都重新实现相同的功能。

Spring 为使用 XML Web Service 的 Java API (一般称为 JAX-WS)来发布和使用 SOAP Web 服务提供了支持。

10.5.1 创建 JAX-WS 端点

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

JAX-WS 编程模型使用注解将类和类的方法声明为 Web 服务的操作。使用 @WebService 注解所标注的类被认为 Web 服务的端点,而使用 @WebMethod 注解所标注的方法被认为是操作。

JAX-WS 端点很可能需要与其他对象交互来完成工作。这意味着 JAX-WS 端点可以受益于依赖注入。但是如果端点的生命周期由 JAX-WS Runtime 来管理,而不是 Spring 的话,这似乎不可能把 Spring 管理的 Bean 装配进 JAX-WS管理的端点实例中。

装配 JAX-WS 端点的秘密在于继承 SpringBeanAutowiringSupport 。通过继承 SpringBeanAutowiringSupport(在spring-web.jar包中) ,我们可以使用 @Autowired 注解标注端点的属性,依赖就会自动注入了。SpitterServiceEndpoint 展示了它是如何工作的。

import javax.jws.WebMethod;
import javax.jws.WebService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ejb.interceptor.SpringBeanAutowiringInterceptor;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;

@WebService(serviceName="SpitterService")
public class SpitterServiceEndpoint 
    //启用自动装配,如果该类是Spring Bean,则无需继承SpringBeanAutowiringSupport
    extends SpringBeanAutowiringSupport{
    /**
    尽管在这里使用 SpringBeanAutowiringSupport 为 JAX-WS 企业自动装配,但是在对象的生命周期不是由 Spring 所管理的任何场景中,如果要启用自动装配,这种方式还是很有用的。唯一的要求是 Spring 应用上下文和非 Spring 的 Runtime 要驻留在相同的 Web 应用中。
**/
    //自动装配
    @Autowired
    SpitterService spitterService;

    //委托给 SpitterService
    @WebMethod
    public void addSpittle(Spittle spittle){
        spitterService.saveSpittle(spittle);
    }

    @WebMethod
    public void deleteSpittle(long spittleId){
        spitterService.deleteSpittle(spittleId);
    }
}

我们使用 @Autowired 注解标注 spitterService 来表明它应该自动注入一个从 Spring 应用上下文中所获取的 Bean。在这里,端点委托注入的 SpitterService 来完成实际的工作。

10.5.1.2 导出独立的 JAX-WS 端点

Spring 的 SimpleJaxWsServiceExporter 的工作方式很类似于本章前面所介绍的其他服务导出器。它把 Spring 管理的 Bean 发布为在 JAX-WS Runtime 中服务端点。不像其他服务导出器,SimpleJaxWsServiceExporter 不需要为它指定一个被导出 Bean 的引用,而是将使用 JAX-WS 注解所标注的所有 Bean 导出为 JAX-WS 服务。

SimpleJaxWsServiceExporter 可以使用如下的<bean>声明来配置:

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

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

因为 SimpleJaxWsServiceExporter 的基址(base address)默认为 http://localhost:8080/ 而 SpitterServiceEndpoint 使用了 @WebService(serviceName=”SpitterService”)注解所标注,所以这个 Bean 所形成的 Web 服务地址为 http://localhost:8080/SpitterService

我们希望调整服务URL,可以调整基址。例如:

<!-- 将服务端点发布到 http://localhost:8888/services/SpitterService -->
<bean class="org.springframework.remoting.jaxws.SimpleJaxWsServiceExporter"
    p:baseAddress="http://localhost:8888/services/" />

SimpleJaxWsServiceExporter 只能用于支持将端点发布到指定地址的 JAX-WS Runtime 中。这包含了 Sun 1.6 JDK 自带的 JAX-WS Runtime。其他的 JAX-WS Runtime,例如 JAX-WS 2.1 的参考实现,不支持这种类型的端点发布,因此不能使用 SimpleJaxWsServiceExporter。

10.5.2 在客户端代理 JAX-WS 服务

使用 Spring 发布 Web 服务与我们使用 RMI、Hessian、Burlap 和 HTTP invoker 发布服务是完全不同的。但是我们很快就会发现,使用 Spring 访问 Web 服务所涉及的客户端代理的工作方式与基于 Spring 的客户端使用其他远程调用技术是相同的。

使用 JaxWsPortProxyFactoryBean,我们可以在 Spring 中装配 Spitter Web 服务,就像其他 Bean 一样。JaxWsPortProxyFactoryBean 是一个 Spring 工厂 Bean,它生成一个知道如何与 SOAP Web 服务进行交互的代理。所创建的代理实现了服务接口。因此,JaxWsPortProxyFactoryBean 让装配和使用一个远程 Web 服务变成了可能,就像远程 Web 服务是本地 POJO 一样。

这里写图片描述

我们可以像下面这样配置 JaxWsPortProxyFactoryBean 来引用 Spitter 服务:

<bean id="spitterService"
    class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean"
    p:wsdlDocumentUrl="http://localhost:8080/services/SpitterService?wsdl"
    p:serviceName="spitterService"
    p:portName="spitterServiceHttpPort"
    p:serviceInterface="com.habuma.spitter.service.SpitterService"
    p:namespaceUri="http://spitter.com" />
  • wsdlDocumentUrl属性:标识了远程 Web 服务定义文件的位置。JaxWsPortProxyFactoryBean 将使用这个位置上的有效的 WSDL 来为服务创建代理。
  • serviceInterface 属性:由 JaxWsPortProxyFactoryBean 所生成的代理实现了 serviceInterface 属性所指定的 SpitterService 接口。
    • serviceName、portName、namespaceUri属性:这三个属性通常可以通过查看服务的 WSDL 来确定。

假设如下为 Spitter 服务的 WSDL:

<wsdl:definitions targetNamespace="http://spitter.com">
    ...
        <wsdl:service name="spitterService">
            <wsdl:port name="tns:spitterServiceHttpBinding">
            </wsdl:port>
        </wsdl:service>
    ...
</wsdl:definitions>

虽然不太可能这么做,但是在服务的 WSDL 定义多个服务和端口是有可能的。鉴于此,JaxWsPortProxyFactoryBean 需要使用 portName 和 serviceName 属性指定端口和服务名称。 WSDL 中 <wsdl:port> 和 <wsdl:service>元素的 name 属性可以帮助我们识别出这些属性是为谁设置的。

最后,namespaceUri 属性指定了服务的命名空间。命名空间将有助于 JaxWsPortProxyFactoryBean 去定位 WSDL 中的服务定义。正如端口和服务名一样,我们可以在 WSDL 中找到该属性的正确值。它通常会在
<wsdl:definitions的 targetNamespace 属性中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值