理论背景:《新手初识webservice-理论篇》http://my.oschina.net/achi/blog/52766
编程语言:Java
必要技能:JavaWeb,MVC,Spring IOC
情景假设:你刚用Java做了个网上店铺,用了servlet,MVC分层,Spring IOC,MySQL之类的,初学JavaWeb时老师都会让做一个的那种,它的其中一个功能是根据页面传来的商品数量和商品单价计算总额并返回。然后你漂亮的女同学-Linda-对你说,帅哥我也刚做了个Java的店铺系统,但是根据商品数量和商品单价计算总额并返回这块儿不会写,你帮我写吧~于是你决定把自己原来的方法做些修改,然后发布成webservice让Linda调用。
框架介绍:Java中针对webservice开发的框架我接触到的有cxf(最新版本2.5.2)和axis(工作中使用的1.4),本文介绍的是cxf,因为cxf和Spring高度集成,通过简单的配置和适量的代码就能完成服务端和客户端的开发,我觉得适合新手入门,等有一定使用经验了再去了解细节也更容易。cxf的官网为http://cxf.apache.org/,下面项目中用到的cxf方面的jar都可以从http://www.apache.org/dyn/closer.cgi?path=/cxf/2.5.2/apache-cxf-2.5.2.zip下载到,另外本文主要参考了http://cxf.apache.org/docs/writing-a-service-with-spring.html
正文如下
打开你最熟悉的IDE,这里使用的是Eclipse3.7,新建一个web工程,我们命名为cxf_server,结构如下
然后是所需要的jar包(请注意servlet相关的几个jar包是外部引用的)
首先定义bean
public class Order{ /** 商品名 */ private String name; /** 数量 */ private int amount; /** 单价 */ private BigDecimal price; /** 总价 */ private BigDecimal total; // 省略getter/setter
然后是形式主义的返回报文
public class ApplicationResponse { /** 执行结果标志 1 成功 0 失败 */ private int flag; /** 订单内容 */ private Order order; /** 服务端返回的其他信息 */ private String message; // 省略getter/setter
下面是我们要发布成webservice的service层的接口
/** * 通过@WebService注解定义成对外开放的订单处理接口 */ @WebService public interface OrderService { /** * 处理订单并返回结果 */ public ApplicationResponse processOrder(Order order); }
这里最重要的一点就是@WebService注解,通过这个注解,后面cxf框架才会将这个普通的Java接口类发布为webservice服务
然后是这个接口的实现类
/** * 对外开放的接口实现类,同样需要@WebService注解,如果该类同时实现了其他接口,则必须加上endpointInterface参数指定接口 */ @WebService(endpointInterface = "foo.cxf.service.OrderService") public class OrderServiceImpl implements OrderService { private OrderDAO orderDAO; public ApplicationResponse processOrder(Order order) { ApplicationResponse response = new ApplicationResponse(); if (order != null && order.getName() != null && order.getPrice() != null && (Integer) order.getAmount() != null) { order = orderDAO.processOrder(order); if (order.getTotal() != null) { response.setFlag(1); response.setOrder(order); StringBuilder sb = new StringBuilder(); sb.append("Your order: ").append(order.getName()).append(" * ") .append(order.getAmount()).append("\n"); sb.append("Total money is ").append(order.getTotal()); response.setMessage(sb.toString()); } else { response.setFlag(0); response.setOrder(order); response.setMessage("计算失败"); } } else { response.setFlag(0); response.setOrder(order); response.setMessage("订单信息不全"); } return response; }
如注释所说,对外开放的接口实现类,同样需要@WebService注解,如果该类同时实现了其他接口,则必须加上endpointInterface参数指定接口。里边的方法就比较简单了,检查参数,然后调用DAO方法,并返回结果,和普通的service方法没区别
下面是OrderDAO
/** * 订单处理接口 */ public interface OrderDAO { /** * 处理订单 */ public Order processOrder(Order order); }
它就是一个普通的DAO接口
然后是DAO接口的实现类
/** * 订单处理实现类 */ public class OrderDAOImpl implements OrderDAO { public Order processOrder(Order order) { BigDecimal total = order.getPrice().multiply(new BigDecimal(order.getAmount())); order.setTotal(total); return order; } }
这里,本来应该是执行数据库操作之类的东西的,为了节省篇幅,只让它执行了一个简单的计算
到此,代码相关的东西就结束了,然后我们开始Spring的相关配置
首先是component-service.xml
<bean id="orderService" class="foo.cxf.service.OrderServiceImpl"> <property name="orderDAO" ref="orderDAO" /> </bean> <bean id="orderDAO" class="foo.cxf.dao.OrderDAOImpl"></bean>
这个是普通的Spring的配置
然后是component-cxf.xml,这个配置文件实现了将service接口发布成webservice服务
<import resource="classpath:META-INF/cxf/cxf.xml" /> <import resource="classpath:META-INF/cxf/cxf-servlet.xml" /> <jaxws:endpoint id="orderProcess" implementor="#orderService" address="/bar/OrderProcess"> <jaxws:features> <bean class="org.apache.cxf.feature.LoggingFeature"/> </jaxws:features> </jaxws:endpoint> <!-- #orderService指向component-service.xml中的orderService -->
两个import引入了cxf的一些框架配置,具体内容我还没研究过,初学的话,先用过了再研究细节吧
<jaxws:endpoint>标签定义了我们要发布的webservice服务,id属性和普通的Spring的bean的id是一样的,然后implementor指向的是Spring的某个bean的id,注意前面要加一个"#",最后address是我们要发布的webservice的服务的相对地址,它的目录结构我们可以随意指定
然后<jaxws:features>标签是用来打log的,这样写的话打的log比较不爽,但是更详细的配置我还没研究,先这样吧。。。
接下来是webl.xml的配置
<!-- Spring config --> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath*:component-*.xml </param-value> </context-param> <!-- log4j config --> <context-param> <param-name>webAppRootKey</param-name> <param-value>cxf.root</param-value> </context-param> <context-param> <param-name>log4jConfigLocation</param-name> <param-value>/WEB-INF/classes/log4j.properties</param-value> </context-param> <context-param> <param-name>log4jRefreshInterval</param-name> <param-value>60000</param-value> </context-param> <!-- Spring listener --> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <!-- log4j listener --> <listener> <listener-class> org.springframework.web.util.Log4jConfigListener </listener-class> </listener> <!-- CXF servlet config --> <servlet> <servlet-name>CXFServlet</servlet-name> <display-name>CXF Servlet</display-name> <servlet-class> org.apache.cxf.transport.servlet.CXFServlet </servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>CXFServlet</servlet-name> <url-pattern>/foo/*</url-pattern> </servlet-mapping>
Spring和log4j的配置就不多说了,这里重点解释下cxf servlet config
<servlet>标签的class就按默认的来写,<load-on-startup>值为1是为了在容器启动时就加载该servlet
<servlet-mapping>中的<url-pattern>标签定义了cxf servlet的目录,这个目录是上文component-cxf.xml中address的父目录,即完整的webservice服务的url格式为:http://hostname:port/appname/url-pattern/address,例如http://127.0.0.1:8080/cxf/foo/bar/OrderProcess
将我们的工程发布到tomcat,设置path为/cxf,成功启动tomcat后,访问http://127.0.0.1:8080/cxf/foo/bar/OrderProcess?wsdl,可以看到如下图所示的内容
出现这个,表示我们的webservice已经成功发布了(话说这个xml内容就是平时所说的wsdl接口来),观察下内容,会发现我们发布的方法里涉及到的实体类如Order、ApplicationResponse、接口方法如processOrder等这里面都有,还有一些如OrderServiceImplServiceSoapBinding等奇怪的东西(这些是为SOAP服务的),也就是说到这里,我们的Java应用里的一个普通的接口方法已经被发布成使用“中立的契约”的webservice服务了,任何平台、硬件和语言的应用,只要满足了webservice规范,就可以调用这个服务。
关于服务端的开发到此就结束了,很多细节和wsdl的内容我没有细讲,一方面是我也不懂,一方面是我觉得对于新手熟悉新框架,先搭建并跑起来一个最小功能的demo比上来就给一大堆的理论更直观
接下来是客户端的开发了
首先回顾下最上面的情景假设,帅哥是为美女同学开发webservice服务的,因此开发工作完成后,帅哥需要告诉美女一些必要的东西,包括:
1、webservice服务的url,如http://127.0.0.1:8080/cxf/foo/bar/OrderProcess?wsdl
2、webservice服务的接口方法,如processOrder
3、接口方法的参数及其字段说明,最好是以文档形式,如Order{"String|name|商品名|必填", "int|amount|数量|必填", "BigDecimal|price|单价|必填", "BigDecimal|total|总价|非必填"}
4、接口方法的返回值及其字段说明,最好是以文档形式,如ApplicationResponse{"int|flag|执行结果标识1成功0失败", "Order|order|订单内容,flag为1的话total为计算结果", "String|message|服务端返回的其他信息,如“参数某某字段不能为空”或“计算成功|失败”之类的"}
实际中的话,要看服务端和客户端谁更强势了,也许如上面描述的那样服务端把所有材料都为客户端准备好,也很有可能是服务端只扔给客户端一个url说你调用吧,然后客户端求爷爷告奶奶的一点一点从服务端那问出来各个对象及其字段的属性、要求
接下来打开IDE,新建一个cxf_client的工程
本来美女同学做的也是一个JavaWeb的工程,大家应该能大致想想出来这个工程的结构吧,servlet或struts或SpringMVC什么的,Spring IOC,service层DAO层,然后持久层+MySQL什么的,然后主要的一点是要在某个地方开个口子,把帅哥发布的webservice服务弄进去。一般我们会把这个口子开在service层。
到这里我们先停一下建客户端的进度,总结一下,我们已经知道了服务端如何发布webservice服务,也知道了客户端在哪里调用服务,但是还不清楚如何调用。为了能清楚的说明客户端的完整结构,我想在这里插入客户端调用webservice服务的方法。
首先,需要从http://www.apache.org/dyn/closer.cgi?path=/cxf/2.5.2/apache-cxf-2.5.2.zip下载cxf框架并解压到某目录,如E:\JavaEE\apache-cxf-2.5.2
然后,我们需要知道我们要调用的服务的url,如http://127.0.0.1:8080/cxf/foo/bar/OrderProcess?wsdl
然后打开cmd,输入如下命令并回车
关于这个命令
1、确保你的jdk正确安装并配置好了环境变量
2、灰色部分是cxf的lib所在目录
3、棕色?部分是将上面我们在浏览器里看到的wsdl文档生成为Java文件的类的包(package)结构
4、绿色部分是3中生成的Java文件连同其包结构的东西的存放地
5、土红色部分是我们要调用的webservice服务的url
一切顺利的话,在E:\JavaEE\interface目录下会生成一个包结构,最里边是一堆Java文件,包括Order、ApplicationResponse等实体类,和OrderService等接口(interface,而不是OrderServiceImpl),还有如ObjectFactory、package-info等奇怪的类(与提供SOAP功能有关(这个是我猜测的。。。)),借助这些类和cxf框架的支持,客户端就可以通过SOAP和http去调用服务端的服务了
据说有用IDE如Eclipse等方便的根据wsdl生成Java的方式,不过暂时我还没时间去研究~
现在,让我们把服务端所需要的那些jar包全部放到想象出来的cxf_client工程的lib里,然后把刚才通过命令生成的Java文件连同其包结构拷贝到cxf_client的src目录下,然后打开src目录下Spring的配置文件,添加如下内容
<bean id="client" class="bar.cxf.client.wsdl.OrderService" factory-bean="clientFactory" factory-method="create"/> <bean id="clientFactory" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean"> <property name="serviceClass" value="bar.cxf.client.wsdl.OrderService" /> <property name="address" value="http://localhost:8080/cxf/foo/bar/OrderProcess" /> </bean>
"clientFactory"定义了一个客户端工程,"serviceClass"属性指定了要进行生产的接口,即我们刚通过命令生成的那个接口,"address"指向的是服务端发布的服务,注意这个值是没有"?wsdl"后缀的,为什么这样,我也说不清。。。
然后"client"定义了一个具体的需要Spring帮助实例化的bean对象,类型是"bar.cxf.client.wsdl.OrderService",即"clientFactory"中指定的类型,然后"factory-bean"指向了"clientFactory",method么,就是"create"了,这个我也不知道细节,这么写就这么用吧。。。
至此,客户端关于webservice的配置就结束了,这时候启动应用,Spring和cxf就会为我们实例化来自http://localhost:8080/cxf/foo/bar/OrderProcess的服务为一个id="client"的bean了,剩下的只要会用Spring就都ok了
这里,我就不再建立完整的web格式的client工程了,只在main方法里模拟了一下,工程目录结构如下
(2012/4/12 0:10,难道系统在升级,图上传不上去。。。。)
lib里的jar和cxf_server的一样,就不贴图了
然后在client-beans.xml中加个配置
<bean id="testService" class="bar.cxf.client.TestService"> <property name="client" ref="client" /> </bean>
然后TestService的代码
public class TestService { private OrderService client; public OrderService getClient() { return client; } public void setClient(OrderService client) { this.client = client; } public ApplicationResponse test(Order order){ return client.processOrder(order); } }
然后是TestCase的main方法
public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext( new String[] { "client-beans.xml" }); TestService test = (TestService)context.getBean("testService"); Order order = new Order(); order.setName("Intel i5 2500"); order.setAmount(5); order.setPrice(new BigDecimal(1439.00)); ApplicationResponse response = test.test(order); if(response != null && response.getFlag() == 1){ System.out.println(response.getMessage()); }else if(response != null && response.getFlag() == 0){ System.out.println(response.getMessage()); }else{ System.out.println("计算失败"); } }
至此所有的开发工作就结束了,启动server,然后执行TestCase,正常的话,会在client的console看到
- Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@4e106082: startup date [Thu Apr 12 00:16:56 CST 2012]; root of context hierarchy - Loading XML bean definitions from class path resource [client-beans.xml] - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@4102799c: defining beans [client,clientFactory,testService]; root of factory hierarchy 2012-4-12 0:16:56 org.apache.cxf.service.factory.ReflectionServiceFactoryBean buildServiceFromClass 信息: Creating Service {http://service.cxf.foo/}OrderServiceService from class bar.cxf.client.wsdl.OrderService Your order: Intel i5 2500 * 5 Total money is 7195
前3行是Spring容器的log
4、5行是cxf框架的log
6、7行就是返回的ApplicationResponse中的message信息啦
然后再去看server的console,信息就很多了
2012-4-12 0:16:57 org.apache.cxf.services.OrderServiceImplService.OrderServiceImplPort.OrderService 信息: Inbound Message ---------------------------- ID: 3 Address: http://localhost:8080/cxf/foo/bar/OrderProcess Encoding: UTF-8 Http-Method: POST Content-Type: text/xml; charset=UTF-8 Headers: {Accept=[*/*], cache-control=[no-cache], connection=[keep-alive], Content-Length=[258], content-type=[text/xml; charset=UTF-8], host=[localhost:8080], pragma=[no-cache], SOAPAction=[""], user-agent=[Apache CXF 2.5.2]} Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:processOrder xmlns:ns2="http://service.cxf.foo/"><arg0><amount>5</amount><name>Intel i5 2500</name><price>1439</price></arg0></ns2:processOrder></soap:Body></soap:Envelope> --------------------------------------
首先是"Inbound Message",主要看Payload那一行,格式化一下,会发现它就是我们调用processOrder(Order order)时order的xml版本
然后是后半部log "Outbound Message"
2012-4-12 0:16:57 org.apache.cxf.services.OrderServiceImplService.OrderServiceImplPort.OrderService 信息: Outbound Message --------------------------- ID: 3 Encoding: UTF-8 Content-Type: text/xml Headers: {} Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns1:processOrderResponse xmlns:ns1="http://service.cxf.foo/"><return><flag>1</flag><message>Your order: Intel i5 2500 * 5 Total money is 7195</message><order><amount>5</amount><name>Intel i5 2500</name><price>1439</price><total>7195</total></order></return></ns1:processOrderResponse></soap:Body></soap:Envelope> --------------------------------------
Payload部分,格式化后同样发现它就是返回值ApplicationResponse的xml版本
这两个xml串就是SOAP格式化后的消息,通常我们叫它报文,和别的系统联调,出问题了之后一般都会先看报文内容的。
我把代码放到如下目录了,有需要的可以SVN拉下来看看
https://my-test-attendance.googlecode.com/svn/trunk/cxf_server/
https://my-test-attendance.googlecode.com/svn/trunk/cxf_client/
以上就是暂时想到的实践篇该有的东西了,这两天病倒了,结果项目紧,还要求每天无偿加班到9点,今天熬夜终于是把这篇文章赶出来了,想来疏漏的地方肯定很多了,真心希望不要有误导大家的地方~暂时就先这么发表了吧,希望能对大家有些许帮助~有问题欢迎交流~
最后这么辛苦了,按照惯例,奉上福利一份,注意,NSFW~