SOA(Service-Oriented Architecture)面向服务架构是一种思想,它将应用程序的不同功能单元通过中立的契约(独立于硬件平台、操作系统和编程语言)联系起来,使得各种形式的功能单元更好的集成。目前来说,WebService 是SOA 的一种较好的实现方WebService 采用HTTP 作为传输协议,SOAP(Simple Object Access Protocol)作为传输消息的格式。但WebService 并不是完全符合SOA 的概念,因为SOAP 协议是WebService 的特有协议,并未符合SOA 的传输协议透明化的要求。SOAP 是一种应用协议,早期应用于RPC 的实现,传输协议可以依赖于HTTP、SMTP 等。SOA 的产生共经历了如下过程:
通常采用SOA 的系统叫做服务总线(BUS),结构如下图所示:
----------------------------------------------------------------------------------------------------------------------------------------
JAVA中的Web服务规范:
JAVA 中共有三种WebService 规范,分别是JAXM&SAAJ、JAX-WS(JAX-RPC)、JAX-RS。
下面来分别简要的介绍一下这三个规范。
(1.)JAX-WS:
JAX-WS(Java API For XML-WebService),JDK1.6 自带的版本为JAX-WS2.1,其底层支
持为JAXB。早期的基于SOAP 的JAVA 的Web 服务规范JAX-RPC(Java API For
XML-Remote Procedure Call)目前已经被JAX-WS 规范取代,JAX-WS 是JAX-RPC 的演进
版本,但JAX-WS 并不完全向后兼容JAX-RPC,二者最大的区别就是RPC/encoded 样式的
WSDL,JAX-WS 已经不提供这种支持。JAX-RPC 的API 从JAVA EE5 开始已经移除,如
果你使用J2EE1.4,其API 位于javax.xml.rpc.*包。
JAX-WS(JSR 224)规范的API 位于javax.xml.ws.*包,其中大部分都是注解,提供API 操
作Web 服务(通常在客户端使用的较多,由于客户端可以借助SDK 生成,因此这个包中的
API 我们较少会直接使用)。
WS-MetaData(JSR 181)是JAX-WS 的依赖规范,其API 位于javax.jws.*包,使用注解配
置公开的Web 服务的相关信息和配置SOAP 消息的相关信息。
(2.)JAXM&SAAJ:
JAXM(JAVA API For XML Message)主要定义了包含了发送和接收消息所需的API,相当
于Web 服务的服务器端,其API 位于javax.messaging.*包,它是JAVA EE 的可选包,因此
你需要单独下载。
SAAJ(SOAP With Attachment API For Java,JSR 67)是与JAXM 搭配使用的API,为构建
SOAP 包和解析SOAP 包提供了重要的支持,支持附件传输,它在服务器端、客户端都需要
使用。这里还要提到的是SAAJ 规范,其API 位于javax.xml.soap.*包。
JAXM&SAAJ 与JAX-WS 都是基于SOAP 的Web 服务,相比之下JAXM&SAAJ 暴漏了SOAP
更多的底层细节,编码比较麻烦,而JAX-WS 更加抽象,隐藏了更多的细节,更加面向对
象,实现起来你基本上不需要关心SOAP 的任何细节。那么如果你想控制SOAP 消息的更
多细节,可以使用JAXM&SAAJ,目前版本为1.3。
(3.)JAX-RS:
JAX-RS 是JAVA 针对REST(Representation State Transfer)风格制定的一套Web 服务规范,
由于推出的较晚,该规范(JSR 311,目前JAX-RS 的版本为1.0)并未随JDK1.6 一起发行,
你需要到JCP 上单独下载JAX-RS 规范的接口,其API 位于javax.ws.rs.*包。
这里的JAX-WS 和JAX-RS 规范我们采用Apache CXF 作为实现,CXF 是Objectweb Celtix
和Codehaus XFire 合并而成。CXF 的核心是org.apache.cxf.Bus(总线),类似于Spring 的
ApplicationContext,Bus 由BusFactory 创建,默认是SpringBusFactory 类,可见默认CXF
是依赖于Spring 的,Bus 都有一个ID,默认的BUS 的ID 是cxf。你要注意的是Apache CXF
2.2 的发行包中的jar 你如果直接全部放到lib 目录,那么你必须使用JDK1.6,否则会报
JAX-WS 版本不一致的问题。对于JAXM&SAAJ 规范我们采用JDK 中自带的默认实现。
------------------------------------------------------------------------------------
1.JAVA的WebService规范JAX-WS:
Web 服务从前面的图中不难看出自然分为Server、Client 两部分,Server 公开Web 服务,
Client 调用Web 服务,JAX-WS 的服务端、客户端双方传输数据使用的SOAP 消息格式封
装数据,在后面我们会看到其实SOAP 信封内包装的就是一段XML 代码。
I.服务端示例:
我们先看一个服务器端示例:
(1.)公开Web 服务的接口IHelloService:
package net.ilkj.soap.server; import javax.jws.WebService; @WebService public interface IHelloService { Customer selectMaxAgeStudent(Customer c1, Customer c2); Customer selectMaxLongNameStudent(Customer c1, Customer c2); }
我们看到这个接口很简单,仅仅是使用类级别注解@WebService 就标注了这个接口的方法
将公开为Web 服务,使用了这个注解的接口的所有方法都将公开为Web 服务的操作,如果
你想屏蔽某个方法,可以使用方法注解@Method 的exclude=true。我们也通常把公开为Web
服务的接口叫做SEI(Service EndPoint Interface)服务端点接口。
(2.)实现类HelloServiceImpl:
package net.ilkj.soap.server; public class HelloServiceImpl implements IHelloService { @Override public Customer selectMaxAgeStudent(Customer c1, Customer c2) { if (c1.getBirthday().getTime() > c2.getBirthday().getTime()) return c2; else return c1; } @Override public Customer selectMaxLongNameStudent(Customer c1, Customer c2) { if (c1.getName().length() > c2.getName().length()) return c1; else return c2; } }
这个实现类没有任何特殊之处,但是如果你的实现类还实现了其他的接口,那么你需要在实
现类上使用@WebService 注解的endpointInterface 属性指定那个接口是SEI(全类名)。
(3.)Customer 类:
package net.ilkj.soap.server; import java.util.Date; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "Customer") public class Customer { private long id; private String name; private Date birthday; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } }
这个类是公开为Web 服务的接口中的参数类型和返回值,因此你需要使用JAXB 注解告诉
CXF 如何在XML和Java Object 之间处理,因为前面说过SOAP 消息格式包装的是一段XML
代码,那么无论是服务器端还是客户端在接收到SOAP 消息时都需要将XML 转化为Java
Object,在发送SOAP 消息时需要将Java Object 转化为XML。
(4.)发布Web 服务:
package net.ilkj.soap.server; import javax.xml.ws.Endpoint; public class SoapServer { public static void main(String[] args) { Endpoint.publish("http://127.0.0.1:8080/helloService", new HelloServiceImpl()); } }
注意我们发布Web 服务使用的是javax.xml.ws.*包中的EndPoint 的静态方法publish()。
(5.)查看WSDL:
我们访问http://127.0.0.1:8080/helloService?wsdl 地址,您会看到很长的XML 文件(由于浏
览器的问题,如果你看到的是空白页面,请查看源代码),这就是WSDL(WebService Definition
Language),对于你要访问的Web 服务,只要在其地址后加上,就可以在浏览器中查看用于
描述Web 服务的WSDL,这也是一种XML,Web 服务能够被各种编程语言书写的程序访
问就是通过WSDL 这种通用的契约来完成的。
如果你已经看到WSDL,那么表示我们的Web 服务发布成功了。你可能会差异,我们没有
借助Tomcat 这样的Web 服务器,直接运行一个main 方法是怎么发布的Web 服务呢?其实
CXF 内置了Jetty(Servlet 容器),因此你不需要将你的程序部署到Tomcat 等Web 服务器
也可以正常发布Web 服务。
------------------------------------------------------------------------------------
II.分析WSDL的构成:
下面我们来解释一下你所看到的WSDL 的结构,你可以对照你所生成WSDL(文件太长,
Word 里实在放不下)。
(1.)
口的实现类+Service(上例中为name="HelloServiceImplService",不同的JAX-WS
实现名字是不一样的);targetNamespace 指定目标名称空间,targetNamespace 的值被后面
的xmlns:tns 属性作为值, 默认是使用接口实现类的包名的反缀
(targetNamespace="http://server.soap.ilkj.net/" …
xmlns:tns="http://server.soap.ilkj.net/"),
你可以使用@WebService 注解的targetNamespace 属性指定你想要的名称空间。
(2.)
元素定义
及一个复杂类型Customer,Customer 的@XmlRootElement 注解的name 属性值为Customer,
因此你会看到
再向下你会看到XXX 元素和XXXResponse 元素,其中XXX 是方法名称(你可以使用
@WebMethod 的operationName 属性值指定XXX 的值),XXX 是对方法参数的封装,
XXXResponse 是对返回值的封装,上例中你会看到
内容,
最 后你会看到一组
我们看到方法参数名称为arg0、arg1、…,如果你想指定方法参数的名字在方法参数前使用
@WebParam 的name 属性指定值,同样,方法的返回值同样可以使用@WebResult 注解指定
相关的属性值。
例如:
@WebResult(name = "method")
Customer selectMaxAgeStudent(@WebParam(name = "c1") Customer c1,
@WebParam(name = "c2") Customer c2);
(3.)
息。
(4.)
属性默认为接口名称(你可以使用@WebService 注解的name 属性指定值)。这个元素包含
了一系列的
(5.)
着依赖关系),其中的
(6.)
III.客户端调用示例:
我们从上面可以知道Web 服务只向客户端暴漏WSDL,那么客户端必须将WSDL 转换为自
己的编程语言书写的代码。JAX-WS 的各种实现都提供相应的工具进行WSDL 与JAVA 之
间的互相转换,你可以在CXF 的运行包中找到bin 目录,其中的wsdl2java.bat 可以将WSDL
转换为JAVA 类,bin 目录的各种bat 的名字可以很容易知道其作用,但要注意JAVA 类转
换为WSDL 最好使用前面的URL?wsdl 的方式获得,因为这样得到的是最准确的。
你可以在命令行将当前目录切换到CXF 的bin 目录,然后运行wsdl2java –h 查看这个批处理
命令的各个参数的作用,常用的方式就是wsdljava –p 包路径 –d 目标文件夹 wsdl 的url
地址。现在我们将前面的WSDL生成客户端代码:
wsdl2java -p net.ilkj.soap.client –d E:/ http://127.0.0.1:8080/helloService?wsdl
你会在E 盘根目录找到生成的客户端代码,然后将它复制到Eclipse 工程即可使用。
如果你使用MyEclipse,可以按照如下步骤从WSDL 生成客户端代码:
New--->Other--->MyEclipse--->Web Services--->Web Services Client,然后依据设置向导即可
完成,但最好还是使用CXF 的wsdl2java 来完成,因为CXF2.2+版本开始支持JAX-WS2.1
规范,而MyEclipse 自带的好像是XFire 的wsdl2java,生成的客户端代码可能不是最新规范
的。
我们上面的WSDL 会生成如下所示的客户端代码:
Customer.java
HelloServiceImplService.java
IHelloService.java
ObjectFactory.java
package-info.java
SelectMaxAgeStudent.java
SelectMaxAgeStudentResponse.java
SelectMaxLongNameStudent.java
SelectMaxLongNameStudentResponse.java
其中package-info.java、ObjectFactory.java 是JAXB 需要的文件;HelloServiceImplService.java
继承自javax.xml.ws.Service 类,用于提供WSDL 的客户端视图,里面使用的是大量
javax.xml.ws.*包中的注解;剩下的类是Web 服务的接口、方法参数、响应值的类。
在 CXF 中使用JaxWsProxyFactoryBean 客户端代理工厂调用Web 服务,代码如下所示:
package net.ilkj.soap.client; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.GregorianCalendar; import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; import com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendar Impl; public class SoapClient { public static void main(String[] args) throws ParseException { JaxWsProxyFactoryBean soapFactoryBean = new JaxWsProxyFactoryBean(); soapFactoryBean.setAddress("http://127.0.0.1:8080/helloService"); soapFactoryBean.setServiceClass(IHelloService.class); Object o = soapFactoryBean.create(); IHelloService helloService = (IHelloService) o; Customer c1 = new Customer(); c1.setId(1); c1.setName("A"); GregorianCalendar calendar = (GregorianCalendar) GregorianCalendar .getInstance(); calendar .setTime(new SimpleDateFormat("yyyy-MM-dd").parse("1989-01-28")); c1.setBirthday(new XMLGregorianCalendarImpl(calendar)); Customer c2 = new Customer(); c2.setId(2); c2.setName("B"); calendar .setTime(new SimpleDateFormat("yyyy-MM-dd").parse("1990-01-28")); c2.setBirthday(new XMLGregorianCalendarImpl(calendar)); System.out.println(helloService.selectMaxAgeStudent(c1, c2).getName()); } }
这里要注意的就是Customer 的生日字段JAX-WS 在客户端映射为了XMLGregorianCalendar
类型。我们运行这个客户端,结果输出A,我们的Web 服务调用成功。你还要注意Web 服
务调用可能经常出现超时的问题,但你切不可以为只要WSDL 可以访问,就代表Web 服务
一定可以访问,因为是否可以访问与SEI 的实现类有关,而WSDL 仅是SEI 的一种XML
表示。
------------------------------------------------------------------------------------
IV.SOAP消息的格式:
我们从前面了解WebService 使用HTTP 协议传输消息,消息格式使用SOAP,那么在客户
端和服务器端传输的SOAP 消息是什么样子的呢?下面我们将服务端SoapServer.java 的代
码改为如下的形式:
package net.ilkj.soap.server; import org.apache.cxf.interceptor.LoggingInInterceptor; import org.apache.cxf.interceptor.LoggingOutInterceptor; import org.apache.cxf.jaxws.JaxWsServerFactoryBean; public class SoapServer { public static void main(String[] args) { JaxWsServerFactoryBean soapFactoryBean = new JaxWsServerFactoryBean(); soapFactoryBean.getInInterceptors().add(new LoggingInInterceptor()); soapFactoryBean.getOutInterceptors().add(new LoggingOutInterceptor()); // 注意这里是实现类不是接口 soapFactoryBean.setServiceClass(HelloServiceImpl.class); soapFactoryBean.setAddress("http://127.0.0.1:8080/helloService"); soapFactoryBean.create(); } }
我们注意到这里将javax.xml.ws.EndPoint 改为CXF 特有的API---JaxWsServerFactoryBean,
并且我们对服务端工厂Bean 的输入拦截器集合、输出拦截器集合中分别添加了日志拦截器
(拦截器是CXF 的一项扩展功能,CXF 提供了很多拦截器实现,你也可以自己实现一种拦
截器),这样可以在Web 服务端发送和接收消息时输出信息。
现在我们再次运行服务器端和客户端,你会看到控制台输出如下信息:
2009-6-17 22:35:57 org.apache.cxf.interceptor.LoggingInInterceptor
logging
信息: Inbound Message
----------------------------
ID: 2
Address: /helloService
Encoding: UTF-8
Content-Type: text/xml; charset=UTF-8
Headers: {content-type=[text/xml; charset=UTF-8],
connection=[keep-alive], Host=[127.0.0.1:8080], Content-Length=[367],
SOAPAction=[""], User-Agent=[Apache CXF 2.2.2], Content-Type=[text/xml;
charset=UTF-8], Accept=[*/*], Pragma=[no-cache],
Cache-Control=[no-cache]}
Payload:
xmlns:ns2="http://server.soap.ilkj.net/">
00:00.000+08:00
1990-01-28T00:00:00.000+08:00
--------------------------------------
2009-6-17 22:35:57
org.apache.cxf.interceptor.LoggingOutInterceptor$LoggingCallback
onClose
信息: Outbound Message
---------------------------
ID: 2
Encoding: UTF-8
Content-Type: text/xml
Headers: {}
Payload:
xmlns:ns2="http://server.soap.ilkj.net/">
T00:00:00+08:00
--------------------------------------
Inbound Message 输出的是服务器端接收到的SOAP 信息,Outbound Message 输出的服务器
端响应的SOAP 信息,SOAP 的Headers:{}的前面是SOAP 消息的标识、编码方式、MIME
类型,Headers:{}熟悉HTTP 应该很容易看懂这里面的消息报头的作用,Headers:{}后面的
Payload(有效负载,也叫净荷)的XML 就是SOAP 消息的真正内容,我们看到SOAP 消
息内容被封装为
package net.ilkj.soap.server; import javax.jws.WebParam; import javax.jws.WebService; @WebService public interface IHelloService { Customer selectMaxAgeStudent( @WebParam(name = "c1", header = true) Customer c1, @WebParam(name = "c2") Customer c2); Customer selectMaxLongNameStudent(Customer c1, Customer c2); }
我们注意第一个方法的第一个参数的header=true,也就是放在SOAP 的消息头中传输。然
后我们重新生成客户端的代码,SoapClient 的调用代码改为如下的形式:
package net.ilkj.soap.client; ------------------------------------------------------------------------------------------------------------------------------- 暂时先这些吧
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.GregorianCalendar;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import
com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendar
Impl;
public class SoapClient {
public static void main(String[] args) throws ParseException {
JaxWsProxyFactoryBean soapFactoryBean = new
JaxWsProxyFactoryBean();
soapFactoryBean.setAddress("http://127.0.0.1:8080/helloService");
soapFactoryBean.setServiceClass(IHelloService.class);
Object o = soapFactoryBean.create();
IHelloService helloService = (IHelloService) o;
Customer c1 = new Customer();
c1.setId(1);
c1.setName("A");
GregorianCalendar calendar = (GregorianCalendar)
GregorianCalendar
.getInstance();
calendar
.setTime(new
SimpleDateFormat("yyyy-MM-dd").parse("1989-01-28"));
c1.setBirthday(new XMLGregorianCalendarImpl(calendar));
Customer c2 = new Customer();
c2.setId(2);
c2.setName("B");
calendar
.setTime(new
SimpleDateFormat("yyyy-MM-dd").parse("1990-01-28"));
c2.setBirthday(new XMLGregorianCalendarImpl(calendar));
SelectMaxAgeStudent sms = new SelectMaxAgeStudent();
sms.setC2(c2);
System.out.println(helloService.selectMaxAgeStudent(sms, c1)
.getReturn().getName());
}
}
我们注意到现在客户端的IHelloService 的第一个方法的第一个参数是SelectMaxAgeStudent,
而不是Customer,运行之后控制台输出如下语句:
2009-6-17 23:02:29 org.apache.cxf.interceptor.LoggingInInterceptor
logging
信息: Inbound Message
----------------------------
ID: 2
Address: /helloService
Encoding: UTF-8
Content-Type: text/xml; charset=UTF-8
Headers: {content-type=[text/xml; charset=UTF-8],
connection=[keep-alive], Host=[127.0.0.1:8080], Content-Length=[443],
SOAPAction=[""], User-Agent=[Apache CXF 2.2.2], Content-Type=[text/xml;
charset=UTF-8], Accept=[*/*], Pragma=[no-cache],
Cache-Control=[no-cache]}
Payload:
ns2:c1
xmlns:ns2="http://server.soap.ilkj.net/">
0.000+08:00
00:00.000+08:00
--------------------------------------
2009-6-17 23:02:29
org.apache.cxf.interceptor.LoggingOutInterceptor$LoggingCallback
onClose
信息: Outbound Message
---------------------------
ID: 2
Encoding: UTF-8
Content-Type: text/xml
Headers: {}
Payload:
xmlns:ns2="http://server.soap.ilkj.net/">
T00:00:00+08:00
--------------------------------------
我 们注意到Inbound Message 中的SOAP 信封将第一个方法的第一个参数放在