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

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

17.1. 简介

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

  • 远程方法调用(RMI。通过使用 RmiProxyFactoryBean RmiServiceExporterSpring同时支持传统的RMI(使用java.rmi.Remote接口和java.rmi.RemoteException)和通过RMI调用器实现的透明远程调用(支持任何Java接口)。
  • SpringHTTP调用器Spring提供了一种特殊的允许通过HTTP进行Java串行化的远程调用策略,支持任意Java接口(就像RMI调用器)。相对应的支持类是 HttpInvokerProxyFactoryBean HttpInvokerServiceExporter
  • Hessian。通过 HessianProxyFactoryBean HessianServiceExporter,可以使用Caucho提供的基于HTTP的轻量级二进制协议来透明地暴露服务。
  • Burlap BurlapCaucho的另外一个子项目,可以作为Hessian基于XML的替代方案。Spring提供了诸如 BurlapProxyFactoryBean BurlapServiceExporter 的支持类。
  • JAX RPCSpring通过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暴露服务

使 用SpringRMI支持,你可以通过RMI基础设施透明的暴露你的服务。设置好SpringRMI支持后,你会看到一个和远程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进行通讯。使用SpringDispatcherServlet,可以很容易的配置这样一个 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>

                              

你可能对SpringDispatcherServlet很熟悉,这样你就知道,需要在 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>

                              

现在,我们准备在客户端连接服务了。不必显示指定处理器的映射,只要使用BeanNameUrlHandlerMappingURL请求映射到服务上:所以,这个服务将在由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,它是一个基于XMLHessian替代方案。它的配置方法和上述Hessian的一样。只要把 Hessian 换成 Burlap 就行了。

17.3.5 对通过HessianBurlap暴露的服务使用HTTP基础认证

HessianBurlap的一个优势是我们可以容易的使用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调用器暴露服务

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

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

17.4.1 暴露服务对象

为服务对象设置HTTP调用器和你在HessianBurlap中使用的方式类似。就象为Hessian支持提供的 HessianServiceExporterSpringHTTP调用器提供了 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 在客户端连接服务

同样,从客户端连接业务与你使用HessianBurlap时所做的很相似。使用代理,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使用J2SEHTTP功能,但是你也可以通过设置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暴露服务

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

/**

 * 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服务代理,LocalJaxRpcServiceFactoryBean JaxRpcPortProxyFactoryBean。 前者只返回一个JAX-RPT服务类供我们使用。后者是一个全功能的版本,可以返回一个实现我们业务服务接口的代理。本例中,我们使用后者来为前面段落中 暴露的AccountService端点创建一个代理。你将看到SpringWeb服务提供了极好的支持,只需要很少的代码 - 大多数都是通过类似下面的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文件的URLSpring需要这些在启动时创建JAX-RPC服务。namespaceUri 对应到.wsdl文件中的targetNamespaceserviceName 对应到.wsdl文件中的service nameportName 对应到.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服务,这样它可以在消息被发送前调用 handleRequestSpring目前对注册处理方法还不提供声明式支持。所以我们必须使用编程方式。但是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库。在写作这个文档时(20053月)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序列化,SpringHTTP调用器是一个很好的选择。它和RMI调用 器使用相同的基础设施,仅仅使用HTTP作为传输方式。注意HTTP调用器不仅只能用在Java-to-Java的远程访问,而且在客户端和服务器端都必 须使用Spring。(Spring为非RMI接口提供的RMI调用器也要求客户端和服务器端都使用Spring

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

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

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


 

参考资料:http://www.redsaga.com/spring_ref/2.0/html/remoting.html


  
  

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值