ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Axis反序列化报错Unmarshalling Error:空字符串转数字失败排查与修复

Axis反序列化报错Unmarshalling Error:空字符串转数字失败排查与修复 先说结论这个看着像天书的报错Unmarshalling Error: For input string: 用大白话翻译就是——客户端用 Axis 去解析服务端返回的 SOAP 报文时发现某个声明为数字类型的字段实际塞进来的却是空字符串Java 试图把它转成数字结果当场翻车抛出了NumberFormatException。这两年 WebService 像是上个时代的产物但你真要维护过银行、电信、政企这些老系统之间的接口绝对绕不开 Axis。我自己就接过一个存量项目服务端是.NET 写的客户端是 Java Axis 1.4两边契约文件传了 N 回终于调到最后一个数字字段时报了这个错。当天下午到晚上全耗在这上面报错信息在网上搜了半天基本没一篇能直接照抄解决的。这篇文章就把我这回的排障过程、根因拆解、几种可落地的修复方案全部写清楚顺手把调试 WebService 常用的工具姿势也一并整理出来希望能帮你少走点弯路。1. 问题现场这个异常到底长什么样1.1 接口背景和故障表现先说下我当时的项目情况。这个接口是拿 Axis 1.4 生成的客户端 Stub 去调一个第三方服务端传输的数据是订单信息其中包含金额、数量、税率这类数字字段。接口之前一直运行得好好的突然某天开始调用同一个接口的某几笔订单会稳定复现异常而另外一些订单又是正常的。异常栈大概是这个样子AxisFault faultCode: {http://schemas.xmlsoap.org/soap/envelope/}Server.userException faultString: org.xml.sax.SAXException: Unmarshalling Error: For input string: faultActor: null faultDetail: {http://xml.apache.org/axis/}stackTrace: org.xml.sax.SAXException: Unmarshalling Error: For input string: at org.apache.axis.encoding.ser.SimpleDeserializer.onEndElement(SimpleDeserializer.java:172) at org.apache.axis.encoding.DeserializationContext.endElement(DeserializationContext.java:1085) ...注意最后那个For input string: 这里其实有两层含义For input string: 是 JDK 的NumberFormatException的标准消息格式表示试图把空字符串转成数字至于你有时会看到三个引号甚至更多是日志框架或者异常栈在拼接消息时又在整个消息外面加了一层引号实际解析的目标就是一个长度为零的字符串没有别的值。1.2 排查的第一反应先别急着改代码遇到这种报错很多人的第一反应是去看客户端代码觉得是不是自己传参传错了。但Unmarshalling Error的关键词已经说明问题了这个错误发生在客户端解析服务端返回的 XML 的阶段换句话说你的请求服务端收到了服务端也处理了但返回的 SOAP 响应里某个字段的值不合契约。我的建议是第一步千万别闷头去翻业务代码而是要先把服务端返回的原始报文拿过来看。怎么拿最简单的是在客户端开启 Axis 的报文调试打印出完整的 request 和 response。如果你用的是 JUnit 或者 main 方法本地调直接在系统属性里加这两行-Daxis.tcpmon.enabletrue -Dorg.apache.axis.components.net.SocketFactoryorg.apache.axis.components.net.SunHTTPFactory不过更通用的做法是配一个 TCPMon 拦截端口稍后我在调试工具那节详细讲。当你抓到返回报文基本一眼就能定位到问题字段。2. 定位过程从异常信息挖到真正的脏数据2.1 异常栈透露的关键线索回头细看异常栈SimpleDeserializer.onEndElement这个方法名非常关键。在 Axis 1.x 里SimpleDeserializer负责把 XML 元素的内容转换成 Java 的基本类型比如 int、long、double、boolean 这些。它的源码逻辑大致是这样的public void onEndElement(DeserializationContext context) throws SAXException { ... String value context.getText(); if (value null || .equals(value)) { // 这里就会走 new Integer() 的逻辑然后抛 NumberFormatException } if (type.equals(java.lang.Integer.class)) { setValue(Integer.valueOf(value)); } else if (type.equals(java.lang.Long.class)) { setValue(Long.valueOf(value)); } ... }context.getText()拿到的就是当前 XML 元素里的文本内容然后 Axis 会把这个文本交给对应的 Java 包装类型做转换。所以当它碰上空字符串Integer.valueOf()就会抛出异常Axis 再把这个异常包装成SAXException: Unmarshalling Error抛出来。这说明根子一定是在生成 Stub 的时候WSDL 里把某个字段定义成了xsd:int或xsd:long或者更准确地说Axis 在 WSDL2Java 的时候把这个字段映射成了 Java 的Integer或Long类型。而服务端这次返回的对应 XML 节点内容为空。2.2 抓包确认报文里到底传了什么把 TCPMon 抓到响应报文一打开我之前还抱有的一丝侥幸立马没了。返回的订单节点长这样order orderId10086/orderId amount/amount quantity2/quantity taxRate xsi:niltrue/ /order这里有两个问题字段amount是空节点taxRate是xsi:niltrue的空节点。但有意思的是taxRate没有报错而amount报错了。原因是在 Axis 的序列化/反序列化规则里xsi:niltrue会被识别为显式空值Axis 在某些配置下会把它转成 Java 的null而amount/amount这种“看起来是空但其实不是 null”的写法Axis 就把里面的文本当成空字符串去解析了。服务端为什么会产生这种奇怪的数据最常见的原因就是服务端代码里这个字段的值是null但服务端的序列化引擎没有输出xsi:niltrue而是直接输出了一对空标签。跨语言调用时这种坑特别常见尤其是服务端用了某种自定义的 XML 序列化封装它把 null 字段按“空字符串”给序列化了出来。2.3 为什么偏偏是某些订单报错这也是一个值得解释的现象。为什么同一套接口、同一段代码一部分订单正常一部分报错其实很简单正常订单的amount字段有值比如amount199.00/amount反序列化自然没问题。而报错的那些订单恰好这个字段为空。这种“间歇性”故障最有迷惑性——但它其实一点不随机只要某个订单满足“数字字段无值”这个条件就必然报错。后来我翻了历史数据所有报错的订单确实都集中在金额缺失的少数记录上对上了。3. 根因拆解Axis 反序列化的机制盲区3.1 Axis 1.x 的序列化反序列化核心流程要彻底理解这个问题还是得回到 Axis 的机制本身。Axis 1.x 是当年 JAX-RPC 规范下的老牌实现它的客户端调用流程大致是通过服务接口的 Proxy 发起调用将 Java 方法的参数序列化为 SOAP 请求报文发送给服务端服务端返回 SOAP 响应报文Axis 根据 WSDL 里声明的类型用对应的 Deserializer 把 XML 解析回 Java 对象。问题出在第 4 步。Axis 在解析元素时不太关心这个 XML 节点到底是“没有值”还是“空标签”它的Deserializer拿到的是字符串内容然后直接尝试类型转换。没有值的节点getText()返回空字符串转换失败就抛异常。可以这么理解WSDL 契约定的是“这个字段是 Integer”但是回应报文里给它的文本内容是在 Axis 眼里这和给你塞了个abc没有任何区别反正都是转不成数字的字符串。区别只是abc报的是For input string: abc而空字符串报的是For input string: 后面这个看起来像乱码一样。3.2 null 与空字符串在 SOAP 规范里的微妙区别这里面的坑其实源于 SOAP 规范和实现之间的一层落差。SOAP 编码规范里用xsi:niltrue表示“这是个空值”如果一个元素既没有xsi:nil属性中间又是空的规范的语义是“空字符串”而不是“null”。很多服务端框架比如某些 .NET 早期版本或一些自定义序列化封装在序列化 null 字段时输出的是amount/amount只有严格遵循规范的实现才会输出amount xsi:niltrue/。问题在于对于 Integer 类型的字段在业务语义上“空字符串”是不合法的因为数字类型没有“空字符串”这种状态。两边对“空”的认知不对齐导致客户端拿到空字符串后翻车。3.3 WSDL 中的类型声明和 nillable 属性如果 WSDL 里对字段做了足够明确的表达其实可以规避一部分问题。例如xsd:element nameamount typexsd:decimal minOccurs0 nillabletrue/nillabletrue明确告诉客户端“这个字段可以为空”。Axis 在做 WSDL2Java 时对nillable的处理在不同版本里表现不一致。很多老项目用的 WSDL 是从别的工具生成的字段声明里压根没写nillable这就等于没告诉客户端“这里可能没值”。所以排查这类问题时建议同时打开 WSDL 源文件对照报错的字段看看原始声明长什么样。如果是那种手工拼的 WSDL往往能看出很多契约层面的漏洞。4. 解决方案从应急到根治的五种办法4.1 应急方案把字段类型改成 String如果你现在正在生产环境告警最优先的目标是快速恢复接口。这种情况下最快、最稳的应急办法是重新生成客户端 Stub把出问题的字段类型从Integer/Long/BigDecimal改成String。怎么改如果你手头有 WSDL直接用WSDL2Java重新生成然后在生成的 Stub 里手动把对应字段类型改成 String。如果没有 WSDL只能在生成的代码里硬改// 原类型 private java.lang.Integer amount; // 改为 private java.lang.String amount;更重要的是所有引用这个字段的地方比如getAmount()、setAmount()以及构造器、equals、hashCode、toString这些生成代码都要跟着改。这活很机械但也不难IDE 的全局替换能帮你完成。改成 String 之后服务端返回空字符串也好、xsi:niltrue也好都不会再引发反序列化异常。你在业务层再做一层转换空字符串就按 null 处理有值就new BigDecimal(amount)活一点没少但接口不会再挂了。这个方案的缺点是治标不治本——它没有解决服务端为什么会返回空值的根因只是把客户端防御住了。作为应急完全可以但要理解它的边界。4.2 治本方向推动服务端修复数据从根上解决问题的方式是让服务端不要返回空字符串给数字字段而是返回0或者规范的xsi:nil。如果是你们自己的服务端改起来很简单把相应字段的默认值从 null 改成 0或者让序列化引擎输出xsi:niltrue。但如果你跟我一样调的是第三方的服务端就得提工单、发邮件推动对方改数据源或者改序列化配置。这个过程有时比改代码本身还慢所以应急方案还是得有。这里有一个谈判抓手你可以明确告诉对方他们返回的 XML 不符合 SOAP 规范中“数字类型空值”的约定规范的写法是amount xsi:niltrue/而不是amount/amount前者表示“空值”后者表示“空字符串”在类型严格的客户端解析逻辑里两者是截然不同的东西。对方如果做过几年 WebService 接口通常会认可这个说法。4.3 中间地带在 WSDL 上做文章如果 WSDL 是你们维护的而且能推动一次合同版本的更新那可以在 WSDL 层面把字段声明调整一下增加nillabletrue明确允许空值xsd:element nameamount typexsd:decimal minOccurs0 nillabletrue/这样当 Axis 重新生成 Stub 时字段有更大概率被映射成可空的对象类型同时对空值也更有容忍度。不过要注意nillable在不同版本的 Axis 生成器里表现差异很大有些老版本根本不把nillable当回事你还是会遇到同样的问题。所以这个方案更多是“配合”其他方案使用而不是单独作为解法。4.4 进阶方案自定义 Deserializer 做兜底如果你既改不了服务端又不想把字段类型降级成 String那就得在 Axis 的反序列化层动手脚了。Axis 1.x 允许你注册自定义的类型映射器TypeMapping为某个 QName 指定自定义的 Deserializer在反序列化时对空字符串做容错处理。核心思路是写一个DeserializerImpl的子类在onEndElement阶段做拦截import org.apache.axis.encoding.DeserializerImpl; import org.apache.axis.encoding.DeserializationContext; import org.xml.sax.SAXException; public class LenientBigDecimalDeserializer extends DeserializerImpl { Override public void onEndElement(DeserializationContext context) throws SAXException { String text context.getText(); if (text null || text.trim().isEmpty()) { setValue(null); // 或者 setValue(BigDecimal.ZERO) return; } try { setValue(new BigDecimal(text.trim())); } catch (NumberFormatException e) { setValue(null); } } }然后在客户端初始化的地方把这个 Deserializer 注册到类型映射里import org.apache.axis.client.Service; import org.apache.axis.encoding.TypeMapping; import org.apache.axis.encoding.TypeMappingRegistry; import javax.xml.namespace.QName; Service service new Service(); TypeMappingRegistry registry service.getTypeMappingRegistry(); TypeMapping mapping registry.getOrMakeTypeMapping(http://schemas.xmlsoap.org/soap/encoding/); mapping.register(BigDecimal.class, new QName(http://www.w3.org/2001/XMLSchema, decimal), null, LenientBigDecimalDeserializer.class);这方案的优点是粒度细、不影响其它字段的正常校验缺点是对 Axis 内部机制有一定理解门槛而且注册逻辑维护成本略高。我那次最后没有走到这一步因为时间太紧直接用了 4.1 的 String 方案先顶上后续再推动服务端规范数据。但如果你对 Axis 比较熟这个方案其实很优雅。4.5 终极方案架构层面的替换思路如果你实在被 Axis 折磨够了也可以考虑把客户端调用层换成更主流的 JAX-WS 实现比如 Apache CXF 或 JAX-WS RI。CXF 在反序列化时对空值和 null 的处理比 Axis 1.x 宽容得多遇到空字符串转数字的场景有自己的一套容错策略很多类似问题会自动化解。但悲催的是老系统的 WSDL 常常是 JAX-RPC 风格或者 RPC/encoded 风格的JAX-WS 不一定能直接兼容迁移成本不小。这种方案只适合那种“反正是个新接口要从头接那干脆别用 Axis”的场景不适合修线上故障。所以我的建议排序是应急用 4.1 → 治本推 4.2 → 条件允许再加 4.3 → 对 Axis 熟悉可考虑 4.4 → 新项目彻底避开 Axis 用 4.5。5. 实战中的调试工具把 SOAP 报文晾在阳光下5.1 Axis 开发的传统神器 TCPMonTCPMon 是 Apache 出的一个轻量级 TCP 监听工具专门用来截获 HTTP/SOAP 请求。原理特别简单你在本机起一个 TCP 端口客户端把 WebService 的 endpoint 地址改成本机这个端口TCPMon 再把请求转发到真正的服务端地址同时把两边收发的报文都打印出来。用的时候这样配Listen Port比如 8888Target Hostname真正的服务端 IP比如 10.20.30.40Target Port真正的服务端端口比如 8080然后在 Java 客户端里把 endpoint 临时改成http://localhost:8888/xxx/services/xxx跑一次调用TCPMon 面板里就能看到完整的请求和响应 XML。这个工具在 Axis 时代几乎是标配外挂定位报文问题不可或缺。5.2 Postman 调用 WebService 的正确姿势现在很多人已经不用 TCPMon 了因为 Postman 就够用。用 Postman 调 SOAP 接口要点有三个第一请求方式必须是 POST。第二Header 里要设置Content-Type: text/xml; charsetutf-8有些服务端还会校验SOAPAction你得从 WSDL 里找到对应方法的 SOAPAction 值按需加上。第三Body 选择raw类型直接把 SOAP 请求体贴进去soapenv:Envelope xmlns:soapenvhttp://schemas.xmlsoap.org/soap/envelope/ soapenv:Header/ soapenv:Body ns1:queryOrder xmlns:ns1http://service.example.com/ orderId10086/orderId /ns1:queryOrder /soapenv:Body /soapenv:Envelope用 Postman 有个好处你可以自由修改请求参数反复验证服务端的各种返回边界比如故意让它返回一个空值字段看它给的到底是amount/amount还是amount xsi:niltrue/。这种对照实验对定位问题非常有帮助。我当时就是用 Postman 手动构造了不同订单的请求把服务端返回的 XML 全部抓出来对比才确认了“报错订单的 amount 字段全是空标签”这个规律。5.3 SoapUI 和公共测试服务然后是 SoapUI它的强项是导入 WSDL 后自动生成所有方法的请求模板还能做断言、跑简单的回归测试。对付复杂 WSDL、多方法接口SoapUI 比 Postman 更适合当工程化工具。调试阶段还有一个技巧如果没有真实服务端环境可以找一些公共的 SOAP 测试服务来验证你的客户端代码和思路。网上有不少公网可用的 SOAP 示例接口比如一些免费的天气、汇率、号码归属地查询服务拿它们当靶场测试 Axis 客户端、Postman 请求格式、自定义 Deserializer 这些都是极好的。不过公网服务时好时坏别太依赖自己本地起一个最简单的最稳妥。5.4 Java 端快速打印 SOAP 报文的配置除了外部拦截工具Axis 客户端自身也支持开调试日志。在 log4j 配置里把 Axis 的日志级别调到 DEBUGlog4j.logger.org.apache.axisDEBUG log4j.logger.httpclient.wireDEBUG还可以在客户端代码里设置EngineConfiguration来开启报文输出System.setProperty(axis.tcpmon.enable, true); System.setProperty(axis.tcpmon.port, 8888);这种方式能抓到 Stub 层实际发送和接收的完整报文信息量比 TCPMon 还大因为它能看到 Axis 内部处理后的数据。6. 排障之后沉淀下来的几条经验6.1 跨语言 WebService 永远是契约先行这次问题的根本诱因不是代码写得差而是契约解释权的缺失。服务端觉得“空就空呗”客户端觉得“空字符串转不了数字”两边各说各话。所以遇到这类接口第一个要确认的永远不是代码而是 WSDL 和实际报文WSDL 上字段是什么类型、允许不允许为空实际报文的空值是用什么形式表达的。一条可以直接抄走的教训数字字段在跨系统接口里要么就约定“没值传 0”要么就严格用xsi:niltrue。最怕的就是“有时候有值、有时候空标签”这种脏数据迟早让你在半夜被电话叫起来。6.2 反序列化错误先抓报文再查代码所有Unmarshalling Error、SAXParseException这类反序列化异常正确排查顺序永远是先抓完整报文再对照 WSDL 看类型声明最后才轮到客户端代码。很多人一开始就去翻 Stub 代码查来查去查不出所以然因为问题根本不在那里。报文是一切 WebService 问题的第一现场没有报文空谈代码大概率白忙一场。6.3 手写转换层是最后的万能兜底如果你对 Axis 的各种 TypeMapping、Deserializer 机制不熟也不想深入研究那最实用的兜底策略就是把所有可能出问题的字段都映射成 String在业务层做显式转换。虽然不够“优雅”但在生产环境面前稳定压倒一切。等你有空回头重构时再考虑用自定义 Deserializer 或者直接换框架。后来我在这类老接口的维护里默认就带一套“字符串接收 显式转换”的防御层极大减少了反序列化异常导致的线上故障。如果你也是接手老系统的人可以试试这个思路它能让你少挨不少骂。这次排障让我印象最深的一点是技术问题翻来覆去绕到最后往往是“数据结构语义”的对齐问题。报错本身五分钟就能定位真正耗时间的是想清楚该让哪一边让步。我的习惯是先让客户端防御住保住业务再推动服务端修正数据。两条线同时走效率最高。希望这篇记录对你接手的 Axis 老系统也能派上用场。
返回列表