17.5. Web服务

Spring支持:

除了上面所说的支持方法,你还可以用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映射。

注意

在服务器端通常在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