一般动作
登录
Wiki: BROPEN ▼: Document Index
»
Space: FAQ ▼: Document Index
»
Page: 系统集成之Web服务（WebService）
搜索
zh
页面动作
导出 ▼: 以PDF格式导出 以RTF格式导出 以HTML格式导出
更多动作 ▼: 打印预览 察看源码
欢迎进入Wiki » FAQ » 系统集成之Web服务（WebService）?

# 系统集成之Web服务（WebService）?

在2013-12-11 11:35上被李小翔修改
评论 (0) · 附件 (1) · 记录 · 信息
目录

- 服务端开发：创建 Endpoint
  - Simple 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();
}

# 补充说明

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上被李小翔创建

- 评论 （0）
- 附件 （1）
- 记录
- 信息

# 快速链接

- 首页
- 新闻动态
- FAQ

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