使用SimpleJdbcCall获取存储过程XML输出参数遇异常该如何解决?
我之前碰到过一模一样的问题——用SimpleJdbcCall调用SQL Server存储过程拿XML输出,结果SQLXML对象调用getString()直接炸NegativeArraySize异常,读二进制流还只拿到开头的ÿþ(这是UTF-16LE的字节序标记)。SSMS跑存储过程完全正常,说明问题出在JDBC驱动对SQLXML参数的解析逻辑上,给你几个实测有效的解决思路:
1. 放弃SimpleJdbcCall,用原生CallableStatement手动处理
SimpleJdbcCall的封装虽然方便,但在处理SQLXML这种特殊类型时容易踩坑。改用原生CallableStatement可以精准控制读取逻辑:
String procSql = "{call dbo.pProcName(?, ?)}"; try (CallableStatement cs = dataSource.getConnection().prepareCall(procSql)) { // 设置输入ID参数 cs.setLong(1, inDTO.getIdCertificado()); // 重点:把输出参数注册为LONGVARCHAR而不是SQLXML cs.registerOutParameter(2, Types.LONGVARCHAR); cs.execute(); // 直接拿字符串,再手动去掉开头的BOM(如果有的话) String xmlContent = cs.getString(2); if (xmlContent != null && xmlContent.startsWith("\uFEFF")) { xmlContent = xmlContent.substring(1); } // 现在xmlContent就是正常的XML字符串了 } catch (SQLException e) { e.printStackTrace(); }
为什么用Types.LONGVARCHAR?因为SQL Server返回的XML默认是带BOM的UTF-16LE编码,用字符串类型读取可以绕开SQLXML对象的解析bug,直接拿到完整内容。
2. 正确读取SQLXML的二进制流
如果你不想换CallableStatement,那得把SQLXML的二进制流读对——不能只看开头的两个字符,要跳过BOM后用UTF-16LE编码解析:
Map<String, Object> resultMap = simpleCall.execute(params); SQLXML sqlXml = (SQLXML) resultMap.get("@XmlOut"); try (InputStream xmlStream = sqlXml.getBinaryStream()) { // 跳过前2个字节的UTF-16LE BOM(ÿþ对应的就是这两个字节) xmlStream.skip(2); // 用UTF-16LE编码读取剩余流内容 InputStreamReader reader = new InputStreamReader(xmlStream, StandardCharsets.UTF_16LE); StringBuilder xmlBuilder = new StringBuilder(); char[] buffer = new char[1024]; int readLen; while ((readLen = reader.read(buffer)) != -1) { xmlBuilder.append(buffer, 0, readLen); } String xmlContent = xmlBuilder.toString(); } catch (IOException | SQLException e) { e.printStackTrace(); }
之前你只看到两个字符,是因为没继续读后面的内容,跳过BOM后按正确编码解析就能拿到完整XML了。
3. 升级SQL Server JDBC驱动版本
这个NegativeArraySize异常其实是旧版本mssql-jdbc驱动的已知bug,比如8.x及更早版本容易出现。建议升级到最新的12.x版本,官方修复了不少SQLXML相关的解析问题,说不定升级后直接用getString()就正常了。
4. 调整存储过程的XML输出编码(可选)
如果有权限改存储过程,可以强制让XML输出不带BOM的UTF-8编码:
ALTER PROCEDURE dbo.pProcName @ID BIGINT, @XmlOut XML OUTPUT AS BEGIN -- 生成XML时指定TYPE和ROOT,再转换为VARCHAR(MAX)输出 SET @XmlOut = CAST( (SELECT * FROM YourTargetTable WHERE ID = @ID FOR XML PATH('Item'), ELEMENTS, TYPE, ROOT('Root')) AS VARCHAR(MAX) ); END
不过这种方式要注意XML的大小不能超过VARCHAR(MAX)的限制(大概2GB),如果XML太大还是用前面的方法更稳妥。
内容的提问来源于stack exchange,提问作者H. Grigolli

