WebService开发工具AXIS指南
1 介绍
Axis ( A pache eX tensible I nteractionS ystem )是一款开源的 WebService 运行引擎,它是 SOAP 协议的一个实现,其本身来源于 Apache 的另一个项目 Apache SOAP 。 Axis 分为 1.x 系列和 Axis 2 系列,两个系列体系结构和使用上有较大的区别,相对而言, Axis1.x 更加稳定,文档也比较齐全,因此本文内容以 Axis 1.x 系列最新版本 1.4 为基础, Axis 有 C++ 和 Java 两个版本的实现,本文描述 Java 版的 Axis 。
采用 Axis 实现 WebService 时,只需要 POJO 即可,不要求实现特定的接口或继承特定的父类,不必为提高效率而购买专门的开发工具,在 Eclipse 中开发不需要添加任何插件。
Axis 对运行环境的要求是只要支持 Servlet2.3 或以上即可,目前主要的应用服务器都可运行 Axis ,如 Tomcat 、 JBoss 、 Websphere 、 WebLogic 、 Jetty 。并且 Axis 可以很容易和应用程序整合在一个 Web 应用中,将应用程序的功能展示为 WebService 。
此外, Axis 的另一个好处就是不用和某个应用服务器绑定在一起,具有很好的可移植性,除配置文件可能要作小的修改外,服务的实现代码不需要做任何修改。
Axis 官方网站为 http://ws.apache.org/axis
本文例子代码运行环境要求如下:
Windows XP SP2 中文版, JDK1.4 或以上, Apache Axis1.4 , Tomcat 4.1 或其他任何支持 Servlet2.3 的应用服务器,本文以 Tomcat 4.1 为例, Java 开发工具为 Eclipse3.1 。
如果还没有安装 JDK , 从 http://java.sun.com/javase下载并安装到本地硬盘,以下用 JAVA_HOME 表示 JDK 安装在本地的路径。
从 http://ws.apache.org/axis 下载 Axis1.4 ,安装包是一个压缩文件,不用安装程序,直接解压到本地硬盘即可。以下用 AXIS_HOME 表示 axis 解压到本地的路径。
从 http://tomcat.apache.org 下载 Tomcat 4.1 安装包,按 Tomcat 安装指南安装到本地,以下用 TOMCAT_HOME 表示 Tomcat 安装在本地的路径。安装完成后验证安装正确,缺省情况下输入 http://localhost:8080 可进入 Tomcat 页面。
下面在 Tomcat 中增加一个 Web 应用,并为其增对 Axis 支持:
在 TOMCAT_HOME/webapps 下创建一个新目录 myservices ,然后将 AXIS_HOME/webapps/axis 目录下的文件和子目录都复制到 myservices 目录下。并将 AXIS_HOME/lib/axis-ant.jar 也复制到 myservices/WEB-INF/lib 目录下,该 jar 文件提供对 ant 脚本的支持。
启动 Tomcat ,然后在浏览器中输入 http://localhost:8080/myservices,如果能显示下面的页面,则表示 axis 已成功安装到 myservices 应用中,下面创建的 WebService 都在该应用中运行。
进一步点击 Validation 可以显示运行环境信息,点击 List 可以列出 Axis 自带的两个 WebService 。 以后增加的 WebService 也显示在这里。
以下代码在 Eclipse 中开发,为此在 Eclipse 创建一个 java 项目,名称为 axis-guide ,并将 Axis 的包加到项目的 java build path 中,如下图所示: Axis 这些包位于 Web 引用 myservices 的 WEB-INF/lib 目录下。
准备好运行环境后,下面就开始开发 WebService ,该 WebService 名为 EchoService ,有一个方法 echo ,获得一个输入信息后,加入附加信息返回给调用者。原代码如下:
EchoService.java
package chen.axisguide.ch3; public class EchoService { public String echo(String msg) { return "your input message is " + msg; } }
|
可以看到该类是一个 POJO ,不需要其他任何附加说明来表明其是一个 WebService 实现。
为了让 Axis 能识别服务,需要为服务提供一个描述文件,通常文件名为 deploy.wsdd ,因为该文件用于将服务部署到 Axis 引擎所在的 Web 应用中。 deploy.wsdd 文件格式为 xml 格式, EchoService 的 deploy.wsdd 内容如下:
<deployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java"> <service name="EchoService" provider="java:RPC" > <parameter name="className" value="chen.axisguide.ch3.EchoService" /> <parameter name="allowedMethods" value="*" /> </service> </deployment>
|
service 节点的 name 属性定义服务的名字,在客户端访问时会用到该名字, provider 中定义 WebService 的提供方式为 RPC 。
第一个 parameter 用来描述服务实现类的类名,包括包名。第二个 parameter 参数描述类中哪些方法可以暴露为 WebService 方法,这里用 * 表示全部方法。
如果要卸载一个已部署的 WebService ,需要提供一个卸载描述文件,用 Axis 提供的工具读取该文件来卸载已部署的 WebService 。 上述 deploy.wsdd 对应的卸载文件为 undeploy.wsdd ,内容如下:
<undeployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java"> <service name="EchoService"/> </undeployment>
|
deploy.wsdd 和 undeploy.wsdd 和原代码 EchoService.java 位于同一个目录中。
在项目 axis-guide 的根目录下创建一个 Ant 的 build.xml 来执行编译和部署任务。目录结构如下图所示:
build.xml 内容如下:
<?xml version="1.0" encoding="utf-8"?> <project name="myservice" default="deployws"> <!-- If your Tomcat installation folder is another , replace below with yourself --> <property name="web.home" location="C:/Tomcat4.1/webapps/myservices" /> <property name="webclass.dir" location="${web.home}/WEB-INF/classes" /> <property name="weblib.dir" location="${web.home}/WEB-INF/lib" /> <property name="src.dir" location="src" />
<path id="lib.class.path"> <fileset dir="${weblib.dir}"> <include name="**/*.jar"/> </fileset> </path> <taskdef name="axis-admin" classname="org.apache.axis.tools.ant.axis.AdminClientTask"> <classpath refid="lib.class.path"/> </taskdef> <target name="compile"> <javac srcdir="${src.dir}" destdir="${webclass.dir}" source="1.4" compiler="javac1.4" debug="on"> <classpath> <path refid="lib.class.path"/> </classpath> <exclude name="**/*Test.java" /> </javac> </target> <target name="deployws" depends="compile"> <axis-admin url="http://localhost:8080/myservices/servlet/AxisServlet" xmlfile="${src.dir}/chen/axisguide/ch3/deploy.wsdd"/> </target> <target name="undeployws"> <axis-admin url="http://localhost:8080/myservices/servlet/AxisServlet" xmlfile="${src.dir}/chen/axisguide/ch3/undeploy.wsdd"/> </target> </project>
|
task-def 定义了一个名为 axis-admin 的任务,该任务负责根据 deploy.wsdd 或 undeploy.wsdd 来部署或卸载 WebService 。此任务的实现类 org.apache.axis.tools.ant.axis.AdminClientTask 位于 axis-ant.jar 文件中。
服务实现类原代码编译后的 .class 文件放在 myservices/WEB-INF/classes 目录下,在 build.xml 的 compile 任务中实现。
在 Eclipse 环境中用 Ant 运行该 build.xml ,执行成功后在浏览器中输入 http://localhost:8080/myservices/servlet/AxisServlet ,可以看到如下的页面,表示 EchoService 已部署成功。
WebService 部署成功后,下面开发客户端去调用 WebService 的方法,打印返回结果。
Client.java
package chen.axisguide.ch3; import java.rmi.RemoteException; import javax.xml.rpc.ParameterMode; import javax.xml.rpc.ServiceException; import org.apache.axis.client.Call; import org.apache.axis.client.Service; import org.apache.axis.encoding.XMLType; /** * 访问 EchoService 的客户端 * * */ public class Client { /** * @param args * @throws ServiceException * @throws RemoteException */ public static void main(String[] args) throws ServiceException, RemoteException { // wsdl 中 address 节点的 location 属性 String endpoint = "http://localhost:8080/myservices/services/EchoService"; // 要调用的方法名 String method = "echo"; Service service = new Service(); Call call = (Call) service.createCall(); // 设置客户端访问的远程端点 call.setTargetEndpointAddress(endpoint); // 设置输入参数类型 call.addParameter("p1",XMLType.XSD_STRING,ParameterMode.IN); // 设置返回值类型 call.setReturnType(XMLType.XSD_STRING); // 输入参数值 String msg = "call from java"; // 调用远程方法 String result = (String)call.invoke(method,new Object[]{msg}); System.out.println(result); } }
|
确认 Tomcat 已启动,在 Eclipse 环境中运行 Client ,如果没有错误,在控制台将会打印从 EchoService 的 echo 方法获得的结果。
客户端访问 WebService 的主要步骤包括:
l 创建 Call 对象,设置 WebService 的访问端点;
l 注册序列化和反序列对象,详见下节;
l 设置访问方法的参数类型以及返回值类型;
l 初始化参数值;
l 调用 WebService 的方法,获得返回结果。
JAX-RPC 规范中定义了三种访问服务端的方法:
1. 在客户端生成远程服务端的静态代理文件,编译后提供给客户端访问服务使用, Axis 提供了工具可根据服务的 wsdl 生成静态代理文件。但这种方法会在客户端增加若干附加文件,服务端接口发生变化时需要重新在客户端生成文件。
2. 根据服务端接口文件访问服务。这种方法要求服务实现一个接口,并且该接口要从 java.rmi.Remote 扩展,客户端通过获得该接口的代理来访问服务。这种方法需要在客户端保存一个服务的接口文件。
3. 动态调用接口。客户端只须通过服务端口、访问方法名和参数类型即可访问服务的方法。访问方式类似于用反射来调用类方法。这种方法的客户端和服务端的耦合度比前两种要低,上面 Cilent.java 用的就是这种方法。 后面的例子也都采用这种方式访问服务端。
Axis 中客户端和服务器端之间的消息使用 SOAP 封装, java 程序中的数据需要序列化后保存在 XML 文档中通过网络传输到接收方,接收方从 XML 文档中反序列化为 java 虚拟机可识别的数据对象。对 java 基本类型, Axis 提供了默认的序列化和反序列化功能,不需要特别申明即可使用。对自定义类型,如果是 JavaBean ,则可用 beanMapping 在 wsdd 文件中申明该类型所需要的序列化和反序列化器即可。 Axis 已自带 JavaBean 的序列化和反序列化功能。 如果 Axis 自带的序列化和反序列化器不能对自定义对象进行序列化处理,你可以自定义序列化和反序列化器,然后用 typeMappping 在 wsdd 文件中申明即可。
在 Axis 中, xml 数据类型和 java 类型之间的映射关系如下:
XML 类型 |
java 类型 |
xsd:base64Binary |
byte[] |
xsd:boolean |
boolean |
xsd:byte |
byte |
xsd:date |
java.util.Date |
xsd:dateTime |
java.util.Calendar |
xsd:decimal |
java.math.BigDecimal |
xsd:double |
double |
xsd:float |
float |
xsd:hexBinary |
byte[] |
xsd:int |
int |
xsd:integer |
java.math.BigInteger |
xsd:long |
long |
xsd:QName |
javax.xml.namespace.QName |
xsd:short |
short |
xsd:string |
java.lang.String |
下面的例子中服务器端向客户端返回一个列表,列表中包含自定义的 Book 对象。
服务实现代码如下:因为只是说明对象的序列化方式,因此就直接创建对象放入列表对象中,没有从数据库中搜索数据。
LibraryService.java :对象序列化服务端代码
package chen.axisguide.ch4; import java.util.ArrayList; import java.util.List; public class LibraryService { // 返回一个包含 Book 对象的列表 public List query(String keyword) {
Book book1 = new Book(); book1.setAuthor("Jack Johnson"); book1.setContent("about java development."); book1.setIsbn("123-456-789"); book1.setName("Java deplopment handbook"); Book book2 = new Book(); book2.setAuthor("Kim Kent"); book2.setContent("about ruby development."); book2.setIsbn("123-654-897"); book2.setName("ruby deplopment handbook"); List res = new ArrayList(); res.add(book1); res.add(book2); return res; } }
|
服务端代码中不需要对序列化做特殊处理。
Book 的定义如下:
Book.java
package chen.axisguide.ch4;
public class Book { private String isbn; private String name; private String author; private String content;
public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getIsbn() { return isbn; } public void setIsbn(String isbn) { this.isbn = isbn; } public String getName() { return name; } public void setName(String name) { this.name = name; }
}
|
序列化的处理在服务部署文件中定义:
deploy.wsdd : LibraryService 描述文件
<deployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
<service name="LibraryService" provider="java:RPC" > <parameter name="className" value="chen.axisguide.ch4.LibraryService" /> <parameter name="allowedMethods" value="*" />
<beanMapping qname="myNS:Book" xmlns:myNS="LibraryService" languageSpecificType="java:chen.axisguide.ch4.Book"/> </service>
</deployment>
|
beanMapping 节点定义了 Book 对象的序列化和反序列化使用 Axis 自带的针对 JavaBean 的序列化和反序列化器来实现。
其中, xmlns 定义命名空间为 LibraryService ,和服务同名, qname 定义为命名空间 myNS 中的 Book ,与被序列化的类名相同,这种命名方式可使处理的对象一目了然。当然也可命名为其他名字。
下面看一下客户端代码中对序列化的处理。
Cilent.java :访问 LibraryService 客户端代码。
package chen.axisguide.ch4; import java.rmi.RemoteException; import javax.xml.namespace.QName; import javax.xml.rpc.ParameterMode; import javax.xml.rpc.ServiceException; import org.apache.axis.client.Call; import org.apache.axis.client.Service; import org.apache.axis.encoding.XMLType; /** * 访问 LibraryService 的客户端 * * */ public class Client { /** * @param args * @throws ServiceException * @throws RemoteException */ public static void main(String[] args) throws ServiceException, RemoteException { // wsdl 中 address 节点的 location 属性 String endpoint = "http://localhost:8080/myservices/services/LibraryService"; // 要调用的方法名 String method = "query"; Service service = new Service(); Call call = (Call) service.createCall(); // 设置客户端访问的远程端点 call.setTargetEndpointAddress(endpoint); QName qn = new QName("LibraryService","Book"); call.registerTypeMapping(Book.class, qn, new org.apache.axis.encoding.ser.BeanSerializerFactory(Book.class, qn), new org.apache.axis.encoding.ser.BeanDeserializerFactory(Book.class, qn));
// 设置输入参数类型 call.addParameter("p1",XMLType.XSD_STRING,ParameterMode.IN); // 设置返回值类型 call.setReturnType(XMLType.XSD_ANYTYPE); // 输入参数值 String msg = "key word"; // 调用远程方法 Object[] result = (Object[])call.invoke(method,new Object[]{msg}); for(int i = 0; i < result.length; i++) { Book bk = (Book)result[i]; System.out.println("ISBN: " + bk.getIsbn() + " Book Name: " + bk.getName()); } } }
|
在客户端,首先为 Book 定义一个 QName 对象,构造函数中的第一个参数值要和 wsdd 中 beanMapping 节点的 xmlns:myNS 属性值相同,第二个参数值要和 beanMapping 的 qname 属性值相同。然后在 Call 对象中注册自定义类型所用的序列化和反序列化器。通过以下语句来实现:
call.registerTypeMapping(Book.class, qn, new org.apache.axis.encoding.ser.BeanSerializerFactory(Book.class, qn), new org.apache.axis.encoding.ser.BeanDeserializerFactory(Book.class, qn)); |
最后在 build.xml 中增加对 deploy.wsdd 文件的部署。内容如下:
<target name="deployws" depends="compile"> <axis-admin url="http://localhost:8080/myservices/servlet/AxisServlet" xmlfile="${src.dir}/chen/axisguide/ch4/deploy.wsdd"/> </target>
|
Axis 引擎在处理消息过程中,可以自定义处理器对请求或响应的消息进行拦截处理,处理器的动作对服务的实现部分透明。这种机制可以将日志、安全检查、加密解密等与业务无关的操作抽取出来单独处理,降低模块间的依赖关系。
自定义处理器时从 org.apache.axis.handlers.BasicHandler 继承,在方法 invoke(MessageContext msgContext) 进行拦截处理。消息等运行环境信息可以从 msgContext 得到。
自定义处理器要发挥作用,需要在 wsdd 文件中进行说明,方法如下:
1- 用 <handler name=”handlername” type=”java:classname”> 定义处理器,如果要给处理器传递参数,可在 <handler> 下用 <parameter name=”name” value=”value” /> 进行说明。 之后在自定义处理器类代码中用 getOption(“name”) 可得到 value 的值。
2- 在 <service> 中用 <requestFlow> 或 <responseFlow> 定义在请求消息流或返回消息流中要调用的处理器。如果有多个处理器,则按其在 <requestFlow> 或 <responseFlow> 中排列顺序依次调用。格式如下:
<service>
<requestFlow>
<handler type=”handlername” />
</requestFlow>
</service>
下面的例子中服务器端和客户端代码使用第 3 节的代码,在请求消息流中增加两个处理器,第一个处理器在控制台打印出 SOAP 包的 body 内容,然后在 MessageContext 中设置一个属性值;第二个处理器从 MessageContext 中取出第一个处理器设置的属性值,在控制台打印出来。
第一个处理器原代码如下:
FirstHandler.java
package chen.axisguide.ch5; import javax.xml.soap.SOAPException; import org.apache.axis.AxisFault; import org.apache.axis.Message; import org.apache.axis.MessageContext; import org.apache.axis.handlers.BasicHandler; import org.apache.axis.message.SOAPBody; public class FirstHandler extends BasicHandler { public void invoke(MessageContext msgContext) throws AxisFault { Message reqMsg = msgContext.getRequestMessage(); try { SOAPBody reqBody = (SOAPBody) reqMsg.getSOAPBody(); System.out.println("==================================="); System.out.println("request SOAP body " + reqBody.getAsString()); System.out.println("==================================="); // 设置属性传递到下一个 handler msgContext.setProperty("handler1","handler1 processed"); } catch (SOAPException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } }
|
第二个处理器原代码:
SecondHandler.java
package chen.axisguide.ch5; import org.apache.axis.AxisFault; import org.apache.axis.MessageContext; import org.apache.axis.handlers.BasicHandler; public class SecondHandler extends BasicHandler { public void invoke(MessageContext msgContext) throws AxisFault { System.out.println("Get data from previous handler : " + msgContext.getProperty("handler1")); } }
|
wsdd 文件内容如下:
deploy.wsdd
<deployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java"> <handler name="handler1" type="java:chen.axisguide.ch5.FirstHandler" /> <handler name="handler2" type="java:chen.axisguide.ch5.SecondHandler" /> <service name="EchoService" provider="java:RPC" > <parameter name="className" value="chen.axisguide.ch5.EchoService" /> <parameter name="allowedMethods" value="*" /> <requestFlow> <handler type="handler1" /> <handler type="handler2" /> </requestFlow> </service> </deployment>
|
在 build.xml 中增加对 deploy.wsdd 文件的部署处理。内容如下:
<target name="deployws" depends="compile"> <axis-admin url="http://localhost:8080/myservices/servlet/AxisServlet" xmlfile="${src.dir}/chen/axisguide/ch5/deploy.wsdd"/> </target>
|
服务端和客户端原代码和第 3 节相同,不再列出。
在 Eclipse 中用 Ant 运行 build.xml ,在 Tomcat 中重新载入 Web 应用,然后在 Eclipse 中运行 Client.java ,在 Tomcat 的控制台将会打印出 SOAP body 信息以及 handler1 属性值。
开发好 WebService 并向外暴露服务后,由于 WebService 访问协议都是公开的,客户端只要知道服务的 wsdl ,就可以访问 WebService 中的方法。为防止非法的访问,可以要求客户端访问 WebService 时提供用户名和口令,只有在服务端通过身份认证后才可继续访问方法,否则请求被拒绝。
下面看一下 Axis 中如何使用用户名、口令来控制对 WebService 的访问。
在 myservices/WEB-INF 下创建一个名为 users.lst 的文本文件,一行表示一对用户名、口令,之间用空格分隔,内容如下:
user1 password1 user2 password2 |
实现一个 WebService ,它有一个 foo() 方法,该方法打印出通过身份认证的用户的用户名,代码如下:
SecurityService.java
package chen.axisguide.ch6; import org.apache.axis.MessageContext; public class SecurityService { public void foo() { MessageContext msgContext = MessageContext.getCurrentContext(); System.out.println(" 你已通过身份认证 !"); System.out.println(" 你的用户名 : " + msgContext.getUsername()); } }
|
为了让 Axis 知晓要对该服务的访问验证用户名和口令,需要在 wsdd 文件中增加一个 Axis 自带的处理器 SimpleAuthenticationHandler ,该处理器会从 MessageContext 中取出用户名和口令,并与 users.lst 中的用户名和口令进行比对,如果匹配,则通过验证,继续后面的处理,否则抛出 (401)Unauthorized 异常。
deploy.wsdd
<deployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java"> <service name="SecurityService" provider="java:RPC" > <parameter name="className" value="chen.axisguide.ch6.SecurityService" /> <parameter name="allowedMethods" value="*" /> <requestFlow> <handler type="java:org.apache.axis.handlers.SimpleAuthenticationHandler" /> </requestFlow> </service> </deployment>
|
客户端在访问 WebService 时,需要在 Call 对象中设置在 users.lst 文件中已有的用户名和对应的口令。客户端代码如下:
package chen.axisguide.ch6; import java.rmi.RemoteException; import javax.xml.rpc.ServiceException; import org.apache.axis.AxisFault; import org.apache.axis.client.Call; import org.apache.axis.client.Service; /** * 访问 SecurityService 的客户端 * */ public class Client { /** * @param args * @throws ServiceException * @throws RemoteException */ public static void main(String[] args) throws ServiceException, RemoteException { // wsdl 中 address 节点的 location 属性 String endpoint = "http://localhost:8080/myservices/services/SecurityService"; // 要调用的方法名 String method = "foo"; Service service = new Service(); Call call = (Call) service.createCall(); // 设置客户端访问的远程端点 call.setTargetEndpointAddress(endpoint); // 设置用户名和口令 call.setUsername("user1"); call.setPassword("password1"); // 调用远程方法 try{ call.invoke(method,new Object[]{}); }catch(AxisFault e) { System.err.println(e.getFaultString()); } } } |
Call 对象用 setUserName() 和 setPassword() 来设置用户名和口令。
Axis 自带的身份认证功能比较简单,如果其安全性不能满足应用系统的要求,可以自定义一个处理器替代 SimpleAuthenticationHandler ,在自定义处理器中采用其他的安全框架(如 Acegi http://www.acegisecurity.org/ )来保护你的 WebService 。
从上面的实例可以看出, Axis 在开发 WebService 方面具有如下优势:
l 轻量级: Axis 本身只需要 Servlet 容器就可运行,因此能够在大多数轻量级的 Web 应用服务器上运行;另外在 Axis 中服务的实现类只要求是一个 POJO ,没有强制要求实现特定接口或从特定父类继承,这使得开发和测试非常容易,不需要为提高开发效率而购置重量级的 IDE 。
l 扩展性好:在 Axis 中,可以通过加入自定义处理器( Handler )方式来扩展 Axis 的功能,并且这种功能的增加对业务部分可以做到透明,即业务代码中不需要为使用增加的功能而修改代码。
l 可移植性强: Axis 可以在支持 Servlet 的容器中运行,运行于其中的 WebService 也就可以在现有的大多数的 Web 应用服务器上运行,不需要对服务的实现代码做任何的改动。