系统集成之Web服务（WebService）?
李小翔
2013-12-11 11:35

内容列表
服务端开发：创建 Endpoint 	 # 
Simple Frontend 	 # 
创建接口类 	 # 
测试 	 # 
列表数据 	 # 
Domain数据 	 # 
JAX-WS Frontend 	 # 
创建接口类 	 # 
Map数据 	 # 
附件 	 # 
客户端开发：groovy-wslite 	 # 
客户端开发：cxf-client 	 # 
补充说明 	 # 

目录服务端开发：创建 EndpointSimple Frontend创建接口类
测试
列表数据
Domain数据
JAX-WS Frontend创建接口类
Map数据
附件
客户端开发：groovy-wslite
客户端开发：cxf-client
补充说明
BroToolkit 中内置了 WS 的几个插件：grails-cxf、grails-cxf-client、groovy-wslite。
其中 cxf 用于创建Web服务、而 cxf-client 和 groovy-wslite 均为WS客户端，后者仅仅是一个152K多点的jar包，但是功能一点也不简单。
默认情况下，两个 cxf 的插件是不启用的（安装了 BroBPM 后会自动启用 grails-cxf），需要在工程的 BuildConfig.groovy 中将它们从 “bropen.removed.plugins” 中删除/注释掉即可。
为了调试方便，可以安装一个 SoapUI （Eclipse插件或者独立的程序，推荐后者，Eclipse插件有点不稳定、而且版本比较老）来进行WS的测试。
下面分别对这几个组件做一个入门的介绍和示例，更多说明还请参考上面的官网链接。

服务端开发：创建 Endpoint
所谓 Endpoint，简单的说就是服务接口，每个Web服务至少包含一个 Endpoint，而客户端通过 Endpoint 来访问服务。每个Endpoint必须包含地址、绑定和协议（契约）。最常见的当然就是以WSDL来定义的、基于HTTP、用 SOAP 封装的服务接口。
使用 CXF 开发一个服务接口，首先要选择一种 Frontend（前端），也就是用来开发和对外暴露Web Services的不同的API，CXF提供了 JAX-WS、Simple、JAX-RS、Javascript等四种前端。最常用的是前两种，其中 JAX-WS 是 JCP 提供的一组WS的规范与API，而 Simple 是一个基于反射、类似 JAX-WS 的简易前端，相对来说，前者更为完整与灵活，而后者不需要写任何注解、依赖也更少（唯一的两个优点），因此，多数情况下都建议选择 JAX-WS。
grails-cxf 插件提供四个创建WS的命令，create-endpoint、create-endpoint-simple、create-cxf-service、create-cxf-service-simple，前面两个会将创建的类放在 grails-app/endpoints 中，后面两个会将创建的类放在 grails-app/services 中。当然，为了将WS Endpoint 和普通的 Service 区别开来，建议使用前面两个命令。

Simple Frontend

创建接口类
下面创建一个基于 Simple 前端的Endpoint：
$ grails create-endpoint-simple bropen.Simple
| Created file grails-app/endpoints/bropen/SimpleEndpoint.groovy
生成的 SimpleEndpoint.groovy 类的内容如下：
package bropen
import org.grails.cxf.utils.EndpointType
class SimpleEndpoint { 
   static expose = EndpointType.SIMPLE
   static excludes = []
    String serviceMethod(String s) { 
       return s
   } 
} 
run-app后，控制台会有如下输出：
[INFO org.apache.cxf.service.factory.ReflectionServiceFactoryBean] - Creating Service { http://bropen/} SimpleEndpoint from class bropen.SimpleEndpoint
打开浏览器，访问下面的URL就可以看到simple前端自动生成的wsdl定义：
http://localhost:8080/app/services/simple?wsdl
该定义中，自动生成了一个Web服务方法 serviceMethod，有一个参数s，参数类型、返回值均为 string。
值得注意的是，targetNamespace 为 “http://bropen/” —— 也就是 Endpoint 类的包名路径（逆序） —— 是不可自定义的。

测试
打开SoupUI，并 Ctrl+N 创建一个工程，将上面的wsdl的地址设置到 “Initial WSDL/WADL”中，点击“OK”后，会自动生成一个 serviceMethod 的 Request 1，双击后，打开一个调试窗口，左侧内容为XML格式的SOAP封包样例，如：
<soapenv:Envelope xmlns:soapenv=”http://schemas.xmlsoap.org/soap/envelope/”
xmlns:brop=”http://bropen/”>
  <soapenv:Header/>
  <soapenv:Body>
     <brop:serviceMethod>
        <!--Optional:-->
        <s>?</s>
     </brop:serviceMethod>
  </soapenv:Body>
</soapenv:Envelope>
将 <s>?</s> 中的问号替换成任何字符串（如foobar），点击左上角的submit图标（绿三角），则右侧窗口中会显示出WS返回结果的SOAP封包，如：
<soap:Envelope xmlns:soap=”http://schemas.xmlsoap.org/soap/envelope/”>
  <soap:Body>
     <ns1:serviceMethodResponse xmlns:ns1=”http://bropen/”>
        <return>foobar</return>
     </ns1:serviceMethodResponse>
  </soap:Body>
</soap:Envelope>
此外，点击两个文本框左侧的大大的“RAW”按钮，能看到 HTTP 提交的原始数据（对使用 groovy-wslite 开发WS客户端时很有帮助）。

列表数据
cxf 插件可以支持 List、Map、Domain 等数据类型的交互，但是在 Simple 前端中，只支持 List 与 Domain 类。
创建一个新的service方法，其参数和返回值的类型都是字符串列表：
List<String> testList(List<String> p) { 
   return p
} 
使用 SoupUI 测试：
<!-- 请求 -->
<brop:testList>
   <!--Zero or more repetitions:-->
   <p>1</p>
   <p>2</p>
   <p>3</p>
</brop:testList>
<!-- 响应 -->
<ns1:testListResponse xmlns:ns1=”http://bropen/”>
   <return>1</return>
   <return>2</return>
   <return>3</return>
</ns1:testListResponse>

Domain数据
创建一个简单的Domain类 Foobar，并且仅有两个属性id（这里必须显式的定义）和name，并且创建两个测试服务方法 testDomain 和 testDomainList：
package bropen
import javax.xml.bind.annotation.XmlAccessorType
import javax.xml.bind.annotation.XmlAccessType
import javax.xml.bind.annotation.XmlElement
/** Domain类，需要加上下面这些注解 */
@XmlAccessorType(XmlAccessType.NONE)
class Foobar { 
   @XmlElement
    Long id
   @XmlElement
    String name;
} 
/** 服务接口 */
class SimpleEndpoint { 
   ....
    Foobar testDomain(String name) { 
        Foobar.withTransaction { 
           def foobar = new Foobar(name: name)
            foobar.save();
           return foobar;
       } 
   } 
    List<Foobar> testDomainList() { 
       return Foobar.list();
   } 
} 
使用 SoupUI 测试：
<!-- 请求：testDomain -->
<brop:testDomain>
   <name>yyyyy</name>
</brop:testDomain>
<!-- 响应：testDomain -->
<return>
   <id>1217041</id>
   <name>yyyyy</name>
</return>
<!-- 请求：testDomainList -->
<brop:testDomainList/>
<!-- 响应：testDomainList -->
<return><id>1217040</id><name>xxxxx</name></return>
<return><id>1217041</id><name>yyyyy</name></return>
此外，cxf 插件还支持 Domain 类的 hasMany 映射，详细请参考官方文档。

JAX-WS Frontend

创建接口类
创建一个 JAX-WS 的 Endpoint：
$ grails create-endpoint bropen.JaxWs
| Created file grails-app/endpoints/bropen/JaxWsEndpoint.groovy
生成的 JaxWsEndpoint.groovy 类的内容如下：
package bropen
import org.grails.cxf.utils.EndpointType
class JaxWsEndpoint { 
   static expose = EndpointType.JAX_WS
   static excludes = []
    String serviceMethod(String s) { 
       return s
   } 
} 
和默认的 Simple 前端接口差不太多，可是通过浏览器访问其 wsdl（..../services/jaxWs?wsdl）时，却并没有注册上服务方法 serviceMethod，原因是按照JAX-WS规范，这些都需要使用注解来驱动。
把这个类修改一下，加上：
自定义定义WS的 targetNamespace：这是 Simple 前端里无法自定义的
添加一个 testMethod 的WS方法：这里相对复杂，需要使用注解详细定义方法的各个方面
定义一个返回结果类
添加一些文档说明：会在 wsdl 中显示出来，便于第三方集成
修改后的代码如下：
package bropen
import javax.jws.WebParam
import javax.jws.WebMethod
import javax.jws.WebService
import javax.jws.WebResult
import org.apache.cxf.annotations.WSDLDocumentation
import org.apache.cxf.annotations.WSDLDocumentationCollection
import javax.xml.bind.annotation.XmlAccessorType
import javax.xml.bind.annotation.XmlAccessType
import javax.xml.bind.annotation.XmlElement
import org.grails.cxf.utils.GrailsCxfEndpoint
import org.grails.cxf.utils.GrailsCxfEndpointProperty
import org.grails.cxf.utils.EndpointType
@WebService(targetNamespace = ’http://test.services.bropen.com.cn/’)
@GrailsCxfEndpoint( expose = EndpointType.JAX_WS )
@WSDLDocumentationCollection([
   @WSDLDocumentation(value = ”WSDL定义说明111”,
        placement = WSDLDocumentation.Placement.TOP),
   @WSDLDocumentation(”Endpoint说明222”)
])
class JaxWsEndpoint { 
   /** 不自动注册为WS方法 */
   @WebMethod(exclude = true)
    String excludedMethod() { 
       //...
    } 
   /** WS方法 testMethod */
   @WebResult(name = ’result’)
   @WebMethod(operationName = ’testMethod’)
   @WSDLDocumentation(”WS方法说明3333”)
    Result testMethod( @WebParam(name = ’s’) String s ) { 
        Result r = new Result();
        r.success = Boolean.FALSE;
        r.message = ”莫须有！”
       return r;
   } 
   
   // 定义几个内部的静态类
    
   /** 操作结果 */
   @XmlAccessorType(XmlAccessType.NONE)
   static class Result { 
       /** 操作是否成功 */
       @XmlElement(nillable=false, required=true)
        Boolean success = Boolean.TRUE;
       
       /** 成功或失败的消息 */
       @XmlElement(nillable=true, required=false)
        String message
   } 
} 
测试略。

Map数据
JAX-WS 前端可以支持 Map 类型的数据交互（List、Domain自然也是不在话下的），示例如下：
@WebResult(name = ’result’)
@WebMethod(operationName = ’testMap’)
@WSDLDocumentation(”Map类型的数据交换”)
Map<String, String> testMap(Map<String, String> p) { 
   return p
} 
观察生成的WSDL，会生成如下的Map类型定义：
<xs:complexType name=”testMap”>
 <xs:sequence>
   <xs:element name=”arg0”>
     <xs:complexType>
       <xs:sequence>
         <xs:element maxOccurs=”unbounded” minOccurs=”0” name=”entry”>
           <xs:complexType>
             <xs:sequence>
               <xs:element minOccurs=”0” name=”key” type=”xs:string”/>
               <xs:element minOccurs=”0” name=”value” type=”xs:string”/>
             </xs:sequence>
           </xs:complexType>
         </xs:element>
       </xs:sequence>
     </xs:complexType>
   </xs:element>
 </xs:sequence>
</xs:complexType>
测试结果如下：
<!-- 请求 -->
<soapenv:Body>
 <test:testMap>
    <arg0>
       <!--Zero or more repetitions:-->
       <entry>
          <key>k1</key>
          <value>v1</value>
       </entry>
       <entry>
          <key>k2</key>
          <value>v2</value>
       </entry>
    </arg0>
 </test:testMap>
</soapenv:Body>
<!-- 响应 -->
<soap:Body>
 <ns2:testMapResponse xmlns:ns2=”http://test.services.bropen.com.cn/”>
    <result>
       <entry>
          <key>k1</key>
          <value>v1</value>
       </entry>
       <entry>
          <key>k2</key>
          <value>v2</value>
       </entry>
    </result>
 </ns2:testMapResponse>
</soap:Body>

附件
通过WS的MTOM（Message Transmission Optimization Mechanism，消息优化传输机制）可以进行附件的上传、下载，示例参考：CXF官方示例、Grails – CXF Web Service with MTOM Attachments。
通过 grails-cxf 插件编写附件上传、下载的WS方法示例：
给 Endpoint 类加一个 @MTOM 的注解
附件类型的参数或返回值的类型设置为 DataHandler
testUpload 和 testDownload 方法中，附件上传后保存到临时目录、下载也是从临时目录中找对应的文件
import javax.xml.ws.soap.MTOM
import javax.xml.bind.annotation.XmlMimeType
import javax.activation.DataHandler
import javax.activation.FileDataSource
....
@MTOM(enabled = true)
class JaxWsEndpoint { 
   @WebResult(name = ’result’)
   @WebMethod(operationName = ’testUpload’)
   @WSDLDocumentation(”上传附件”)
    Result testUpload(
           @WebParam(name = ’fileName’)
            String fileName,
           @XmlMimeType(”*/*”)
           @WebParam(name=”fileDataHandler”)
            DataHandler fileDataHandler ) { 
        Result result = new Result();
        InputStream is = null;
        OutputStream os = null;
       try { 
            is = fileDataHandler.getInputStream();
            os = new FileOutputStream( System.getProperty(”java.io.tmpdir”) + File.separator + fileName );
           byte[] buf = new byte[1024*1024];
           int read;
           while( (read = is.read(buf)) != -1 ) { 
                os.write(buf, 0, read);
                os.flush();
           } 
       }  catch ( Exception e ) { 
            result.success = Boolean.FALSE;
            result.message = e.getMessage();
            log.error(null, e);
       }  finally { 
            is?.close();
            os?.close();
       } 
       return result;
   } 
   @WebResult
   @WebMethod(operationName = ’testDownload’)
   @XmlMimeType(”*/*”)
   @WSDLDocumentation(”下载附件”)
    DataHandler testDownload( @WebParam(name=”fileName”) String fileName )
           throws FileNotFoundException, SecurityException { 
        File file = new File( System.getProperty(”java.io.tmpdir”) + File.separator + fileName );
       if ( !file.exists() ) { 
           throw new FileNotFoundException(”File [” + fileName + ”] not found!”);
       }  else if ( !file.canRead() ) { 
           throw new SecurityException(”Cannot read file [” + fileName + ”]!”);
       } 
       return new DataHandler(
           new FileDataSource(file){ 
               public String getContentType() { 
                   return new javax.activation.MimetypesFileTypeMap().getContentType(fileName);
               } 
           } 
       );
   } 
} 
上述代码可以使用 SoupUI 进行初步测试，需要注意的是：
附件上传（testUpload）里的fileDataHandler参数随便写点文本上传后保存为乱码
附件下载（testDownload）可以通过RAW模式来观察响应中的附件内容。
完整的测试参见下面 cxf-client 客户端开发过程。

客户端开发：groovy-wslite
groovy-wslite 是一个仅仅依赖于 Groovy 的 jar 包，配合 SoupUI，开发WS客户端简单到爆。
以上面创建的 SimpleEndpint 的 serviceMethod 方法为例：
import wslite.soap.SOAPClient
import wslite.soap.SOAPFaultException
import wslite.soap.SOAPResponse
try { 
   def client = new SOAPClient(”http://localhost:8080/app/services/simple”)
    SOAPResponse resp = client.send(”””<soapenv:Envelope
        xmlns:soapenv=”http://schemas.xmlsoap.org/soap/envelope/” xmlns:brop=”http://bropen/”>
      <soapenv:Header/><soapenv:Body><brop:serviceMethod><s>test</s></brop:serviceMethod></soapenv:Body></soapenv:Envelope>”””
    );
    // 解析结果
   def xml = new XmlParser().parseText(resp.text);
    println xml.”soap:Body”.”ns1:serviceMethodResponse”.”return”.text()
}  catch ( SOAPFaultException sfe ) { 
    log.error(sfe.fault.detail.text(), sfe);
}  catch ( Exception e ) { 
    log.error( null, e )
} 
将输出结果：test
除此之外，SOAPClient 还有多个同名的 send 方法、并且可以调整各种协议参数，详细可参考官方文档。不过 SOAPClient 不支持附件的处理。
BTW：使用 SoupUI 测试以获得 SOAP XML 时，最好看看 RAW 里的 HTTP 请求，避免由于少设置了某些 http 头导致无法获得期望的响应，如 SOAPAction。

客户端开发：cxf-client
grails-cxf-client 是基于 CXF 的，相对上面小巧的wslite来说，功能、个头等各个方面自然都不是一个数量级的，但是开发过程还是比较简单的。
此外，针对大家都比较头疼的日期类型转换问题，先在 JaxWsEndpoint 中添加一个日期类型的WS方法：
@WebResult(name = ’result’)
@WebMethod(operationName = ’testDate’)
@WSDLDocumentation(”日期类型测试”)
Date testDate( @WebParam(name = ’date’) Date date ) { 
   return date
} 
客户端的开发与调用步骤如下。
1、在 Config.groovy 中配置：
cxf { 
    client { 
        jaxWsSampleClient { 
            wsdl = ”grails-app/conf/jaxWsSample.wsdl”
           // 定制文件：可以进行类名、包名、类型等转换
            bindingFile = ”plugins/bro-toolkit-3.0.0/grails-app/conf/WebserviceDate.binding.xml”
           // 控制 wsdl2java 生成的包名
            namespace = ”bropen.wsclient”
       } 
   } 
} 
wsdl：配置wsdl文件的位置，可以为本地路径，或者直接写一个HTTP的远程URL
bindingFile：一个binding用的定制文件，其详细语法可以参考官方文档（jaxb、jaxws），这里仅仅是进行日期类型转换。
该文件的完整内容如下：
<?xml version=”1.0” encoding=”UTF-8” standalone=”yes”?>
<jaxws:bindings xmlns:xsd=”http://www.w3.org/2001/XMLSchema”
   xmlns:xs=”http://www.w3.org/2001/XMLSchema” xmlns:wsdl=”http://schemas.xmlsoap.org/wsdl/”
   xmlns:jaxws=”http://java.sun.com/xml/ns/jaxws” xmlns:jaxb=”http://java.sun.com/xml/ns/jaxb”
   jaxb:version=”2.1”>
   <jaxws:bindings node=”wsdl:definitions/wsdl:types/xs:schema”>
       <jaxb:globalBindings generateElementProperty=”false”>
           <jaxb:javaType name=”java.util.Date” xmlType=”xs:dateTime”
               parseMethod=”org.apache.cxf.tools.common.DataTypeAdapter.parseDateTime”
               printMethod=”org.apache.cxf.tools.common.DataTypeAdapter.printDateTime” />
       </jaxb:globalBindings>
   </jaxws:bindings>
</jaxws:bindings>
namespace：用于控制生成的java文件的包名，默认情况会和 targetNamespace 保持一致
2、在命令行运行：
$ grails wsdl2java
| Generating java stubs from grails-app/conf/jaxWsSample.wsdl
| Completed wsdl2java
执行完后，会在 src/java/bropen/wsclient 下自动生成一大堆 java 文件。
3、修改 Config.groovy，补充如下配置：
cxf { 
    client { 
        jaxWsSampleClient { 
            ......
           // 自动生成的接口类的名称：grails wsdl2java 后自动生成java类，找到后把完整的类名复制过来
            clientInterface = bropen.wsclient.JaxWsEndpoint
           // 服务的远程地址，用于正式的调用
            serviceEndpointAddress = ”http://localhost:8080/app/services/jaxWs”
        } 
    } 
} 
此外，这里还可以配置 httpClientPolicy 等属性，详细可参考官方文档，以及 CXF 的文档。
4、run-app后，上面配置的 jaxWsSampleClient 会自动注册为一个 Spring Bean，可以直接依赖注入到控制器、Service类中。在 console 里调用客户端示例：
import javax.activation.DataHandler
import javax.activation.FileDataSource;
// 获得WS客户端
def jaxWsSampleClient = ctx.getBean(”jaxWsSampleClient”)
// 测试日期类型
assert jaxWsSampleClient.testDate(new Date()).class == Date.class;
// 测试附件上传：将 /tmp/test.txt 上传到 /tmp/test2.txt
def file = new File(”/tmp/test.txt”);
file.write(”this is a test file”);    // 创建一个文件
def result = jaxWsSampleClient.testUpload(”test2.txt”, new DataHandler(
   new FileDataSource(file){ 
       public String getContentType() { 
           return new javax.activation.MimetypesFileTypeMap().getContentType(fileName);
       } 
   } 
));
assert result.success == true
assert result.message == null
assert new File(”/tmp/test2.txt”).getText() == ”this is a test file”
// 测试附件下载：下载 /tmp/test.txt，并保存到 /tmp/test3.txt
def fileName = ”test3.txt”
def fileDataHandler = jaxWsSampleClient.testDownload(”test.txt”)
InputStream is = null;
OutputStream os = null;
try { 
   def fn = System.getProperty(”java.io.tmpdir”) + File.separator + fileName
    is = fileDataHandler.getInputStream();
    os = new FileOutputStream( fn );
   byte[] buf = new byte[1024*1024];
   int read;
   while( (read = is.read(buf)) != -1 ) { 
      os.write(buf, 0, read);
      os.flush();
   } 
   assert new File(fn).getText() == ”this is a test file”
}  catch ( Exception e ) { 
   throw e;
}  finally { 
    is?.close();
    os?.close();
} 

补充说明
· 	默认情况下，两个 cxf 的插件是不启用的，需要在工程的 BuildConfig.groovy 中将它们从 “bropen.removed.plugins” 中删除/注释掉即可。
· 	不过安装了 BroBPM 后，会自动启用 grails-cxf 插件，且无法禁用。
· 	BroFramework 中，对 “/services/**” 做了安全控制（通过 Requestmap），并且允许匿名访问
· 	BroFramework 中，UrlMappings 时排除了 “/services/*”，以避免无法传输附件的问题
·