从CXF 3.5.7迁移至4.1.0后SOAP修改失效,报cl空指针异常
问题
将项目从Apache CXF 3.5.7迁移至4.1.0版本后,自定义SOAP消息修改拦截器触发空指针异常,移除拦截器后系统恢复正常。异常栈如下:
java.lang.NullPointerException: NullPointerException invoking http://test:4004/se/tm/cc: Cannot invoke "java.net.http.HttpClient.sendAsync(java.net.http.HttpRequest, java.net.http.HttpResponse$BodyHandler)" because "cl" is null at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.mapException(HTTPConduit.java:1452) at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.close(HTTPConduit.java:1438) at org.apache.cxf.transport.http.HttpClientHTTPConduit$HttpClientWrappedOutputStream.close(HttpClientHTTPConduit.java:824) at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:307) at org.apache.cxf.endpoint.ClientImpl.doInvoke(ClientImpl.java:530) at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:441) at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:356) at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:314) at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:334) at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:320) at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:307) at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121) at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:265) at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:233) at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:207) at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:159) at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:224) at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:312) at org.apache.cxf.transport.servlet.AbstractHTTPServlet.doPost(AbstractHTTPServlet.java:217) at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)
自定义拦截器代码:
public class OutPreStreamInterceptor extends AbstractPhaseInterceptor<Message>{ public OutPreStreamInterceptor(String phase) { super(phase); addBefore(SoapPreProtocolOutInterceptor.class.getName()); } @Override public void handleMessage(Message message) { OutputStream os = message.getContent(OutputStream.class); boolean isOutbound = message == message.getExchange().getOutMessage() || message == message.getExchange().getOutFaultMessage(); String content = getRawContent(message); String newContent = content; if (isOutbound) { InterceptorUtils.CachedStream cs = new InterceptorUtils.CachedStream(); message.setContent(OutputStream.class, cs); String action = retrieveSOAPAction(message); if (action.contains("test") ) { newContent = content.replace("<ns2:deviceType>tt</ns2:deviceType>", ""); } if (action.contains("test2")) { newContent = changeJobId(newContent); } if (!StringUtils.equalsIgnoreCase(newContent, content)) { log.debug("0;Outbound message has been changed: " + newContent); } try { replaceContent(message, newContent, os); } catch (IOException e) { log.error("0;Error while replacing outbound message {}", e.getMessage()); throw new RuntimeException("Error while replacing outbound message" + e); } } else { // If necessary, in-bound message may be changed here. } } public static void replaceContent(Message message, String newContent, OutputStream os) throws IOException { InputStream replaceInStream = new ByteArrayInputStream(newContent.getBytes(StandardCharsets.UTF_8)); IOUtils.copy(replaceInStream, os); replaceInStream.close(); os.flush(); message.setContent(OutputStream.class, os); os.close(); }
解决方案
1. 修正OutputStream生命周期管理
CXF 4.x对传输层流的生命周期管控更严格,禁止自定义拦截器提前关闭原始输出流。修改replaceContent方法,移除os.close()调用:
public static void replaceContent(Message message, String newContent, OutputStream os) throws IOException { InputStream replaceInStream = new ByteArrayInputStream(newContent.getBytes(StandardCharsets.UTF_8)); IOUtils.copy(replaceInStream, os); replaceInStream.close(); os.flush(); // 移除os.close(),由CXF框架负责流的最终关闭 message.setContent(OutputStream.class, os); }
2. 正确使用CachedStream处理内容修改
原始代码中缓存流的使用逻辑错误,未让框架先将内容写入缓存流再进行修改。调整拦截器核心逻辑:
@Override public void handleMessage(Message message) { boolean isOutbound = message == message.getExchange().getOutMessage() || message == message.getExchange().getOutFaultMessage(); if (isOutbound) { OutputStream originalOs = message.getContent(OutputStream.class); InterceptorUtils.CachedStream cs = new InterceptorUtils.CachedStream(); message.setContent(OutputStream.class, cs); try { // 触发后续拦截器将内容写入缓存流 cs.flush(); // 读取缓存流中的原始内容 String content = IOUtils.toString(cs.getInputStream(), StandardCharsets.UTF_8.name()); String newContent = content; String action = retrieveSOAPAction(message); if (action.contains("test")) { newContent = content.replace("<ns2:deviceType>tt</ns2:deviceType>", ""); } if (action.contains("test2")) { newContent = changeJobId(newContent); } if (!StringUtils.equalsIgnoreCase(newContent, content)) { log.debug("0;Outbound message has been changed: " + newContent); } // 将修改后的内容写入原始输出流 originalOs.write(newContent.getBytes(StandardCharsets.UTF_8)); originalOs.flush(); // 清理缓存流并恢复原始输出流 cs.close(); message.setContent(OutputStream.class, originalOs); } catch (IOException e) { log.error("0;Error while replacing outbound message {}", e.getMessage()); throw new RuntimeException("Error while replacing outbound message", e); } } else { // 入站消息处理逻辑(如有) } }
3. 确认拦截器阶段配置
确保拦截器使用Phase.PRE_STREAM阶段,且在SoapPreProtocolOutInterceptor之前执行,修改构造方法:
public OutPreStreamInterceptor() { super(Phase.PRE_STREAM); addBefore(SoapPreProtocolOutInterceptor.class.getName()); }
4. 适配Jakarta EE API变化
CXF 4.x全面迁移至Jakarta EE API,检查项目中所有相关导入(如servlet、jaxws相关类)是否已从javax替换为jakarta,避免类加载冲突。
关键原因说明
- CXF 4.x的
HttpClientHTTPConduit对输出流状态检查更严格,提前关闭流会导致后续传输逻辑中HttpClient实例初始化失败,触发空指针。 - 原始代码中缓存流的使用逻辑颠倒,未让框架先写入内容再读取修改,直接操作原始流破坏了CXF的流处理链路。
内容的提问来源于stack exchange,提问作者Manav
相关产品推荐
相关产品推荐

