Axis2 1.7.8 WebService开发:从部署到动态调用全攻略
简介Apache Axis2 1.7.8 压缩包是一份可直接落地的 Axis2 客户端环境面向需要调用 Web Service、根据 WSDL 生成客户端代码的 Java 工程师。包内集中放置了核心运行库、依赖 JAR 包与实用命令行脚本wsdl2java、java2wsdl、axis2server 均在 bin 目录下提供可支撑从服务描述解析到客户端桩代码生成的常见开发流程同时保留 AAR 服务包与 Web 管理相关类便于进一步研究 Axis2 服务发布机制也能在一定程度上反推客户端与服务端的交互细节。整个资源包含 512 个文件大小约 21.98MB类型以 Java 源码、JAR 依赖、WSDL 描述、XML 配置为主另有 JSP、properties、XSD、bat 脚本等辅助文件目录结构基本对应官方发行版布局检索和定位比较直观。已有 608 人学习下载适合准备搭建 Axis2 开发环境、使用 wsdl2java 生成客户端代码或排查 Axis2 客户端配置问题的开发者包内 bin 下的脚本可直接调用降低了从 WSDL 到客户端代码的生成门槛。包内附带的 readme、license、notice 等文档还可帮助确认版本和许可范围。1. 拿 Axis2 1.7.8 做 WebService先搞懂这个老牌框架还值不值得用很多做 Java 服务端的老开发者都有过这种经历接手一个老系统对方丢过来一个 WSDL 地址说要对接他们的 WebService 接口。新项目用 Spring Boot 写 RestController 很顺手但碰到 .asmx 或者 .wsdl 结尾的老接口第一个想到的还是 Axis2。axis2-1.7.8 是 Apache Axis2 比较后期的一个稳定版本1.7.x 这个分支修复了之前 1.6.x 时期一堆 SSL、多线程和命名空间处理的问题往后没再有大版本更新所以很多遗留项目都固定在这个版本上。这篇笔记会把 axis2-1.7.8.zip 从下载到落地的完整链路拆开讲清楚解压后哪些 jar 能删、哪些必须留着服务端怎么发布一个最小可用的 WebService客户端又怎么用 WSDL2Java 生成代码快速调用。也会把几个高频坑——比如缺少 XmlSchema 依赖导致启动报错、RPC 风格调用时参数顺序错乱、以及 AAR 热部署不生效——逐个拆开。如果你正准备对接老系统或者需要把一个已经跑了好几年的 Axis2 服务迁移到新环境这篇文章应该能帮你省下至少一个下午的排查时间。2. 环境和依赖1.7.8 的目录结构、精简 jar 包与第一个 Hello 服务先说结论Axis2 不需要装独立服务器它既可以以 Web 应用的形式部署到 Tomcat也可以作为独立服务跑起来。实际项目里绝大多数是前者所以我的做法是依赖 Tomcat让它以 war 包方式承载。2.1 解压后哪些目录是核心哪些可以扔拿到 axis2-1.7.8.zip 之后解压出来会看到 bin、conf、lib、modules、services、webapp 这些目录。很多人第一次看会懵不知道从哪里开始我一般按角色把它们分成三类webapp 目录这是一个完整的 Axis2 Web 应用可以直接把里面的 axis2.war 丢到 Tomcat 的 webapps 下。生产环境我建议这样用管理后台直接通过 /axis2 路径访问部署服务上传 AAR 也方便。lib 目录里面是所有运行时依赖的 jar。注意这个目录的 jar 不是全部都要搬到 Tomcat 里需要做一次精简。services、modules、conf 目录配置和热部署入口。services 用于放服务归档文件modules 是扩展模块conf 里是 axis2.xml 全局配置。有一点值得留意1.7.8 的 war 包本身已经内置了大部分依赖如果直接用 axis2.war 部署lib 目录基本不用动。只有当你想把 Axis2 嵌入到自己的 Web 项目里时才需要从 lib 目录挑选 jar 复制过去。2.2 嵌入现有项目的依赖清单与最小配置我一般会在自己的 Web 项目里集成 Axis2这样服务端和业务代码在同一个应用里不用单独维护一个 Axis2 容器。这里给出一份最小依赖清单基于 1.7.8 版本实测可行dependency groupIdorg.apache.axis2/groupId artifactIdaxis2-kernel/artifactId version1.7.8/version /dependency dependency groupIdorg.apache.axis2/groupId artifactIdaxis2-adb/artifactId version1.7.8/version /dependency dependency groupIdorg.apache.axis2/groupId artifactIdaxis2-transport-http/artifactId version1.7.8/version /dependency dependency groupIdorg.apache.axis2/groupId artifactIdaxis2-transport-local/artifactId version1.7.8/version /dependency这里 axis2-kernel 是核心包括 SOAP 处理链和 AxisEngineaxis2-adb 是数据绑定框架用来把 XML 和 Java 对象互转transport-http 提供 HTTP 传输层transport-local 用于本地或单元测试环境的消息传输。这四者缺一不可少了任意一个启动时都会报 ClassNotFoundException后面避坑章节会具体讲。如果项目里用了 Spring不需要额外加专门的 axis2-spring 集成包直接定义一个 Servlet 把 AxisServlet 挂进去就行。常见做法是在 web.xml 里配置如下内容servlet servlet-nameAxisServlet/servlet-name servlet-classorg.apache.axis2.transport.http.AxisServlet/servlet-class load-on-startup1/load-on-startup /servlet servlet-mapping servlet-nameAxisServlet/servlet-name url-pattern/services/*/url-pattern /servlet-mapping逻辑说明这里把 AxisServlet 映射到 /services/* 前缀所有以该路径开头的请求都会交给 Axis2 处理。load-on-startup 设为 1 是为了让 Axis2 在容器启动阶段就初始化避免第一次访问时才加载导致超时。参数说明url-pattern 的路径前缀决定了服务发布后的访问地址比如 class 里 WebService 注解声明的服务名是 HelloService那么访问地址就是 http://ip:port/项目名/services/HelloService。这个前缀不要和项目里已有的 REST 接口路径重合否则会互相拦截。2.3 在 Tomcat 下启动并验证服务列表依赖配好后启动 Tomcat访问管理页面验证是否正常# 启动 Tomcat以 8.5/9.x 为例 bin/startup.sh # 查看启动日志确认 AxisServlet 被加载 tail -f logs/catalina.out # 服务列表验证 curl http://localhost:8080/axis2/services/listServices如果返回的 HTML 里列出了服务名说明 Axis2 核心已经跑起来了。这里要提醒的是 1.7.8 对 JDK 版本比较挑实测 JDK 8 和 JDK 11 都能正常运行但 JDK 17 上跑会遇到模块访问限制问题如果必须用高版本 JDK需要在启动参数里加 --add-opens java.base/java.langALL-UNNAMED 这类规则。后面避坑章节会把这个展开讲。3. 服务端发布与客户端调用RPC 风格和 Document 风格都跑通Axis2 支持两种服务编程模型一种是直接把 Java 类暴露成 WebServicePOJO 方式另一种是基于 WSDL 生成服务端骨架。实际项目里用前者居多因为快。客户端则反过来一般先用 WSDL2Java 工具根据 WSDL 生成 stub再调用。这个章节我会把服务端和客户端的关键路径都走一遍。3.1 服务端用注解发布一个 POJO 服务在 1.7.8 里最省事的发布方式是通过 WebService 注解标注一个普通 Java 类。下面这段代码演示了一个最简单的服务package com.example.ws; import javax.jws.WebMethod; import javax.jws.WebService; WebService(targetNamespace http://ws.example.com/sample, serviceName OrderService) public class OrderService { WebMethod public String createOrder(String orderId, int quantity) { return Order orderId created, quantity quantity; } WebMethod public String queryOrder(String orderId) { return Order orderId status: SHIPPED; } }逻辑说明WebService 注解告诉 Axis2 这个类要发布成 WebServicetargetNamespace 是命名空间一般和包名反写保持一致性客户端生成代码时会用到这个值serviceName 决定服务对外暴露的名字。WebMethod 注解把方法标记为对外可调用的操作不加这个注解的方法即使 public 也不会暴露。参数说明createOrder 方法里 int 类型的 quantity 在 SOAP 消息里会被映射为 xsd:int。如果调用方传来的参数超过 int 范围客户端会抛 UnmarshalException所以跨语言接口建议入参尽量用 String把校验逻辑放在服务端内部做这样能兼容各种调用端的类型映射差异。3.2 把服务打包成 AAR 并完成热部署POJO 类写好后有几种方式发布到 Axis2。开发阶段最常用的是直接把编译后的 class 文件复制到 Tomcat 下 axis2.war 解压目录的 WEB-INF/classes 里重启即可。但这种方式不适合交付交付时一般打包成 AAR 文件放到 services 目录下动态加载。AAR 本质上是个 ZIP 包结构必须是 META-INF/services.xml 加上 class 文件和依赖 jar。services.xml 是一个关键文件内容如下service nameOrderService scopeapplication descriptionOrder WebService/description messageReceivers messageReceiver mephttp://www.w3.org/ns/wsdl/in-out classorg.apache.axis2.rpc.receivers.RPCMessageReceiver/ /messageReceivers parameter nameServiceClass com.example.ws.OrderService /parameter /service逻辑说明messageReceiver 配置决定了请求进来后由哪个接收器处理。RPC 风格对应 RPCMessageReceiver会把 SOAP 体里的参数按顺序映射到 Java 方法入参。ServiceClass 参数指定真正的实现类。scope 设置为 application表示所有请求共享单例如果服务里有实例变量且不是线程安全的建议改成 request让每次请求单独创建实例。打包命令直接用 zip 工具即可# 把编译后的 class 按包结构放入 classes 目录 cd /tmp/order-service mkdir -p META-INF mkdir -p classes/com/example/ws cp OrderService.class classes/com/example/ws/ # 编写 META-INF/services.xml 后执行打包 zip -r OrderService.aar META-INF classes # 复制到 Axis2 的 services 目录 cp OrderService.aar /path/to/axis2/WEB-INF/services/这里需要注意AAR 里的 classes 不能打成 jar 再放进 AAR必须保持目录形式否则 Axis2 的 classloader 不会加载。3.3 客户端WSDL2Java 生成代码并调用服务端发布后访问 http://localhost:8080/项目名/services/OrderService?wsdl 可以看到 WSDL 文档。接下来在客户端项目里用 WSDL2Java 生成调用代码。Axis2 1.7.8 的发行包 bin 目录下自带这个工具。# 进入 Axis2 发行包 bin 目录 ./wsdl2java.sh -uri http://localhost:8080/axis2/services/OrderService?wsdl \ -o /tmp/client-src \ -p com.example.ws.client \ -d adb \ -s参数说明-uri 指定 WSDL 地址-o 指定输出目录-p 指定生成代码的包名-d adb 表示使用 ADB 数据绑定框架这是 Axis2 默认且推荐的方式-s 表示生成同步调用代码。如果接口里定义了复杂类型还可以加 -uw 让生成代码时忽略未使用的类型减少代码量。生成代码后用常规 Java 调法就可以package com.example.demo; import com.example.ws.client.OrderServiceStub; public class ClientDemo { public static void main(String[] args) throws Exception { OrderServiceStub stub new OrderServiceStub( http://localhost:8080/axis2/services/OrderService); OrderServiceStub.CreateOrder req new OrderServiceStub.CreateOrder(); req.setOrderId(A10001); req.setQuantity(5); OrderServiceStub.CreateOrderResponse resp stub.createOrder(req); System.out.println(resp.get_return()); } }逻辑说明生成出的 OrderServiceStub 内部封装了 SOAP 请求构造、序列化和 HTTP 传输逻辑。每个 WSDL 里的 operation 对应两个内部类一个 Request 类型一个 Response 类型。调用时先 new 出 Request 对象填充参数然后调用 stub 里同名方法返回的 Response 对象通过 get 方法获取结果。参数说明构造 stub 时传入的地址必须是完整的服务 URL不能只写 WSDL 地址。如果服务端需要认证在构造 stub 之后、调用之前需要给 stub 设置 HTTP 认证信息做法是调用 stub._getServiceClient().getOptions().setProperty(org.apache.axis2.transport.http.HTTPConstants.AUTHENTICATION, new UsernameToken(user,pass))。3.4 不用生成代码直接在客户端用 ServiceClient 动态调用有时候只是临时调一个接口不想生成一堆 stub 代码这时候可以用 ServiceClient 做动态调用完全绕开 WSDL2Java 步骤import org.apache.axiom.om.OMElement; import org.apache.axiom.om.util.AOMUtils; import org.apache.axis2.addressing.EndpointReference; import org.apache.axis2.client.Options; import org.apache.axis2.client.ServiceClient; ServiceClient client new ServiceClient(); Options opts new Options(); opts.setTo(new EndpointReference( http://localhost:8080/axis2/services/OrderService)); opts.setAction(urn:createOrder); client.setOptions(opts); OMElement payload AOMUtils.stringToOM( ns:createOrder xmlns:ns\http://ws.example.com/sample\ ns:orderIdA10002/ns:orderId ns:quantity3/ns:quantity /ns:createOrder); OMElement response client.sendReceive(payload); System.out.println(response.toString());逻辑说明setTo 指定服务端地址setAction 设置 SOAPAction 头。payload 直接用 OMElement 构造 XML 请求体不再需要生成任何 Java 类型。sendReceive 方法发出同步请求并阻塞等待响应返回的也是一个 OMElement直接 getText 或 toString 查看结果。参数说明setAction 的值必须和服务端 WSDL 里 binding 操作定义的 soapAction 一致不一致时服务端可能报 Action 不匹配。如果接口有命名空间前缀要求XML 字符串里的前缀可以任意取只要 xmlns 声明解析到正确的 namespace URI 即可。4. 避坑排查Axis2 客户端调用里最常见的五个翻车现场Axis2 是个老框架用它的环境往往也老问题集中在依赖冲突、JDK 兼容、命名空间不匹配这几个方向。这里整理的是我在多个项目里实际踩过的坑按现象、原因、解决的顺序写。4.1 启动报错NoClassDefFoundError 关于 XmlSchema现象Tomcat 启动时抛 NoClassDefFoundError指向 org/apache/ws/commons/schema/XmlSchema后续还有一系列 XmlBeans 相关的错误服务根本起不来。原因1.7.8 的 kernel 依赖 XmlSchema 和 XmlBeans 两个库但这两个 jar 没有被打进 axis2.war 的 lib 目录。解决从发行包 lib 目录把 xmlschema-.jar、xmlbeans-.jar、wsdl4j-*.jar 复制到 Tomcat webapps/axis2/WEB-INF/lib重启即可。如果用的是 Maven 依赖方式把 axis2-kernel 的依赖树打出来看看这几个传递依赖是否被 exclusion 排除掉了。# 检查 Maven 传递依赖是否完整 mvn dependency:tree -Dincludesorg.apache.ws.commons:XmlSchema4.2 调用时报错The endpoint reference (EPR) for the Operation not found现象客户端调用时抛 AxisFault提示 EPR not found但服务列表里明明能看到服务。原因publius 的 URL 拼写不对。Axis2 的每个服务有两个地址形态一个是 services/服务名另一个是 services/版本/服务名。如果服务名带了版本后缀而客户端没带就找不到端点。另一个常见原因是服务名大小写写错了。解决在浏览器里打开 http://ip:port/axis2/services/listServices 看列表里准确的 URL复制到客户端代码里。不要手敲别问为什么手敲错的概率极高。4.3 RPC 风格调用时参数全部为 null 或顺序错乱现象服务端方法签名为 createOrder(String orderId, int quantity)客户端传了正确的值但服务端收到后 orderId 为 null或 quantity 接到了 orderId 的内容。原因RPC 风格下 Axis2 按参数顺序把 SOAP 子元素映射到方法入参。如果客户端用 Document 风格构造请求体或者 SOAP body 里子元素顺序和 WSDL 定义不一致就会出现错位。还有一种是客户端和服务端的命名空间不一致导致 Axis2 找不到对应的方法入参直接返回 null。解决能改成 Document 风格尽量改。服务端接口保持 RPC 风格时客户端必须严格使用生成的 stub 或按 WSDL 里 sequence 的顺序手动构造 SOAP body。排查时先打开 ?wsdl 页面检查 message part 的顺序和类型和客户端发送的 XML 做对比。4.4 IP 直连正常域名访问超时现象服务端网络没问题客户端用 IP 访问服务秒通换成域名就卡住直到超时。原因大多数时候是服务端防火墙或安全组没放行该域名的访问或者 DNS 解析慢。但在 Axis2 场景下还有一个特殊原因服务端把自己发布的服务 EndpointReference 写成了内网 IP客户端通过域名访问时服务端返回的 SOAP 响应里携带的地址是内网 IP导致后续的某些回调机制如 WS-Addressing寻址失败。解决在服务端 axis2.xml 里找到 transportSender 配置把 http 传输的 Address 参数改成服务对外暴露的域名或外网 IP。修改后重启服务再通过 ?wsdl 验证地址是否已变更。4.5 JDK 17 及以上跑 Axis2 的服务端报 IllegalAccessError现象JDK 17 下启动服务端第一次发布服务时抛 IllegalAccessError消息里带 java.lang 相关字样把 --illegal-accesspermit 加上也无效。原因JDK 16 开始默认强封装 JDK 内部 APIAxis2 1.7.8 里有些反射调用直接访问了 java.lang 的内部结构没有做模块适配。解决给 JVM 加启动参数 java --add-opens java.base/java.langALL-UNNAMED \ --add-opens java.base/java.lang.reflectALL-UNNAMED \ --add-opens java.base/java.ioALL-UNNAMED \ -jar 应用.jar如果这个应用本身还要被别的框架调用反射那还需要把对应的包也加进去。注意这个参数对 JDK 8 和 11 不需要加了也不影响。4.6 AAR 改完再复制进去服务不生效现象改完 services.xml 或 class 文件后重新打包 AAR 覆盖到 services 目录访问管理页面发现还是旧版本的服务。原因Axis2 对 AAR 的更新检测依赖文件时间戳和 zip 压缩包内部结构变化如果 services.xml 没变只换 class服务 model 不会重载。另外复制过去如果 AAR 文件处于被 Tomcat 占用状态Windows 常见会复制失败但没报错。解决修改 services.xml 里的版本号描述并重新打包或者先删掉旧的 .aar 文件等 3-5 秒再复制新的。最稳妥的办法是直接改名比如 OrderService-2.aar再把旧文件删掉这样 Axis2 会当成新服务加载。5. 进阶用 ServiceClient 绕过 WSDL2Java 做动态调用降低对接成本凡是做过 WebService 对接的都知道生成 stub 代码虽然方便但缺点很清楚每拿一个新 WSDL 就要重新跑一次工具项目里会堆一大片自动生成类而且这些类跟 WSDL 强绑定服务端一旦调整参数结构本地代码就得重新生成再改调法。对于接口比较少、变更频繁的场景我更倾向于用 ServiceClient 做动态调用把请求 XML 作为配置保存下来改动时只改 XML 字符串即可。5.1 把发起动作和响应解析封装成统一的调用工具为了实现动态调用我在实际项目里封装过一个简化版本的工具类核心逻辑是把 OMElement 的构建和响应解析统一起来。思路是这样public class DynamicWsClient { private final ServiceClient client new ServiceClient(); public DynamicWsClient(String endpoint, String soapAction) { Options opts new Options(); opts.setTo(new EndpointReference(endpoint)); opts.setAction(soapAction); opts.setTimeOutInMilliSeconds(10000); client.setOptions(opts); } public OMElement send(OMElement request) throws Exception { return client.sendReceive(request); } public String sendAndGetText(OMElement request) throws Exception { OMElement resp send(request); return resp.getText(); } public void close() throws Exception { client.cleanup(); } }逻辑说明构造函数绑定端点地址和 SOAPActionsend 方法负责发送sendAndGetText 直接返回响应体的文字内容适合响应结构简单、只需要取一个值的情况。close 方法调用 cleanup 释放连接资源。参数说明setTimeOutInMilliSeconds 设置请求超时不设置时默认走 HTTP transport 的全局超时往往很长甚至不生效。生产环境务必显式设置。5.2 动态调用的适用边界动态调用不是万能的。复杂类型的序列化、附件上传、Mutual SSL 这些场景手写 XML 的复杂度会急剧上升这时候老老实实用 WSDL2Java 生成代码反而更省事。我的经验总结是三看原则看接口数量少于 5 个操作且结构简单动态调用。看变更频率服务端一周改一次参数动态调用改 XML 比重新生成代码快得多。看传输需求只要普通 SOAP/HTTP动态调用涉及 MTOM 附件或者 WS-Security生成 stub。如果对接的都是复杂嵌套的 XML 结构我一般会先用 WSDL2Java 生成一版代码做参考看服务端期望的 XML 长什么样子然后基于这个参考结构手写模板字符串这样既能绕开一堆生成类又不会因为命名空间猜错而踩坑。5.3 结合配置文件的动态调用设计再进一步可以把 endpoint、soapAction、请求模板全部外置到配置文件order.endpointhttp://localhost:8080/axis2/services/OrderService order.actionurn:createOrder order.templatens:createOrder xmlns:nshttp://ws.example.com/samplens:orderId{orderId}/ns:orderIdns:quantity{quantity}/ns:quantity/ns:createOrder然后加载模板用字符串替换填充参数再用 AOMUtils.stringToOM 转成 OMElement 发送。这种方式下新增一个第三方接口不需要改代码只需要加一条配置对交付和维护都比较友好。我最初是从一个对接了 7 个不同银行接口的项目里总结出这套写法的。那 7 个接口每个都有自己的命名空间、SOAPAction 和报文结构每次都生成 stub 的话光是编译时间和代码量就能把项目拖垮。后来把动态调用封装好之后新接口从拿到文档到联调上线通常半天之内就能完成。从那以后我每次接新的 WebService 接口都会先问自己一个问题这里真的需要生成一堆 stub 类吗如果答案是否定的就直接走动态调用把时间和精力留给真正的业务逻辑。希望这篇笔记能帮你把 Axis2 这条老路走得更顺一点。本文还有配套的精品资源点击获取