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定义:
该定义中,自动生成了一个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/*”,以避免无法传输附件的问题