CXF 请求 HTTP 上下文不会在 2 个请求之间刷新
CXF request HTTP context is not flushed between 2 requests
TL;DR : 我试图创建一个测试,它对 2 个不同的 SOAP 操作(saveUser 和 getUser)执行 2 个 CXF 调用,但第二个失败,因为 SOAPAction 在http 请求设置为 "saveUser" 而不是 "getUser"。
我找到了 2 个解决方案来解决这个问题,但我对它们并不完全满意。
配置
这是我的端点配置:
final JaxWsProxyFactoryBean jaxWsProxyFactory = new JaxWsProxyFactoryBean();
jaxWsProxyFactory.setServiceClass(Internal.class);
jaxWsProxyFactory.setAddress(backendUrl + "INTERNAL");
jaxWsProxyFactory.setFeatures(getFeatures());
final Internal portType = (Internal) jaxWsProxyFactory.create();
((BindingProvider) portType).getRequestContext().put("thread.local.request.context", "true");
((BindingProvider) portType).getRequestContext().put("schema-validation-enabled", "false");
这是我的用法:
internalBackendG5.saveUser(getRequest);
internalBackendG5.getUser(saveRequest);
这两个操作定义为(感谢 wsdl2java):
(为了更简洁我重写了一些包,下面的代码可能有错别字)
@WebMethod(action = "http://www.test.org/internal/saveUser")
@WebResult(name = "saveUserResponse", targetNamespace = "urn:test.com:xsd:internal:userpreferences", partName = "parameters")
public com.test.internal.type.SaveUserResponseType saveUser(
@WebParam(partName = "parameters", name = "saveUser", targetNamespace = "urn:test.com:xsd:internal:userpreferences")
com.test.internal.type.SaveUserRequestType parameters
);
和
@WebMethod(action = "http://www.test.org/internal/getUser")
@WebResult(name = "getUserResponse", targetNamespace = "urn:test.com:xsd:internal:userpreferences", partName = "parameters")
public com.test.internal.type.GetUserResponseType getUser(
@WebParam(partName = "parameters", name = "getUser", targetNamespace = "urn:test.com:xsd:internal:userpreferences")
com.test.internal.type.GetUserRequestType parameters
);
运行时间
当我 运行 我的测试时,我在第二次调用时收到一个错误,该错误从后端作为 Soap 异常抛出:
javax.xml.ws.soap.SOAPFaultException: Unexpected element {urn:test.com:xsd:internal:userpreferences}getUser found. Expected {urn:test.com:xsd:internal:userpreferences}saveUser.
当我查看服务器收到的 Soap 请求时,我得到:
第一个请求(saveUser):
Headers: {[...] SOAPAction=["http://www.test.org/internal/saveUser"], user-agent=[Apache CXF 2.7.12]}
Payload: <soap:Envelope><soap:Body><saveUser>[...]</ns5:saveUser></soap:Body></soap:Envelope>
对于第二个请求 (getUser):
Headers: {[...] SOAPAction=["http://www.test.org/internal/saveUser"], user-agent=[Apache CXF 2.7.12]}
Payload: <soap:Envelope><soap:Body><getUser>[...]</getUser></soap:Body></soap:Envelope>
当我只启动两个请求中的一个时,它们都有效。
我找到的解决方案
经过一些研究,我发现 Apache Cxf 拦截器 SoapPreProtocolOutInterceptor 是罪魁祸首。
当请求中已存在 SoapAction 字符串时,它不会覆盖它。我的第二次调用(由于某些原因我无法解释)重用了在处理第一次调用时计算的 Cxf 请求上下文!
我做了什么来克服这个问题:
- 编写一个在 SoapPreProtocolOutInterceptor 之前调用的拦截器,强制删除 header 中的 SoapAction,以强制此拦截器执行 re-compute 操作。
- 或者我可以重置两个请求之间的请求上下文:
((BindingProvider) portType).getRequestContext().put(Header.HEADER_LIST, emptyList);
问题
所以我的问题是:是否存在 CXF 错误?(不应在 2 个请求之间共享 requestContext)或 我错过了一些 Cxf 配置?
PS:我使用了这个依赖项:
group:'org.apache.cxf', name:'cxf-rt-transports-http-jetty', version:'2.7.5'
group: 'org.apache.cxf', name: 'cxf-rt-features-clustering', version:'2.7.12'
group:'org.apache.cxf', name:'cxf-rt-frontend-jaxws', version:'2.7.12'
group: 'org.apache.cxf', name: 'cxf-api', version: '2.7.12'
group: 'org.apache.cxf', name: 'cxf-rt-bindings-soap', version: '2.7.12'
提前致谢!
我遇到了同样的问题,经过一些调试我想我已经找到了原因。
我在 PROTOCOL_HEADERS 地图上设置了我自己的自定义 http header,然后再调用这样的任何操作:
Map<String, List<String>> headers = new HashMap<>();
headers.put("header-name", Arrays.asList("value"));
proxy.getRequestContext().put(Message.PROTOCOL_HEADERS, headers);
从您的示例中,我没有看到您正在设置 PROTOCOL_HEADERS,但这是我的根本原因,因为这是包含 SOAPAction header 的映射。也许您的代码的其他部分可能正在篡改它。
首先,我们知道客户端代理请求上下文不是线程安全的
Official JAX-WS answer: No. According to the JAX-WS spec, the client proxies are NOT thread safe. To write portable code, you should treat them as non-thread safe and synchronize access or use a pool of instances or similar.
CXF answer: CXF proxies are thread safe for MANY use cases. The exceptions are:
Use of ((BindingProvider)proxy).getRequestContext() - per JAX-WS spec, the request context is PER INSTANCE. Thus, anything set there will affect requests on other threads.
这给我带来了一些关于使用此上下文的危险信号,但这似乎不是问题的原因,我只是向它添加了一个简单的协议 header,它不是特定于用户的.
上下文在 ClientImpl class 中声明,并用作每个服务调用的基础上下文:
protected Map<String, Object> currentRequestContext = new ConcurrentHashMap<String, Object>(8, 0.75f, 4);
每次请求上下文时,都会执行以下方法:
public Map<String, Object> getRequestContext() {
if (isThreadLocalRequestContext()) {
if (!requestContext.containsKey(Thread.currentThread())) {
requestContext.put(Thread.currentThread(), new EchoContext(currentRequestContext));
}
return requestContext.get(Thread.currentThread());
}
return currentRequestContext;
}
假设我们没有设置本地线程 属性,所以我们始终 return 当前请求上下文,它现在有我们的 PROTOCOL_HEADERS 映射,但仍然没有 SOAPAction header在里面。
然后是 SoapPreProtocolOutInterceptor class,(我只提取了 setSoapAction(...) 方法的相关部分:
Map<String, List<String>> reqHeaders
= CastUtils.cast((Map<?, ?>)message.get(Message.PROTOCOL_HEADERS));
if (reqHeaders == null) {
reqHeaders = new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
}
if (reqHeaders.size() == 0) {
message.put(Message.PROTOCOL_HEADERS, reqHeaders);
}
if (!reqHeaders.containsKey(SoapBindingConstants.SOAP_ACTION)) {
reqHeaders.put(SoapBindingConstants.SOAP_ACTION, Collections.singletonList(action));
}
这里我们将在最后一个 if 语句中结束,当我们在此客户端上执行我们的 first 调用时,因此设置 SOAP_ACTION header 在 SAME 协议 headers 映射上,它是当前请求上下文的一部分,在 ClientImpl class.
中声明
此客户端的任何后续请求都将以协议 headers 和上下文中的 SOAP_ACTION header 已经填充的映射为基础,从而导致观察到的行为.我们永远不会输入为我们的服务调用上下文创建具有 PROTOCOL_HEADERS 的新映射的 if 语句。
解决方案:
我认为最干净的解决方案是只使用拦截器在协议 headers 映射中设置值,请注意我们根本不在基本请求上下文上操作:
public class HttpHeaderExampleInterceptor extends AbstractPhaseInterceptor
{
private static final String HEADER_NAME = "header_name";
private String value;
public HttpHeaderExampleInterceptor(String value)
{
super(Phase.MARSHAL);
this.value = value;
}
/**
* @see org.apache.cxf.binding.soap.interceptor.SoapPreProtocolOutInterceptor#setSoapAction(SoapMessage)
*/
@Override
public void handleMessage(Message message) throws Fault
{
Map<String, List<String>> protocolHeaders = CastUtils.cast((Map<?, ?>) message.get(Message.PROTOCOL_HEADERS));
if (protocolHeaders == null)
{
protocolHeaders = new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
message.put(Message.PROTOCOL_HEADERS, protocolHeaders);
}
protocolHeaders.put(HEADER_NAME, Collections.singletonList(value));
}
}
希望这对您有所帮助,如果有人在我的调试中发现错误,请分享,因为这个问题确实很有趣。
所以@DDV的回答是正确的,我想补充两点:
- 我看到版本
3.2.5
也有同样的问题,但是 PhaseInterceptor
有一些变化,所以 @DDV 的答案中的代码没有完全翻译。
- 我在 WS-Addressing 中遇到问题,所以我希望这个答案可以帮助人们搜索 WS-Addressing
class MessageIdResetInterceptor extends AbstractPhaseInterceptor[SoapMessage](Phase.SETUP_ENDING) {
override def handleMessage(message: SoapMessage): Unit = {
val addressing = message
.getContextualProperty("javax.xml.ws.addressing.context.outbound")
.asInstanceOf[AddressingProperties]
addressing.setMessageID(null)
addressing.setAction(null)
}
}
TL;DR : 我试图创建一个测试,它对 2 个不同的 SOAP 操作(saveUser 和 getUser)执行 2 个 CXF 调用,但第二个失败,因为 SOAPAction 在http 请求设置为 "saveUser" 而不是 "getUser"。 我找到了 2 个解决方案来解决这个问题,但我对它们并不完全满意。
配置
这是我的端点配置:
final JaxWsProxyFactoryBean jaxWsProxyFactory = new JaxWsProxyFactoryBean();
jaxWsProxyFactory.setServiceClass(Internal.class);
jaxWsProxyFactory.setAddress(backendUrl + "INTERNAL");
jaxWsProxyFactory.setFeatures(getFeatures());
final Internal portType = (Internal) jaxWsProxyFactory.create();
((BindingProvider) portType).getRequestContext().put("thread.local.request.context", "true");
((BindingProvider) portType).getRequestContext().put("schema-validation-enabled", "false");
这是我的用法:
internalBackendG5.saveUser(getRequest);
internalBackendG5.getUser(saveRequest);
这两个操作定义为(感谢 wsdl2java):
(为了更简洁我重写了一些包,下面的代码可能有错别字)
@WebMethod(action = "http://www.test.org/internal/saveUser")
@WebResult(name = "saveUserResponse", targetNamespace = "urn:test.com:xsd:internal:userpreferences", partName = "parameters")
public com.test.internal.type.SaveUserResponseType saveUser(
@WebParam(partName = "parameters", name = "saveUser", targetNamespace = "urn:test.com:xsd:internal:userpreferences")
com.test.internal.type.SaveUserRequestType parameters
);
和
@WebMethod(action = "http://www.test.org/internal/getUser")
@WebResult(name = "getUserResponse", targetNamespace = "urn:test.com:xsd:internal:userpreferences", partName = "parameters")
public com.test.internal.type.GetUserResponseType getUser(
@WebParam(partName = "parameters", name = "getUser", targetNamespace = "urn:test.com:xsd:internal:userpreferences")
com.test.internal.type.GetUserRequestType parameters
);
运行时间
当我 运行 我的测试时,我在第二次调用时收到一个错误,该错误从后端作为 Soap 异常抛出:
javax.xml.ws.soap.SOAPFaultException: Unexpected element {urn:test.com:xsd:internal:userpreferences}getUser found. Expected {urn:test.com:xsd:internal:userpreferences}saveUser.
当我查看服务器收到的 Soap 请求时,我得到:
第一个请求(saveUser):
Headers: {[...] SOAPAction=["http://www.test.org/internal/saveUser"], user-agent=[Apache CXF 2.7.12]}
Payload: <soap:Envelope><soap:Body><saveUser>[...]</ns5:saveUser></soap:Body></soap:Envelope>
对于第二个请求 (getUser):
Headers: {[...] SOAPAction=["http://www.test.org/internal/saveUser"], user-agent=[Apache CXF 2.7.12]}
Payload: <soap:Envelope><soap:Body><getUser>[...]</getUser></soap:Body></soap:Envelope>
当我只启动两个请求中的一个时,它们都有效。
我找到的解决方案
经过一些研究,我发现 Apache Cxf 拦截器 SoapPreProtocolOutInterceptor 是罪魁祸首。
当请求中已存在 SoapAction 字符串时,它不会覆盖它。我的第二次调用(由于某些原因我无法解释)重用了在处理第一次调用时计算的 Cxf 请求上下文!
我做了什么来克服这个问题: - 编写一个在 SoapPreProtocolOutInterceptor 之前调用的拦截器,强制删除 header 中的 SoapAction,以强制此拦截器执行 re-compute 操作。 - 或者我可以重置两个请求之间的请求上下文:
((BindingProvider) portType).getRequestContext().put(Header.HEADER_LIST, emptyList);
问题
所以我的问题是:是否存在 CXF 错误?(不应在 2 个请求之间共享 requestContext)或 我错过了一些 Cxf 配置?
PS:我使用了这个依赖项:
group:'org.apache.cxf', name:'cxf-rt-transports-http-jetty', version:'2.7.5'
group: 'org.apache.cxf', name: 'cxf-rt-features-clustering', version:'2.7.12'
group:'org.apache.cxf', name:'cxf-rt-frontend-jaxws', version:'2.7.12'
group: 'org.apache.cxf', name: 'cxf-api', version: '2.7.12'
group: 'org.apache.cxf', name: 'cxf-rt-bindings-soap', version: '2.7.12'
提前致谢!
我遇到了同样的问题,经过一些调试我想我已经找到了原因。
我在 PROTOCOL_HEADERS 地图上设置了我自己的自定义 http header,然后再调用这样的任何操作:
Map<String, List<String>> headers = new HashMap<>();
headers.put("header-name", Arrays.asList("value"));
proxy.getRequestContext().put(Message.PROTOCOL_HEADERS, headers);
从您的示例中,我没有看到您正在设置 PROTOCOL_HEADERS,但这是我的根本原因,因为这是包含 SOAPAction header 的映射。也许您的代码的其他部分可能正在篡改它。
首先,我们知道客户端代理请求上下文不是线程安全的
Official JAX-WS answer: No. According to the JAX-WS spec, the client proxies are NOT thread safe. To write portable code, you should treat them as non-thread safe and synchronize access or use a pool of instances or similar.
CXF answer: CXF proxies are thread safe for MANY use cases. The exceptions are:
Use of ((BindingProvider)proxy).getRequestContext() - per JAX-WS spec, the request context is PER INSTANCE. Thus, anything set there will affect requests on other threads.
这给我带来了一些关于使用此上下文的危险信号,但这似乎不是问题的原因,我只是向它添加了一个简单的协议 header,它不是特定于用户的.
上下文在 ClientImpl class 中声明,并用作每个服务调用的基础上下文:
protected Map<String, Object> currentRequestContext = new ConcurrentHashMap<String, Object>(8, 0.75f, 4);
每次请求上下文时,都会执行以下方法:
public Map<String, Object> getRequestContext() {
if (isThreadLocalRequestContext()) {
if (!requestContext.containsKey(Thread.currentThread())) {
requestContext.put(Thread.currentThread(), new EchoContext(currentRequestContext));
}
return requestContext.get(Thread.currentThread());
}
return currentRequestContext;
}
假设我们没有设置本地线程 属性,所以我们始终 return 当前请求上下文,它现在有我们的 PROTOCOL_HEADERS 映射,但仍然没有 SOAPAction header在里面。
然后是 SoapPreProtocolOutInterceptor class,(我只提取了 setSoapAction(...) 方法的相关部分:
Map<String, List<String>> reqHeaders
= CastUtils.cast((Map<?, ?>)message.get(Message.PROTOCOL_HEADERS));
if (reqHeaders == null) {
reqHeaders = new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
}
if (reqHeaders.size() == 0) {
message.put(Message.PROTOCOL_HEADERS, reqHeaders);
}
if (!reqHeaders.containsKey(SoapBindingConstants.SOAP_ACTION)) {
reqHeaders.put(SoapBindingConstants.SOAP_ACTION, Collections.singletonList(action));
}
这里我们将在最后一个 if 语句中结束,当我们在此客户端上执行我们的 first 调用时,因此设置 SOAP_ACTION header 在 SAME 协议 headers 映射上,它是当前请求上下文的一部分,在 ClientImpl class.
中声明此客户端的任何后续请求都将以协议 headers 和上下文中的 SOAP_ACTION header 已经填充的映射为基础,从而导致观察到的行为.我们永远不会输入为我们的服务调用上下文创建具有 PROTOCOL_HEADERS 的新映射的 if 语句。
解决方案:
我认为最干净的解决方案是只使用拦截器在协议 headers 映射中设置值,请注意我们根本不在基本请求上下文上操作:
public class HttpHeaderExampleInterceptor extends AbstractPhaseInterceptor
{
private static final String HEADER_NAME = "header_name";
private String value;
public HttpHeaderExampleInterceptor(String value)
{
super(Phase.MARSHAL);
this.value = value;
}
/**
* @see org.apache.cxf.binding.soap.interceptor.SoapPreProtocolOutInterceptor#setSoapAction(SoapMessage)
*/
@Override
public void handleMessage(Message message) throws Fault
{
Map<String, List<String>> protocolHeaders = CastUtils.cast((Map<?, ?>) message.get(Message.PROTOCOL_HEADERS));
if (protocolHeaders == null)
{
protocolHeaders = new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
message.put(Message.PROTOCOL_HEADERS, protocolHeaders);
}
protocolHeaders.put(HEADER_NAME, Collections.singletonList(value));
}
}
希望这对您有所帮助,如果有人在我的调试中发现错误,请分享,因为这个问题确实很有趣。
所以@DDV的回答是正确的,我想补充两点:
- 我看到版本
3.2.5
也有同样的问题,但是PhaseInterceptor
有一些变化,所以 @DDV 的答案中的代码没有完全翻译。 - 我在 WS-Addressing 中遇到问题,所以我希望这个答案可以帮助人们搜索 WS-Addressing
class MessageIdResetInterceptor extends AbstractPhaseInterceptor[SoapMessage](Phase.SETUP_ENDING) {
override def handleMessage(message: SoapMessage): Unit = {
val addressing = message
.getContextualProperty("javax.xml.ws.addressing.context.outbound")
.asInstanceOf[AddressingProperties]
addressing.setMessageID(null)
addressing.setAction(null)
}
}