Spring和Web Service
SOA的核心概念在于应用程序能够而且应该被设计成依赖于一组核心服务,而不是为每个应用重复实现相同的功能。下面让我们重看Ticket-to-Drive citation服务,这一次我们把citation服务bean变成一个web service。
1. 使用xFire导出web service
前面我们使用Spring的exporter创建了远程服务。这些exporter将Spring配置的bean编程远程服务。现在我们来看SoapServiceExporter的用法。实际上,Spring并没有为导出SOAP的web service提供任何exporter。不过,即使Spring没有提供,这也不意味着我们无法创建web service。我们可以使用xFire。
XFire是一个开源的web service平台,它提供了XFireExporter可以将POJO转化成SOAP服务。XFireExporter和其他exporter一样,只是它处理的是SOAP消息。如果你使用Maven2来构建你的应用,你只需要在项目的pom.xml中添加<dependency>标签即可:
<dependency>
<groupId>org.codehaus.xfire</groupId>
<artifactId>xfire-spring</artifactId>
<version>1.2.6</version>
<scope>compile</scope>
</dependency>
这个<dependency>将会加载几个jar文件到应用程序的classpath中。Maven2会自动帮你寻找你需要的jar文件。如果你使用的是Ant,你就需要参考XFire文档来确定你所需的jar文件。
XFire被添加到classpath之后,你还需要完成三件事;
·配置一个XFireExporter bean导出web service
·配置一个DispatcherServlet处理HTTP请求
·配置一个handler映射DispatcherServlet处理过的请求到XFireExporter导出服务
我们已经在Spring中配置过CitationServiceImpl,现在我们使用XFireExporter。首先就是配置XFireExporter bean。
配置XFireExporter
下面的bean定义即展示了配置XFireExporter的方法:
<bean id="citationService.xfire"
class="org.codehaus.xfire.spring.remoting.XFireExporter">
<property name="serviceFactory" ref="xfire.serviceFactory" />
<property name="xfire" ref="xfire" />
<property name="serviceBean" ref="citationService" />
<property name="serviceClass" value="com.tickettodrive.CitationService" />
</bean>
前两个属性serviceFactory和xfire分别引用了xfire.serviceFactory和xfire。XFireExporter需要这些核心的XFire bean——这些bean已经声明过了,包含在XFire jar文件中。我们只需要导入它们即可,如:
<import resource=”classpath:org/codehaus/xfire/spring/xfire.xml”/>
剩下的两个属性随着创建不同的web service变化。serviceBean属性引用了一个要导出成web service的由Spring管理的bean。这里是一个citationService bean的引用。
同时,serviceClass属性指明了定义web service的接口的全路径类名。在服务接口中定义的方法将会被导出成SOAP操作。值得注意的是,服务接口名称将决定服务的命名空间。因为CitationService接口的包名为com.tickettodrive,所以导出的服务的目标命名空间就是http://tickettodrive.com。如果你要覆盖这个规则并要指定一个不同的命名空间,你需要配置XFireExporter的namespace属性:
<bean id="citationService.xfire"
class="org.codehaus.xfire.spring.remoting.XFireExporter">
...
<property id="namespace"
value="http://www.springinactoin.com/citation" />
</bean>
这么配置之后,新的命名空间就是http://www.springinaction.com/citation。
配置DispatcherServlet
XFireExporter被实现为一个Spring的MVC控制器。传递给导出XFireExporter服务的HTTP请求必须经由Spring的DispatcherServlet传递。我们需要在应用程序的web.xml文件中添加<servlet>和<servlet-mapping>项。
<servlet>
<servlet-name>citation</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>citation</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<servlet-mapping>将会发送URL以http://{host}/{app}开头的所有请求,然后DispatcherServlet将发送请求至控制器。
映射请求到XFireExporter
在Spring MVC中,处理器映射(handler mappings)负责映射请求到它们相应的目标控制器上。下面对SimpleUrlHandlerMapping的声明就将映射一个URL模式到XFireExporter上:
<bean id="handlerMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/citationService">citationService.xfire</prop>
</props>
</property>
</bean>
mappings属性使用了一个包含多个prop元素的props元素。每个prop元素的key都是一个URL模式,其值为一个控制器的引用——在这里是XFireExporter。URL模式和请求相关,因此给定了<servlet-mapping>和配置的mappings,导出的web service将会匹配这样的URL样式:https://{host}/{app}/citationService。下面让我们看看如何配置XFire来从注释bean中创建web service。
2. 使用JSR181注释声明web service
使用XFireExporter有一个问题:如果你要到处web service,你需要为每个bean都声明一个XFireExporter。因此当你要到处大量的web ervice时,你将不得不编写大量的XML代码。JSR181是java平台下的web service元数据。它定义了8个注释元素来声明web service。
若要在XFire中使用JSR181,我们需要在应用程序classpath中添加对JAX-WS的支持。在Maven2中,你需要在pom.xml中添加如下标签:
<dependency>
<groupId>org.codehaus.xfire</groupId>
<artifactId>xfire-jaxws</artifactId>
<version>1.2.6</version>
<scope>compile</scope>
</dependency>
并不是所有的web service都需要使全这些注释。对于前面的citation的服务,我们就将使用其中的一部分,例如:
@WebService(serviceName="citationService",
endpointInterface="com.tickettodrive.CitationService")
public class CitationServiceImpl implements CitationService {
...
}
对CitationServiceImpl类进行注释表明我们希望将它作为一个web service导出。serviceName属性指定了服务名称,endpointInterface属性表明CitationService将被用于定义服务接口。
在CitationService接口类中,我们可以为其加标注释如下:
@WebService
public interface CitationService {
@WebMethod(operationName="getCitations")
Citation[] getCitationsForVehicle(String state, String plateNumber);
}
默认情况下,服务接口的所有public方法都将被导出成名称和Java方法名匹配web方法。不过这里,我们指定操作名称为getCitations。注释本身是没有意义的,所以我们需要配置XFire来解析这些注释。
映射请求到JSR181注释bean
我们首先需要配置一个handler mapping,这样DispatcherServlet会分发请求到那些用JSR181注释过的bean上。前面我们配置过SimpleUrlHandlerMapping来映射请求到XFireExporter bean上。SimpleUrlHandlerMapping知道如何映射URL模式到Spring MVC控制器,但是若要映射JSR181注释过的bean,SimpleUrlHandlerMapping就无能为力了。因此,我们可以使用XFire提供的Jsr81HandlerMapping。顾名思义,Jsr81HandlerMapping知道如何映射请求到JSR181注释过的bean,配置方法如下:
<bean id="annotationHandlerMapping"
class="org.codehaus.xfire.spring.remoting.Jsr181HandlerMapping">
<property name="xfire" ref="xfire" />
<property name="webAnnotations" >
<bean class="org.codehaus.xfire.annotations.jsr181.Jsr181WebAnnotations"/>
</property>
</bean>
第一个属性xfire只是XFire的一个引用。它由<import>加载。webAnnotation属性指定了一个Jsr81WebAnnotations的bean引用。Jsr81WebAnnotations会告知Jsr81HandlerMapping使用JSR181注释来映射URL模式。
这样,当我们构建部署应用时,我们就可以发现形如http://{host}/{app}/services/citationService?wsdl的wsdl。现在我们已经了解了如何在Spring中创建web service,但这只完成了一半的工作,剩下的工作就是如何使用它,即如何在Spring中使用web service的客户端。
3. 使用web service
很多web service平台和框架都提供了API用于访问和调用远程服务的方法,但是它们的问题在于客户端必须要知道它在和一个web service通讯。例如:
CitationService cs = (CitationService)Registry.bind(
“http://ws.springinaction.com/Citation/citationService.wsdl”);
如果上述代码使用XFire客户端API,则可以写成:
Service serviceModel = new ObjectServiceFactory().create(
CitationService.class);
CitationService cs = (CitationService) new XFireProxyFactory().create(
serviceModel, “http://ws.springinaction.com/Citation/citationService”);
上面两个例子都展示了如何访问一个web service。但是这两段客户端代码必须知道它使用的SOAP栈。第一个例子中代码使用了Glue的Registry类,第二个例子中则使用了XFire的ObjectServiceFactory和XFireProxyFactory,而且在这两个例子中客户端都需要知道它处理的对象是一个web service。
根据Spring的依赖注入思想,我们不应该通过API来查询web service,而是将它注入到客户端。客户端应该只通过接口来访问服务。事实上,客户端甚至不需要知道服务是一个web service。它可能是一个RMI服务,一个本地POJO或甚至是一个单元测试时用到的mock实现。
若要在Spring中绑定一个远程引用有两种方式:
·JaxRpcProxyFacotryBean:Spring提供的用于访问web service的代理工厂bean;
·XFireClientFactoryBean:XFire提供的web service代理工厂bean
使用JaxRpcPortProxyFactoryBean
我们可以使用JaxRpcPortProxyFactoryBean,来绑定一个citaiton查询webservice,就好像它是一个普通bean一样。JaxRpcPortProxyFactoryBean是一个Spring的FactoryBean,它创建了一个代理,而该代理知道如何与一个SOAP web service进行会话。创建代理就是为了实现服务接口。因此JaxRpcPortProxyFactoryBean就能够绑定并使用一个远程的web service就好像它只是一个本地的POJO一样,下面就是一个例子:
<bean id="citationService"
class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
<property name="wsdlDocumentUrl"
value="http://localhost:8081/Citation/services/citationService?wsdl"/>
<property name="serviceInterface"
value="com.tickettodrive.CitationService" />
<property name="portName" value="citationServiceHttpPort" />
<property name="serviceName" value="citationService" />
<property name="namespaceUri" value"http://tickettodrive.com" />
</bean>
wsdlDocumentUrl属性指定了远程web service定义文件的位置。JaxRpcPortProxyFactoryBean将根据那个URL使用WSDL来构建一个服务代理。该代理将会实现由serviceInterface属性指定的CitationService接口。
剩下的3个属性值通常都需要查看服务的wsdl才能确定。为了说明,我们先给出一段wsdl文件代码:
<wsdl:definitions targetNamespace=http://tickettodrive.com>
<wsdl:service name=”citationService”>
<wsdl:port name=”citationServiceHttpPort”
binding=”tns:citationServiceHttpBinding”>
…
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
服务的WSDL可能会定义多个服务和/或端口。基于此,JaxRpcPortProxyFactoryBean需要我们在portname和serviceName属性中指定port和服务名称。你应该使用<wsdl:port>和<wsdl:service>元素中的name属性来设置。
最后,namespaceUri属性指定了服务的namespace。命名空间会帮助JaxRpcPortProxyFactoryBean在wsdl中定位服务。你需要根据wsdl来设定port和service的name属性值。通常情况下,它都存在于<wsdl:definitions>元素的targetNamespace属性中。
如果服务只用到原始类型(整型,浮点型,字符串等),那么现在我们就已经完成了创建远程服务代理的工作。但是大多数的web service都不只是这么简单,citation查询服务也不例外。Citation查询服务调用getCitationsForVehicle方法返回一个Citation对象的数组。Citation对象本身就是一个复杂类型,数组类型也是一种复杂类型。
JAX-RPC的实现JaxRpcPortProxyFactoryBean也许并不知道如何处理这些类型,所以我们需要告诉它如何处理它,方法如下:
<bean id="citationService"
class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
<property name="wsdlDocumentUrl"
value="http://localhost:9081/Citation/services/citationService?wsdl" />
<property name="serviceInterface" value="com.tickettodrive.CitationService" />
<property name="portName" value="citationServiceHttpPort" />
<property name="serviceName" value="citationService" />
<property name="namespaceUri" value="http://tickettodrive.com" />
<property name="servicePostProcessors">
<list>
<ref bean="beanMappingPostProcessor" />
<ref bean="arrayMappingPostProcessor" />
</list>
</property>
</bean>
servicePostProcessors属性包含了一个列表。该列表包含一个或多个Spring的JaxRpcServicePostProcessor接口的实现。
在citation的例子中,我们已经把两个ServicePostProcessor引入到了JaxRpcPortProxyFactoryBean中。第一个负责序列化和反序列化Citation对象。第二个负责处理Citation对象数组的序列化和反序列化。让我们首先看看beanMappingPostProcessor是如何被声明的。
映射复杂类型
Apache Axis提供了BeanSerializer和BeanDeserializer来处理复杂类型。这些类使用反射机制将bean类型分解成几个简单的部分。如果没有Spring,你需要为每一个web service中用到的复杂类型注册一个BeanSerializer/BeanDeserializer对,不过Spring提供了AxisBeanMappingServicePostProcessor简化了这个过程。下面的代码展示了如何声明AxisBeanMappingServicePostProcessor,它将告诉JaxRpcPortProxyFactoryBean如何序列化和反序列化Citation对象:
<bean id="beanMappingPostProcessor"
class="org.springframework.remoting.jaxrpc.support.AxisBeanMappingServicePostProcessor">
<property>
<list>
<value>com.tickettodrive.Citation</value>
</list>
</property>
<property name="typeNamespaceUri" value="http://tickettodrive.com" />
</bean>
AxisBeanMappingServicePostProcessor实现了JaxRpcPortProxyFactoryBean接口,它可以自动地为beanClasses属性列出的所有类注册一个BeanSerializer/BeanDeserializer对。这里我们配置它处理Citation对象类型。typeNamespaceUri属性用于指定类型的命名空间,就和服务的WSDL中定义的一样。
映射数组
既然AxisBeanMappingServicePostProcessor能够处理复杂java类型,所以我们可以用它很方便地处理数组。只是Spring并没有提供AxisArrayMappingServicePostProcessor,所以我们需要自己编写,只需实现JaxRpcPortProxyFactoryBean接口即可,如:
public class AxisArrayOfCitationMappingServicePostProcessor implements
JaxRpcServicePostProcessor {
public void postProcessJaxRpcService(Service service) {
TypeMappingRegistry registry = service.getTypeMappingRegistry();
TypeMapping mapping = registry.getDefaultTypeMapping();
QName xmlType = new QName(“http://tickettodrive.com”,
“ArrayofCitation”);
Mapping.register(Citation[].class, xmlType,
new ArraySerializerFactory(Citation[].class, xmlType),
new ArrayDeserializerFactory());
}
}
可以看出,JaxRpcPortProxyFactoryBean只实现了postProcessJaxRpcService方法。该方法会获得Service对象的类型映射。QName标识了SOAP中的所有类型,然后该方法创建了一个QName对象来标识ArrayofCitation类型。QName由两部分组成:一个命名空间和一个名字。它们都可以在服务的WSDL中找到。最后,该方法注册了一个序列化工厂类和一个反序列化工厂类。因为处理的是数组,所以适合用ArraySerializerFactory和ArrayDeserializerFactory。我们现在已经有了数组对象映射服务,我们就需要在Spring中声明它:
<bean id="arrayMappingPostProcessor"
class="com.tickettodrive.AxisArrayOfCitationMappingServicePostProcessor" >
</bean>
这里设置的id名称必须和JaxRpcPortProxyFactoryBean中的PostProcessors属性值保持一致。
4. 代理web service给XFire客户
前面我们介绍过使用XFire导出POJO为web service,XFire还可以被用在客户端。XFire提供了XFireClientFactoryBean,它是一个工厂bean,类似于Spring的JaxRpcPortProxyFactoryBean,配制方法如下:
<bean id="citationService"
class="org.codehaus.xfire.spring.remoting.XFireClientFactoryBean">
<property name="wsdlDocumentUrl"
value="http://localhost:8080/Citation/services/citationService?wsdl" />
<property name="serviceInterface"
value="com.tickettodrive.CitationService" />
</bean>
我们需要指定wsdlDcoumentUrl来告诉XFireClientFactoryBean服务端的wsdl的位置。XFireClientFactoryBean将产生一个服务的代理,该服务将实现由serviceInterface指定的接口。
和JaxRpcPortProxyFactoryBean不同的是,XFireClientFactoryBean只需要配置这两个属性即可。大多数情况下,它都不需要配置服务名、端口名或命名空间。它能够通过检查服务的WSDL自己获得这些信息。另外,XFire处理复杂类型时更为方便,我们不需要额外注册类型映射。