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,简单的说就是服务接口,每个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 前端的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定义:
该定义中,自动生成了一个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 的 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
    }
}
测试略。
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 的 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。
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/*”,以避免无法传输附件的问题