引入:
通常意义上说,SOAP消息就是一个带字符串的内容包,其实CXF还可以发送/接收带附件的SOAP消息,这样SOAP的消息看起来就如下所示:
我们这篇文章就着重讲解如何来从客户端发送带附件的SOAP消息到服务器Endpoint,并且给出详细的例子,下篇文章会讲解相反过程,如何客户端接收并且处理来自Endpoint的带附件的SOAP消息。
实践:
我们假想有这样一个需求,假设我们现在有个ProfileManagement系统,用户需要在客户端吧自己的Profile上传到ProfileManagement系统,并且这个Profile中除了包含姓名,年龄外还包含自己的头像图片(portrait),所以如果用CXF来做的话,就是要上传一个带附件的SOAP消息。
服务端:
首先,我们要定义一个VO来表示Profile信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
/**
* 这个一个VO,我们定义了一个Profile类型,它会带有图片的附件信息。接下来,我们会用JAXB框架将其映射为wsdl文件中的类型定义
*/
package
com.charles.cxfstudy.server.vo;
import
javax.activation.DataHandler;
import
javax.xml.bind.annotation.XmlAccessType;
import
javax.xml.bind.annotation.XmlAccessorType;
import
javax.xml.bind.annotation.XmlMimeType;
import
javax.xml.bind.annotation.XmlRootElement;
import
javax.xml.bind.annotation.XmlType;
/**
* 我们定义一个有姓名(name),年龄(age),图片(portrait)的Profile类
* @author charles.wang
*
*/
@XmlType
(name=
"profile"
)
//注意,这里必须用下面这行指定XmlAccessType.FIELD,来标注利用JAXB框架在JAVA和XML之间转换只关注字段,
//而不会关注getter(),否则默认2个都会关注则会发生以下错误:
//com.sun.xml.bind.v2.runtime.IllegalAnnotationException
//Class has two properties of the same name "portrait",我调试了好久才解决这个问题的
@XmlAccessorType
(XmlAccessType.FIELD)
public
class
Profile {
private
String name;
private
int
age;
//这个字段是一个图片资源
@XmlMimeType
(
"application/octet-stream"
)
private
DataHandler portrait;
private
String imageFileExtension;
public
String getName() {
return
name;
}
public
void
setName(String name) {
this
.name = name;
}
public
int
getAge() {
return
age;
}
public
void
setAge(
int
age) {
this
.age = age;
}
public
DataHandler getPortrait() {
return
portrait;
}
public
void
setPortrait(DataHandler portrait) {
this
.portrait = portrait;
}
public
String getImageFileExtension() {
return
imageFileExtension;
}
public
void
setImageFileExtension(String imageFileExtension) {
this
.imageFileExtension = imageFileExtension;
}
}
|
和一般情况一样,我们这里用了JAXB的注释,这样JAXB框架会自动的把我们的基于JAX-WS的请求/响应参数转为XML格式的消息。特别注意有2点:
a.对于附件资源,我们这里是用了@XmlMimeType("application/octet-stream")来标示,并且其字段类型为DataHandler,这样的目的是为了能让JAXB框架正确的处理附件资源,其实这里MIME类型也可以设为具体类型,比如 image/jpeg, 但是这种用法更通用。
b.需要加@XmlAccessorType(XmlAccessType.FIELD),这样可以使得JAXB只处理字段类型到附件的映射,而不会处理getter()方法,否则会报错
com.sun.xml.bind.v2.runtime.IllegalAnnotationException
//Class has two properties of the same name "portrait"
然后,因为我们用的是JAX-WS的调用方式,所以我们可能从代码中看不到真正SOAP消息长什么样子,所以我们这里定义了一个LogHandler(具体参见http://supercharles888.blog.51cto.com/609344/1361866 ),它可以把入Endpoint和出Endpoint的SOAP消息进行区分,并且把消息内容打印到服务器的控制台上:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
/**
* SOAP Handler可以用来对SOAP消息进行访问。
* 这里演示的是第一种,它必须实现SOAPHandler<SOAPMessageContext>接口
*/
package
com.charles.cxfstudy.server.handlers;
import
java.util.Set;
import
javax.xml.namespace.QName;
import
javax.xml.soap.SOAPMessage;
import
javax.xml.ws.handler.MessageContext;
import
javax.xml.ws.handler.soap.SOAPHandler;
import
javax.xml.ws.handler.soap.SOAPMessageContext;
/**
* 记录进/出Endpoint的消息到控制台的Handler
*
* @author charles.wang
*
*/
public
class
LogHandler
implements
SOAPHandler<SOAPMessageContext> {
/**
* 如何去处理SOAP消息的逻辑。 这里会先判断消息的类型是入站还是出站消息,然后把消息写到标准输出流
*/
public
boolean
handleMessage(SOAPMessageContext context) {
// 先判断消息来源是入站还是出站的
// (入站表示是发送到web service站点的消息,出站表示是从web service站点返回的消息)
boolean
outbound = (Boolean) context
.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
// 如果是出站消息
if
(outbound) {
System.out.println(
"这是从Endpoint返回到客户端的SOAP消息"
);
}
else
{
System.out.println(
"这是客户端的请求SOAP消息"
);
}
SOAPMessage message = context.getMessage();
try
{
message.writeTo(System.out);
System.out.println(
'\n'
);
}
catch
(Exception ex) {
System.err.print(
"Exception occured when handling message"
);
}
return
true
;
}
/**
* 如何去处理错误的SOAP消息 这里会先打印当前调用的方法,然后从消息上下文中取出消息,然后写到标准输出流
*/
public
boolean
handleFault(SOAPMessageContext context) {
SOAPMessage message = context.getMessage();
try
{
message.writeTo(System.out);
System.out.println();
}
catch
(Exception ex) {
System.err.print(
"Exception occured when handling fault message"
);
}
return
true
;
}
/**
* 这里没有资源清理的需求,所以我们只打印动作到控制台
*/
public
void
close(MessageContext context) {
System.out.println(
"LogHandler->close(context) method invoked"
);
}
public
Set<QName> getHeaders() {
return
null
;
}
}
|
我们定义一个HandlerChain文件,把LogHandler加进去:
1
2
3
4
5
6
7
8
9
10
|
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
<
handler-chains
xmlns
=
"http://java.sun.com/xml/ns/javaee"
>
<
handler-chain
>
<!-- 配置可以记录出/入Endpoint消息内容到控制台的Handler -->
<
handler
>
<
handler-name
>LogHandler</
handler-name
>
<
handler-class
>com.charles.cxfstudy.server.handlers.LogHandler</
handler-class
>
</
handler
>
</
handler-chain
>
</
handler-chains
>
|
现在我们就开始写服务器端的处理逻辑了,我们可以让其从客户端发过来的带附件的SOAP消息中提取一般字段和附件,对于附件的图片(头像文件),我们复制到指定位置。
首先,我们定义SEI,它是个服务接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/**
* 这是一个web服务接口定义,定义了如何对于上传的Profile进行处理
*/
package
com.charles.cxfstudy.server.services;
import
javax.jws.WebParam;
import
javax.jws.WebService;
import
com.charles.cxfstudy.server.vo.Profile;
/**
* @author Administrator
*
*/
@WebService
public
interface
IUploadProfileService {
/**
* 上传Profile,并且对于Profile进行处理
*/
void
uploadProfile(
@WebParam
(name=
"profile"
) Profile profile);
}
|
然后我们定义SIB,它实现了SEI:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
/**
* 服务实现类,提供上传Profile的服务
*/
package
com.charles.cxfstudy.server.services;
import
java.io.FileOutputStream;
import
java.io.IOException;
import
java.io.InputStream;
import
java.io.OutputStream;
import
javax.activation.DataHandler;
import
javax.jws.HandlerChain;
import
javax.jws.WebParam;
import
javax.jws.WebService;
import
com.charles.cxfstudy.server.vo.Profile;
/**
* @author charles.wang
*
*/
@WebService
(endpointInterface =
"com.charles.cxfstudy.server.services.IUploadProfileService"
)
@HandlerChain
(file=
"/handler_chains.xml"
)
public
class
UploadProfileServiceImpl
implements
IUploadProfileService {
/**
* 上传Profile,并且对于Profile进行处理
*/
public
void
uploadProfile(
@WebParam
(name=
"profile"
) Profile profile){
//从参数中获得相关信息
String name = profile.getName();
//姓名
int
age = profile.getAge();
//年龄
DataHandler portrait = profile.getPortrait();
//肖像图片
String imageFileExtension = profile.getImageFileExtension();
//肖像图片的扩展名
try
{
//获取输入流,来获取图片资源
InputStream is = portrait.getInputStream();
//打开一个输出流,来保存获得的图片
OutputStream os =
new
FileOutputStream(
"d:/tmp/uploadTo/"
+name+
"."
+imageFileExtension);
//进行复制,把获得的肖像图片复制到
byte
[] b =
new
byte
[
10000
];
int
byteRead=
0
;
while
( (byteRead=is.read(b))!= -
1
){
os.write(b,
0
,byteRead);
}
os.flush();
os.close();
is.close();
}
catch
(IOException ex){
ex.printStackTrace();
}
}
}
|
大体上代码很容易读懂(呵呵,我自信我代码的可读性),和惯例一样,我们用@HandlerChain注解来使得LogHandler可以作用到当前的服务类上并且打印进出当前服务器类的SOAP消息。
为了让MTOM生效(也就是让服务器端支持对带附件的SOAP消息处理),我们必须在当前SIB的bean定义文件中打开MTOM开关,如下的20-22行(在beans.xml中):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
<
beans
xmlns
=
"http://www.springframework.org/schema/beans"
xmlns:xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xmlns:jaxws
=
"http://cxf.apache.org/jaxws"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">
<!-- 导入cxf中的spring的一些配置文件,他们都在cxf-<version>.jar文件中 -->
<
import
resource
=
"classpath:META-INF/cxf/cxf.xml"
/>
<
import
resource
=
"classpath:META-INF/cxf/cxf-servlet.xml"
/>
<!-- 这里是第一个web service,它提供上传用户Profile的功能(主要演示发送带附件的SOAP消息到服务器) -->
<
jaxws:endpoint
id
=
"uploadProfileService"
implementor
=
"com.charles.cxfstudy.server.services.UploadProfileServiceImpl"
address
=
"/uploadProfile"
>
<!-- 下面这段注释在服务器端开启了MTOM,所以它可以正确的处理来自客户端的带附件的SOAP消息 -->
<
jaxws:properties
>
<
entry
key
=
"mtom-enabled"
value
=
"true"
/>
</
jaxws:properties
>
</
jaxws:endpoint
>
...
</
beans
>
|
现在打包完部署应用到服务器上,运行就可以了。
客户端:
现在我们来编写客户端,其实很简单,主要就是封装一个Profile对象(带附件),然后调用业务方法进行上传操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
/**
* 客户端测试代码
*/
package
com.charles.mtom.sendattachedsoap;
import
java.util.HashMap;
import
java.util.Map;
import
javax.activation.DataHandler;
import
javax.activation.DataSource;
import
javax.activation.FileDataSource;
import
org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
/**
* @author charles.wang
*
*/
public
class
MainTest {
public
static
void
main(String [] args){
JaxWsProxyFactoryBean factory =
new
JaxWsProxyFactoryBean();
factory.setServiceClass(IUploadProfileService.
class
);
//下面三行代码:激活客户端能处理带附件的SOAP消息的功能:
Map<String,Object> props =
new
HashMap<String,Object>();
props.put(
"mtom-enabled"
, Boolean.TRUE);
factory.setProperties(props);
factory.setAddress(
"http://localhost:8080/cxf_mtom_service/services/uploadProfile"
);
//调用业务方法
IUploadProfileService service = (IUploadProfileService) factory.create();
//创建 一个要上传的Profile对象
Profile profile =
new
Profile();
profile.setName(
"Charles"
);
profile.setAge(
28
);
//下面两行特别注意如何去吧一个附件附加到请求中的
DataSource source =
new
FileDataSource(
"F:/images/myprofileImage.jpg"
);
profile.setPortrait(
new
DataHandler(source));
profile.setImageFileExtension(
"jpg"
);
//调用业务方法,发送soap请求
service.uploadProfile(profile);
}
}
|
条理也很清楚,这里特别注意是第23-25行激活了客户端对MTOM的支持。第36到39行演示了如何把一个附件(比如图片文件)附加到SOAP消息上。
我们运行例子。
可以清楚的看到从客户端发送的SOAP消息和从服务器端返回的SOAP消息,显然,发送的消息是以attachment的形式附加在SOAP消息上的,符合我们文章开始的结构示意图。
我们去文件系统检查,果然,客户端通过调用,把图片文件从F:/Images/myProfileImage.jpg传递给了webservice,然后webservice从SOAP消息中拿到附件,并且改名为Charles.jpg,然后存储到了D:/tmp/uploadTo目录
额外话题:
注意,对于客户端代码的开启MTOM的支持是必不可少的:
1
2
3
4
|
//下面三行代码:激活客户端能处理带附件的SOAP消息的功能:
Map<String,Object> props =
new
HashMap<String,Object>();
props.put(
"mtom-enabled"
, Boolean.TRUE);
factory.setProperties(props);
|
如果没有这3行的话,我们发送的SOAP消息就是一个不带附件的消息,而是把我们的资源文件(比如图片)转为Base64,然后把编码后的内容和普通字段一样放在SOAP消息中。
比如,当我们注释掉上面几行,再发送请求的时候,其请求的SOAP消息就如下:
这显然就不是一个带附件的SOAP消息了,因为Base64编码很低效,要大量运算,所以如果附件文件很大,那么转为Base64就会花费很多时间,这不是一个很好的选择。