Chapter 17. 使用Spring进行远程访问与Web服务

17.1. 简介

Spring为各种远程访问技术的集成提供了工具类。Spring远程支持是由普通(Spring)POJO实现的,这使得开发具有远程访问功能的服务变得相当容易。目前,Spring支持四种远程技术:

  • 远程方法调用(RMI)。通过使用 RmiProxyFactoryBeanRmiServiceExporter,Spring同时支持传统的RMI(使用java.rmi.Remote接口和java.rmi.RemoteException)和通过RMI调用器实现的透明远程调用(支持任何Java接口)。

  • Spring的HTTP调用器。Spring提供了一种特殊的允许通过HTTP进行Java串行化的远程调用策略,支持任意Java接口(就像RMI调用器)。相对应的支持类是 HttpInvokerProxyFactoryBeanHttpInvokerServiceExporter

  • Hessian。通过 HessianProxyFactoryBeanHessianServiceExporter,可以使用Caucho提供的基于HTTP的轻量级二进制协议来透明地暴露服务。

  • Burlap。 Burlap是Caucho的另外一个子项目,可以作为Hessian基于XML的替代方案。Spring提供了诸如 BurlapProxyFactoryBeanBurlapServiceExporter 的支持类。

  • JAX RPC。Spring通过JAX-RPC为远程Web服务提供支持。

  • JMS(待实现)

在讨论Spring对远程访问的支持时,我们将使用下面的域模型和对应的服务:

// Account domain object
public class Account implements Serializable{
  private String name;

  public String getName();
  public void setName(String name) {
    this.name = name;
  }
}
			

// Account service
public interface AccountService {

  public void insertAccount(Account acc);

  public List getAccounts(String name);
}
			

// Remote Account service
public interface RemoteAccountService extends Remote {

  public void insertAccount(Account acc) throws RemoteException;

  public List getAccounts(String name) throws RemoteException;
}
			

// ... and corresponding implement doing nothing at the moment
public class AccountServiceImpl implements AccountService {

  public void insertAccount(Account acc) {
    // do something
  }

  public List getAccounts(String name) {
    // do something
  }
}
			

我们将从使用RMI把服务暴露给远程客户端开始并探讨使用RMI的一些缺点。然后我们将演示一个使用Hessian的例子。

17.2. 使用RMI暴露服务

使用Spring的RMI支持,你可以通过RMI基础设施透明的暴露你的服务。设置好Spring的RMI支持后,你会看到一个和远程EJB接口类似的配置,只是没有对安全上下文传递和远程事务传递的标准支持。当使用RMI调用器时,Spring对这些额外的调用上下文提供了钩子,你可以在此插入安全框架或者定制的安全证书。

17.2.1. 使用 RmiServiceExporter 暴露服务

使用 RmiServiceExporter,我们可以把AccountService对象的接口暴露成RMI对象。可以使用 RmiProxyFactoryBean 或者在传统RMI服务中使用普通RMI来访问该接口。RmiServiceExporter 显式地支持使用RMI调用器暴露任何非RMI的服务。

当然,我们首先需要在Spring BeanFactory中设置我们的服务:

<bean id="accountService" class="example.AccountServiceImpl">
    <!-- any additional properties, maybe a DAO? -->
</bean>
				

然后,我们将使用 RmiServiceExporter 来暴露我们的服务:

<bean class="org.springframework.remoting.rmi.RmiServiceExporter">
	<!-- does not necessarily have to be the same name as the bean to be exported -->
	<property name="serviceName" value="AccountService"/>
	<property name="service" ref="accountService"/>
	<property name="serviceInterface" value="example.AccountService"/>
	<!-- defaults to 1099 -->
	<property name="registryPort" value="1199"/>
</bean>
				

正如你所见,我们覆盖了RMI注册的端口号。通常,你的应用服务也会维护RMI注册,最好不要和它冲突。更进一步来说,服务名是用来绑定下面的服务的。所以本例中,服务绑定在 rmi://HOST:1199/AccountService。在客户端我们将使用这个URL来链接到服务。

注意:我们省略了一个属性,就是 servicePort 属性,它的默认值为0。 这表示在服务通信时使用匿名端口。当然如果你愿意的话,也可以指定一个不同的端口。

17.2.2. 在客户端链接服务

我们的客户端是一个使用AccountService来管理account的简单对象:

public class SimpleObject {
  private AccountService accountService;
  public void setAccountService(AccountService accountService) {
    this.accountService = accountService;
  }
}
				

为了把服务连接到客户端上,我们将创建另一个单独的bean工厂,它包含这个简单对象和服务链接配置位:

<bean class="example.SimpleObject">
	<property name="accountService" ref="accountService"/>
</bean>

<bean id="accountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
	<property name="serviceUrl" value="rmi://HOST:1199/AccountService"/>
	<property name="serviceInterface" value="example.AccountService"/>
</bean>
				

这就是我们在客户端为支持远程account服务所需要做的。Spring将透明的创建一个调用器并且通过RmiServiceExporter使得account服务支持远程服务。在客户端,我们用RmiProxyFactoryBean连接它。

17.3. 使用Hessian或者Burlap通过HTTP远程调用服务

Hessian提供一种基于HTTP的二进制远程协议。它是由Caucho创建的,可以在 http://www.caucho.com 找到更多有关Hessian的信息。

17.3.1. 为Hessian配置DispatcherServlet

Hessian使用一个特定的Servlet通过HTTP进行通讯。使用Spring的DispatcherServlet,可以很容易的配置这样一个Servlet来暴露你的服务。首先我们要在你的应用里创建一个新的Servlet(下面来自web.xml文件):

<servlet>
	<servlet-name>remoting</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
	<servlet-name>remoting</servlet-name>
	<url-pattern>/remoting/*</url-pattern>
</servlet-mapping>
				

你可能对Spring的DispatcherServlet很熟悉,这样你就知道,需要在 WEB-INF 目录里创建一个名为 remoting-servlet.xml(在你的servlet名后)的应用上下文。这个应用上下文将在下一节中里使用。

17.3.2. 使用HessianServiceExporter暴露你的bean

在新创建的 remoting-servlet.xml 应用上下文里,我们将创建一个HessianServiceExporter来暴露你的服务:

<bean id="accountService" class="example.AccountServiceImpl">
  <!-- any additional properties, maybe a DAO? -->
</bean>

<bean name="/AccountService" class="org.springframework.remoting.caucho.HessianServiceExporter">
  <property name="service" ref="accountService"/>
  <property name="serviceInterface" value="example.AccountService"/>
</bean>
				

现在,我们准备在客户端连接服务了。不必显示指定处理器的映射,只要使用BeanNameUrlHandlerMapping把URL请求映射到服务上:所以,这个服务将在由bean名称指明的URL http://HOST:8080/remoting/AccountService 位置进行暴露。

17.3.3. 客户端连接服务

使用 HessianProxyFactoryBean,我们可以在客户端连接服务。同样的方式对RMI示例也适用。我们将创建一个单独的bean工厂或者应用上下文,而后简单地指明下面的bean SimpleObject将使用AccountService来管理accounts:

<bean class="example.SimpleObject">
  <property name="accountService" ref="accountService"/>
</bean>

<bean id="accountService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
	<property name="serviceUrl" value="http://remotehost:8080/AccountService"/>
	<property name="serviceInterface" value="example.AccountService"/>
</bean>
				

这就是所有要做的。

17.3.4. 使用Burlap

我们在这里将不去仔细讨论Burlap,它是一个基于XML的Hessian替代方案。它的配置方法和上述Hessian的一样。只要把 Hessian 换成 Burlap 就行了。

17.3.5. 对通过Hessian或Burlap暴露的服务使用HTTP基础认证

Hessian和Burlap的一个优势是我们可以容易的使用HTTP基础认证,因为他们二者都是基于HTTP的。例如,普通HTTP Server安全机制可以通过使用 web.xml 安全特征来应用。通常,你不会为每个用户都建立不同的安全证书,而是在Hessian/BurlapProxyFactoryBean级别共享安全证书(类似一个JDBC数据源)。

<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
	<property name="interceptors">
		<list>
			<ref bean="authorizationInterceptor"/>
		</list>
	</property>
</bean>

<bean id="authorizationInterceptor"
	class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor">
	<property name="authorizedRoles">
		<list>
			<value>administrator</value>
			<value>operator</value>
		</list>
	</property>
</bean>
				

这个例子里我们显式使用了BeanNameUrlHandlerMapping,并设置了一个拦截器,后者将只允许管理员和操作员调用这个应用上下文中提及的bean。

注意:当然,这个例子没有演示灵活的安全设施。考虑更多有关安全的问题时,请参阅 http://acegisecurity.sourceforge.net 处的Acegi Security System for Spring

17.4. 使用HTTP调用器暴露服务

和使用自身序列化机制的轻量级协议Burlap和Hessian相反,Spring HTTP调用器使用标准Java序列化机制来通过HTTP暴露业务。如果你的参数或返回值是复杂类型,并且不能通过Hessian和Burlap的序列化机制进行序列化,HTTP调用器就很有优势(参阅下一节,选择远程技术时的考虑)。

实际上,Spring可以使用J2SE提供的标准功能或Commons的HttpClient来实现HTTP调用。如果你需要更先进,更容易使用的功能,就使用后者。你可以参考 jakarta.apache.org/commons/httpclient

17.4.1. 暴露服务对象

为服务对象设置HTTP调用器和你在Hessian或Burlap中使用的方式类似。就象为Hessian支持提供的 HessianServiceExporter,Spring的HTTP调用器提供了 org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter。为了暴露 AccountService(上述的),使用下面的配置:

<bean name="/AccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
  <property name="service" ref="accountService"/>
  <property name="serviceInterface" value="example.AccountService"/>
</bean>

17.4.2. 在客户端连接服务

同样,从客户端连接业务与你使用Hessian或Burlap时所做的很相似。使用代理,Spring可以将你调用的HTTP POST请求转换成被暴露服务的URL。

<bean id="httpInvokerProxy" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
  <property name="serviceUrl" value="http://remotehost:8080/AccountService"/>
  <property name="serviceInterface" value="example.AccountService"/>
</bean>

就象上面说的一样,你可以选择使用你想使用的HTTP客户端。缺省情况下,HttpInvokerPropxy使用J2SE的HTTP功能,但是你也可以通过设置httpInvokerRequestExecutor属性选择使用Commons HttpClient:

<property name="httpInvokerRequestExecutor">
	<bean class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"/>
</property>

17.5. Web服务

Spring支持:

  • 使用JAX-RPC暴露服务

  • 访问Web服务

除了上面所说的支持方法,你还可以用XFire xfire.codehaus.org 来暴露你的服务。XFire是一个轻量级的SOAP库,目前在Codehaus开发。

17.5.1. 使用JAXI-RPC暴露服务

Spring对JAX-RPC Servlet的端点实现有个方便的基类 - ServletEndpointSupport。为暴露我们的Account服务,我们继承了Spring的ServletEndpointSupport类来实现业务逻辑,这里通常把调用委托给业务层。

/**
 * JAX-RPC compliant RemoteAccountService implementation that simply delegates
 * to the AccountService implementation in the root web application context.
 *
 * This wrapper class is necessary because JAX-RPC requires working with
 * RMI interfaces. If an existing service needs to be exported, a wrapper that
 * extends ServletEndpointSupport for simple application context access is
 * the simplest JAX-RPC compliant way.
 *
 * This is the class registered with the server-side JAX-RPC implementation.
 * In the case of Axis, this happens in "server-config.wsdd" respectively via
 * deployment calls. The Web Service tool manages the life-cycle of instances
 * of this class: A Spring application context can just be accessed here.
 */
public class AccountServiceEndpoint extends ServletEndpointSupport implements RemoteAccountService {

    private AccountService biz;

    protected void onInit() {
        this.biz = (AccountService) getWebApplicationContext().getBean("accountService");
    }

    public void insertAccount(Account acc) throws RemoteException {
        biz.insertAccount(acc);
    }

    public Account[] getAccounts(String name) throws RemoteException {
        return biz.getAccounts(name);
    }

}

AccountServletEndpoint需要在Spring中同一个上下文的web应用里运行,以获得对Spring的访问能力。如果使用Axis,把Axis的定义复制到你的web.xml中,并且在"server-config.wsdd"中设置端点(或使用发布工具)。参看JPetStore这个例子中OrderService是如何用Axis发布成一个Web服务的。

17.5.2. 访问Web服务

Spring有两个工厂bean用来创建Web服务代理,LocalJaxRpcServiceFactoryBeanJaxRpcPortProxyFactoryBean。前者只返回一个JAX-RPT服务类供我们使用。后者是一个全功能的版本,可以返回一个实现我们业务服务接口的代理。本例中,我们使用后者来为前面段落中暴露的AccountService端点创建一个代理。你将看到Spring对Web服务提供了极好的支持,只需要很少的代码 - 大多数都是通过类似下面的Spring配置文件:

    <bean id="accountWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
        <property name="serviceInterface" value="example.RemoteAccountService"/>
        <property name="wsdlDocumentUrl" value="http://localhost:8080/account/services/accountService?WSDL"/>
        <property name="namespaceUri" value="http://localhost:8080/account/services/accountService"/>
        <property name="serviceName" value="AccountService"/>
        <property name="portName" value="AccountPort"/>
    </bean>

serviceInterface 是客户端将要使用的远程业务接口。wsdlDocumentUrl 是WSDL文件的URL。Spring需要这些在启动时创建JAX-RPC服务。namespaceUri 对应到.wsdl文件中的targetNamespace。serviceName 对应到.wsdl文件中的service name。portName 对应到.wsdl文件中的端口号。

现在bean工厂将把Web服务暴露为 RemoteAccountService 接口,访问服务变得很容易。我们可以在Spring中这样组装起来:

    <bean id="client" class="example.AccountClientImpl">
        ...
        <property name="service" ref="accountWebService"/>
    </bean>

在客户端我们可以使用类似于普通类的方式来访问Web服务,区别是它抛出RemoteException异常。

public class AccountClientImpl {

    private RemoteAccountService service;

    public void setService(RemoteAccountService service) {
        this.service = service;
    }

    public void foo() {
       try {
           service.insertAccount(...);
        } catch (RemoteException ex) {
           // ouch
           ...
        }
     }

}

由于Spring提供了自动转换成非受控异常的能力,我们可以不用考虑受控的RemoteException异常。这要求我们也提供一个非RMI接口,配置文件现在如下:

    <bean id="accountWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
        <property name="serviceInterface">
            <value>example.AccountService</value>
        </property>
        <property name="portInterface">
            <value>example.RemoteAccountService</value>
        </property>
        ...
    </bean>

这里 serviceInterface 已经改成我们目前的非RMI接口。我们的RMI接口现在使用属性 portInterface 进行定义。现在客户端代码可以不用处理 java.rmi.RemoteException 异常:

public class AccountClientImpl {

    private AccountService service;

    public void setService(AccountService service) {
        this.service = service;
    }

    public void foo() {
        service.insertAccount(...);
     }

}

17.5.3. 注册bean映射

为了传递类似Account等复杂对象,我们必须在客户端注册bean映射。

[Note]Note

在服务器端通常在server-config.wsdd中使用Axis进行bean映射注册。

我们将使用Axis在客户端注册bean映射。为此,我们需要继承一个Spring Bean工厂并通过编程注册这个bean映射。

public class AxisPortProxyFactoryBean extends JaxRpcPortProxyFactoryBean {

	protected void postProcessJaxRpcService(Service service) {
		TypeMappingRegistry registry = service.getTypeMappingRegistry();
		TypeMapping mapping = registry.createTypeMapping();
		registerBeanMapping(mapping, Account.class, "Account");
		registry.register("http://schemas.xmlsoap.org/soap/encoding/", mapping);
	}

	protected void registerBeanMapping(TypeMapping mapping, Class type, String name) {
		QName qName = new QName("http://localhost:8080/account/services/accountService", name);
		mapping.register(type, qName,
		    new BeanSerializerFactory(type, qName),
		    new BeanDeserializerFactory(type, qName));
	}

}

17.5.4. 注册自己的处理方法

本节中,我们将注册自己的 javax.rpc.xml.handler.Handler 到Web服务代理,这样我们可以在SOAP消息被发送前执行定制的代码。javax.rpc.xml.handler.Handler 是一个回调接口。jarxpr.jar中有个方便的基类 - javax.rpc.xml.handler.GenericHandler 供我们继承使用:

public class AccountHandler extends GenericHandler {

    public QName[] getHeaders() {
        return null;
    }

    public boolean handleRequest(MessageContext context) {
        SOAPMessageContext smc = (SOAPMessageContext) context;
        SOAPMessage msg = smc.getMessage();

        try {
            SOAPEnvelope envelope = msg.getSOAPPart().getEnvelope();
            SOAPHeader header = envelope.getHeader();
            ...

        } catch (SOAPException e) {
            throw new JAXRPCException(e);
        }

        return true;
    }

}

我们现在要做的就是把AccountHandler注册到JAX-RPC服务,这样它可以在消息被发送前调用 handleRequest。Spring目前对注册处理方法还不提供声明式支持。所以我们必须使用编程方式。但是Spring中这很容易实现,我们只需继承相关的bean工厂类并覆盖专门为此设计的 postProcessJaxRpcService 方法:

public class AccountHandlerJaxRpcPortProxyFactoryBean extends JaxRpcPortProxyFactoryBean {

    protected void postProcessJaxRpcService(Service service) {
        QName port = new QName(this.getNamespaceUri(), this.getPortName());
        List list = service.getHandlerRegistry().getHandlerChain(port);
        list.add(new HandlerInfo(AccountHandler.class, null, null));

        logger.info("Registered JAX-RPC Handler [" + AccountHandler.class.getName() + "] on port " + port);
    }

}

最后,我们要记得更改Spring配置文件来使用我们的工厂bean。

    <bean id="accountWebService" class="example.AccountHandlerJaxRpcPortProxyFactoryBean">
        ...
    </bean>

17.5.5. 使用XFire来暴露Web服务

XFire是一个Codehaus提供的轻量级SOAP库。在写作这个文档时(2005年3月)XFire还处于开发阶段。虽然Spring提供了稳定的支持,但是在未来应该会加入更多特性。暴露XFire是通过XFire自身带的context,这个context将和RemoteExporter风格的bean相结合,后者需要被加入到在你的WebApplicationContext中。

在所有这些允许你暴露服务的方法中,你都必须使用一个相关的WebApplicationContext来创建一个DispatcherServlet,这个WebApplicationContext包含将暴露的服务:

<servlet>
  <servlet-name>xfire</servlet-name>
  <servlet-class>
    org.springframework.web.servlet.DispatcherServlet
  </servlet-class>
</servlet>
                

你还必须链接XFire配置。这是通过增加一个context文件到ContextLoaderListener(或者是Servlet)指定的 contextConfigLocations 参数中。这个配置文件在XFire jar中,当然这个jar文件应该放在你应用的classpath中。

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>
    classpath:org/codehaus/xfire/spring/xfire.xml
  </param-value>
</context-param>

<listener>
  <listener-class>
    org.springframework.web.context.ContextLoaderListener
  </listener-class>
</listener>
                

在你加入一个Servlet映射后(映射 /* 到上面定义的XFire Servlet),你只需要增加一个额外的bean来暴露使用XFire的服务。例如,在 xfire-servlet.xml 中如下:

<beans>
  <bean name="/Echo" class="org.codehaus.xfire.spring.XFireExporter">
    <property name="service" ref="echo">
    <property name="serviceInterface" value="org.codehaus.xfire.spring.Echo"/>
    <property name="serviceBuilder" ref="xfire.serviceBuilder"/>
    <!-- the XFire bean is wired up in the xfire.xml file you've linked in earlier -->
    <property name="xfire" ref="xfire"/>
  </bean>

  <bean id="echo" class="org.codehaus.xfire.spring.EchoImpl"/>
</beans>

XFire处理了其他的事情。它检查你的服务接口并产生一个WSDL文件。这里的部分文档来自XFire网站,要了解更多有关XFire Spring的集成请访问 docs.codehaus.org/display/XFIRE/Spring

17.6. 对远程接口不提供自动探测

对远程接口不实现自动探测的主要原因是防止带来太多的远程调用。目标对象有可能实现的是类似InitializingBean或者DisposableBean的内部回调接口,而这些是不希望暴露给调用者的。

提供一个所有接口都被目标实现的代理通常和本地情况无关。但是当暴露一个远程服务时,你应该只暴露特定的用于远程使用的服务接口。除了内部回调接口,目标有可能实现了多个因为接口,而往往只有一个是用于远程使用的。出于这些原因,我们 要求 指定这样的服务接口。

这是在配置方便性和意外暴露内部方法具有的危险之间作的平衡。总是指明服务接口并不要花太大代价,并可以使你对于暴露指定方法更加安全。

17.7. 在选择这些技术时的一些考虑

这里提到的每种技术都有它的缺点。你在选择一种技术时,应该仔细考虑你的需要,你所暴露的服务和你在远程访问时传送的对象。

当使用RMI时,通过HTTP协议访问对象是不可能的,除非你用HTTP包裹RMI流。RMI是一种重量级的协议,因为它支持整个对象的序列化,当要求网络上传输复杂数据结构时这样的序列化是非常重要的。然而,RMI-JRMP只能绑定到Java客户端:它是一种Java-to-Java的远程访问解决方案。

如果你需要基于HTTP的远程访问而且还要求使用Java序列化,Spring的HTTP调用器是一个很好的选择。它和RMI调用器使用相同的基础设施,仅仅使用HTTP作为传输方式。注意HTTP调用器不仅只能用在Java-to-Java的远程访问,而且在客户端和服务器端都必须使用Spring。(Spring为非RMI接口提供的RMI调用器也要求客户端和服务器端都使用Spring)

当在异构环境中,Hessian和Burlap将可能极有价值。因为它们可以使用在非Java的客户端。然而,对非Java支持仍然是有限的。已知的问题包括含有延迟初始化的collection对象的Hibernate对象的序列化。如果你有一个这样的数据结构,应当考虑使用RMI或HTTP调用器,而不是Hessian。

在使用服务集群和需要JMS代理(JMS broker)来处理负载均衡,发现和自动-失败恢复服务时JMS是很有用的。缺省情况下,在使用JMS远程服务时使用Java序列化,但是JMS提供者也可以使用不同的机制例如XStream来让服务器用其他技术。

最后的一点是,相对于RMI,EJB有一个优点是它支持标准的基于角色的认证和授权,以及远程事务传递。用RMI调用器或HTTP调用器来支持安全上下文的传递是可能的,虽然这不由核心Spring提供:Spring提供了合适的钩子来插入第三方或定制的解决方案。