欢迎进入Wiki » FAQ » 系统集成之Web服务(WebService)?

系统集成之Web服务(WebService)?

在2013-12-10 21:15上被李小翔修改
评论 (0) · 附件 (0) · 记录 · 信息

BroToolkit 中内置了 WS 的几个插件:grails-cxfgrails-cxf-clientgroovy-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定义:

该定义中,自动生成了一个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:7090/boeWorkbench/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用的定制文件,其详细语法可以参考官方文档,如 jaxbjaxws,这里仅仅是进行日期类型转换。
    该文件的完整内容如下:
    <?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

// 测试附件下载:下载 /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();
}

补充说明

  1. 默认情况下,两个 cxf 的插件是不启用的,需要在工程的 BuildConfig.groovy 中将它们从 “bropen.removed.plugins” 中删除/注释掉即可。
    不过安装了 BroBPM 后,会自动启用 grails-cxf 插件,且无法禁用。
  2. BroFramework 中,对 “/services/**” 做了安全控制(通过 Requestmap),并且允许匿名访问
  3. BroFramework 中,UrlMappings 时排除了 “/services/*”,以避免无法传输附件的问题
标签: grails WebService
在2013-12-10 16:51上被李小翔创建

Copyright © 2013 北京博瑞开源软件有限公司
京ICP备12048974号