当 uri 变量不为 null 时,为什么 HttpUriRequest.getURI() 返回 null?
Why is HttpUriRequest.getURI() returning null when the uri variable is not null?
摘要
我遇到了一个独特的场景,当实例中的变量是不是 null
。在这种情况下 HttpUriRequest.getURI()
到 return 和 null
是不可能的,但这就是正在发生的事情。
代码演练
这是代码演练。它演示了 HttpUriRequest.getURI()
方法如何不可能 return 一个 null
,但它正在 returning null
.
使用的相关库有
- com.sun.jersey.contribs:jersey-apache-client4:jar:1.19.4
- org.apache.httpcomponents:httpclient:jar:4.5.12
我们的内部应用程序调用第三方代码,该代码使用 jersey-apache-client4
通过 apache httpcomponents httpclient
对外部系统进行 RESTful 调用。
就此问题而言,请求从调用 ApacheHttpClient4Handler.handle(ClientRequest)
开始,来源如下。
- 编号 行内评论将指导演练。
class ApacheHttpClient4Handler
v1.19.4
的相关来源
package com.sun.jersey.client.apache4;
// ... irrelevant code removed
public final class ApacheHttpClient4Handler extends TerminatingClientHandler {
private final HttpClient client;
private final boolean preemptiveBasicAuth;
// ... irrelevant code removed
public ClientResponse handle(final ClientRequest cr) throws ClientHandlerException {
// {1} this is where the HttpUriRequest instance is created.
// See source walkthrough of getUriHttpRequest(ClientRequest) below.
final HttpUriRequest request = getUriHttpRequest(cr);
// {8} - calling request.getURI() here returns not null, and logging produces the expected "https://{serverpath}" on toString()
writeOutBoundHeaders(cr.getHeaders(), request);
// {11} - calling request.getURI() here returns not null, and logging produces the expected "https://{serverpath}" on toString()
try {
HttpResponse response;
if(preemptiveBasicAuth) {
// this code block is never entered as preemptiveBasicAuth is always false for the scenario
// ... irrelevant code removed
} else {
// {12} - calling request.getURI() here returns not null, and logging produces the expected "https://{serverpath}" on toString()
// the following line throws the unexpected NullPointerException originating from the call to getHost(request).
// See source walkthrough of getHost(HttpUriRequest) below.
response = getHttpClient().execute(getHost(request), request);
}
// from here to the catch is never reached due to the NPE two lines above.
ClientResponse r = new ClientResponse(response.getStatusLine().getStatusCode(),
getInBoundHeaders(response),
new HttpClientResponseInputStream(response),
getMessageBodyWorkers());
if (!r.hasEntity()) {
r.bufferEntity();
r.close();
}
return r;
} catch (Exception e) {
// {15} - NPE is caught here and rethrown.
throw new ClientHandlerException(e);
}
}
// ... irrelevant code removed
// this is the method where the unexpected NPE is thrown.
private HttpHost getHost(final HttpUriRequest request) {
// {13} - calling request.getURI() here returns null.
// It is not possible for this to be happening.
// {14} - since getURI() returns null, a NPE will be thrown because .getHost() can't be called on a null return.
// WHY IS getURI() returning null ???!!!???
return new HttpHost(request.getURI().getHost(), request.getURI().getPort(), request.getURI().getScheme());
}
// {2} - this is called by handle(ClientRequest)
private HttpUriRequest getUriHttpRequest(final ClientRequest cr) {
final String strMethod = cr.getMethod();
final URI uri = cr.getURI();
// {3} - uri is not null, and logging produces the expected "https://{serverpath}" on toString()
final Boolean bufferingEnabled = isBufferingEnabled(cr);
final HttpEntity entity = getHttpEntity(cr, bufferingEnabled);
final HttpUriRequest request;
// {4} - uri is not null, and logging produces the expected "https://{serverpath}" on toString()
// even odds for a HttpGet, HttpPost, HttpPut, HttpDelete; HttpHead and HttpOptions do not get referenced by this scenario.
// it does not matter which class the request is, the uri will end up being null.
if (strMethod.equals("GET")) {
request = new HttpGet(uri);
} else if (strMethod.equals("POST")) {
request = new HttpPost(uri);
} else if (strMethod.equals("PUT")) {
request = new HttpPut(uri);
} else if (strMethod.equals("DELETE")) {
request = new HttpDelete(uri);
} else if (strMethod.equals("HEAD")) {
request = new HttpHead(uri);
} else if (strMethod.equals("OPTIONS")) {
request = new HttpOptions(uri);
} else {
// this block of code is never hit by the requests for this scenario.
request = new HttpEntityEnclosingRequestBase() {
@Override
public String getMethod() {
return strMethod;
}
@Override
public URI getURI() {
return uri;
}
};
}
// {5} - checking uri shows it is not null, and logging produces the expected "https://{serverpath}" on toString()
// calling request.getURI() here returns not null, and logging produces the expected "https://{serverpath}" on toString()
// {6} - sometimes this is called, sometimes it is not; regardless, the error scenario still happens
if(entity != null && request instanceof HttpEntityEnclosingRequestBase) {
((HttpEntityEnclosingRequestBase) request).setEntity(entity);
} else if (entity != null) {
throw new ClientHandlerException("Adding entity to http method " + cr.getMethod() + " is not supported.");
}
// ... irrelevant code removed
// {7} - calling request.getURI() here returns not null, and logging produces the expected "https://{serverpath}" on toString()
return request;
}
// ... irrelevant code removed
private void writeOutBoundHeaders(final MultivaluedMap<String, Object> headers, final HttpUriRequest request) {
// {9} - none of this code is relevant; it does not change the value of the uri
for (Map.Entry<String, List<Object>> e : headers.entrySet()) {
List<Object> vs = e.getValue();
if (vs.size() == 1) {
request.addHeader(e.getKey(), ClientRequest.getHeaderValue(vs.get(0)));
} else {
StringBuilder b = new StringBuilder();
for (Object v : e.getValue()) {
if (b.length() > 0) {
b.append(',');
}
b.append(ClientRequest.getHeaderValue(v));
}
request.addHeader(e.getKey(), b.toString());
}
}
// {10} - calling request.getURI() here returns not null, and logging produces the expected "https://{serverpath}" on toString()
}
// ... irrelevant code removed
}
界面的相关来源HttpUriRequest
v4.5.12
package org.apache.http.client.methods;
import java.net.URI;
import org.apache.http.HttpRequest;
public interface HttpUriRequest extends HttpRequest {
// ... irrelevant code removed
URI getURI();
// ... irrelevant code removed
}
摘要的相关来源 class HttpRequestBase
v4.5.12(HttpGet
、HttpPost
等扩展)
package org.apache.http.client.methods;
import java.net.URI;
// ... irrelevant code removed
public abstract class HttpRequestBase extends AbstractExecutionAwareRequest
implements HttpUriRequest, Configurable {
// ... irrelevant code removed
private URI uri;
// ... irrelevant code removed
@Override
public URI getURI() {
return this.uri;
}
// ... irrelevant code removed
public void setURI(final URI uri) {
this.uri = uri;
}
}
class HttpGet
v4.5.12 的相关来源(HttpPost
等类似实现)
package org.apache.http.client.methods;
import java.net.URI;
public class HttpGet extends HttpRequestBase {
public final static String METHOD_NAME = "GET";
public HttpGet() {
super();
}
public HttpGet(final URI uri) {
super();
setURI(uri);
}
/**
* @throws IllegalArgumentException if the uri is invalid.
*/
public HttpGet(final String uri) {
super();
setURI(URI.create(uri));
}
@Override
public String getMethod() {
return METHOD_NAME;
}
}
鉴于源代码演练和证明 Object 实例状态的可行性的支持评论,对 request.getURI()
的调用怎么可能 returning null
?
环境
- 此代码在 Linux 服务器 (JVM 8.0-5.40) 上的 IBM Liberty 下运行。
- 此代码也在 Windows 服务器 (JVM 1.7) 上的 Wildfly 下运行。
- 两种系统类型的代码相同。
- 代码在 Windows 服务器上从未出现过上述问题。
- 该代码在安装了 Liberty 管理控制台的 Liberty 服务器上没有上述问题。
观察结果
ApacheHttpClient4Hander.handle(ClientRequest)
的前 2-3 次传递在没有 request.getURI()
returning null
的情况下成功;仅在第 4 次和后续请求时 returns null
.
调查
以下是我目前采取的一些调查步骤:
1 - 我在 SO 上创建了这个案例以获取群体思维洞察力。
2 - 我在 IBM 开了一个案例。
3 - 自定义 ApacheHttpClient4Handler
- 我通过 SL4J 向所有方法添加调试日志记录。
结果
- 日志输出证明对
HttpUriRequest.getURI()
的调用仅在输入 ApacheHttpClient4Handler.getHost(final HttpUriRequest request)
后才开始 returning null
。
- 在
getHost(HttpUriRequest)
之前的任何调用显示 getURI()
到 return 非空。
- 在添加调试日志记录之后,而不是在 2-3 次和后续请求之后(根据观察),
HttpUriRequest.getURI()
现在在 1-2 次(和后续)请求之后 returns 为空。我怀疑这是由于调试日志记录主动调用 HttpUriRequest.getURI()
,但我无法确认这一点,也不知道为什么场景现在变短了。
4 - 进一步自定义 ApacheHttpClient4Handler
- 将
HttpUriRequest
的所有引用更改为 HttpRequestBase
结果
- 这样做并没有解决问题;
getURI()
还是 returns null
.
5 - 自定义 HttpRequestBase
- 将调试日志记录添加到
getURI()
/ setURI(...)
方法
结果
- 应用程序开始按设计工作,但有大量调试日志记录!!!
- 请参阅下面的 主要更新 部分。
6 - 自定义 HttpGet
、HttpPut
等
- 向构造函数添加调试日志记录。
结果
- 请参阅下面的 主要更新 部分。
7 - 将 IBM JVM 从 8.0-5.40 更新到 8.0-6.11
- IBM 发行说明中与 NPE 相关的错误修复列表
- IJ17563 | NPE WHEN WHEN CALLING JAVA.LANG.STRING.INDEXOF API WITH A LARGE OFFSET
- IJ19669 |使用 JAVA 8
读取空 PKCS12 密钥库时发生 NPE
- IJ20528 |对原始包装类型对象的方法调用的 NPE 或段错误
- IJ23079 |错误的 JIT 编译器优化导致意外的 NPE
- TODO:要在更新中发布的结果
结论
这是怎么回事?
Liberty 使用的 JVM 的 IBM 实现是否滥用了这段代码,还是有其他原因?
根据代码和演练,HttpUriRequest.getURI()
不可能 returning null
.
主要更新
根据调查中的第 5 步,我使用来自全球发行版的源代码自定义 httpclient
调试日志记录。将 JAR 添加到我的项目后,它开始工作了。我回滚了我的更改,从未修改的源重建了 JAR,将它添加到我的项目中,应用程序开始工作了。
我使用全球分布的 httpclient-4.5.12.jar
重新部署 我的应用程序失败了。
同样,我没有对代码进行任何更改;我获取了 httpclient-4.5.12-sources.jar
,在 Eclipse 中编译它并使用我新命名的 JAR httpclient-4.5.12.joshdm.jar
重新部署。并且有效。
我使用 JAD 反编译了两个 JAR 以检查代码。差异很小,仅在标签上,这意味着编译器不同。我比较了两个 JAR 的 MANIFEST.MF; Build-Jdk 条目不同。我使用 Jdk 1.8.0_191 编译。全局的是使用 Jdk 1.8.0_181.
编译的
我已向 IBM 报告了这种奇怪的差异,同时提供了编译后的 JAR 和源 JAR,并且正在等待回复,以了解为什么我的可以使用他们的 JVM 而全局的不能。我现在 99% 确定这是一个 IBM JVM 或服务器配置问题,与部署的应用程序无关。
更新 #2
添加以下配置并回滚到全局 JAR 有效(也适用于重新编译的 JAR)。
-Xshareclasses:none
- 禁用共享 class 缓存和 AOT 编译。
-Xnojit
- 禁用 JIT 编译
禁用共享 class 缓存、禁用 AOT 编译和禁用 Liberty 上的 JIT 编译似乎已经解决了这个问题。一旦我从 IBM 那里收到为什么需要上述设置的回复,我将写下一个实际的答案。
IBM Liberty 运行 版本 ibm-java-sdk-8.0-5.40
将它更新为 ibm-java-sdk-8.0-6.11
解决了它。
IBM 回应,“自 Java 8 SR5 FP40 {...}以来修复了几个 JIT 问题{...} {i}很可能您没有命中工作环境中相同的代码路径。"
摘要
我遇到了一个独特的场景,当实例中的变量是不是 null
。在这种情况下 HttpUriRequest.getURI()
到 return 和 null
是不可能的,但这就是正在发生的事情。
代码演练
这是代码演练。它演示了 HttpUriRequest.getURI()
方法如何不可能 return 一个 null
,但它正在 returning null
.
使用的相关库有
- com.sun.jersey.contribs:jersey-apache-client4:jar:1.19.4
- org.apache.httpcomponents:httpclient:jar:4.5.12
我们的内部应用程序调用第三方代码,该代码使用 jersey-apache-client4
通过 apache httpcomponents httpclient
对外部系统进行 RESTful 调用。
就此问题而言,请求从调用 ApacheHttpClient4Handler.handle(ClientRequest)
开始,来源如下。
- 编号 行内评论将指导演练。
class ApacheHttpClient4Handler
v1.19.4
package com.sun.jersey.client.apache4;
// ... irrelevant code removed
public final class ApacheHttpClient4Handler extends TerminatingClientHandler {
private final HttpClient client;
private final boolean preemptiveBasicAuth;
// ... irrelevant code removed
public ClientResponse handle(final ClientRequest cr) throws ClientHandlerException {
// {1} this is where the HttpUriRequest instance is created.
// See source walkthrough of getUriHttpRequest(ClientRequest) below.
final HttpUriRequest request = getUriHttpRequest(cr);
// {8} - calling request.getURI() here returns not null, and logging produces the expected "https://{serverpath}" on toString()
writeOutBoundHeaders(cr.getHeaders(), request);
// {11} - calling request.getURI() here returns not null, and logging produces the expected "https://{serverpath}" on toString()
try {
HttpResponse response;
if(preemptiveBasicAuth) {
// this code block is never entered as preemptiveBasicAuth is always false for the scenario
// ... irrelevant code removed
} else {
// {12} - calling request.getURI() here returns not null, and logging produces the expected "https://{serverpath}" on toString()
// the following line throws the unexpected NullPointerException originating from the call to getHost(request).
// See source walkthrough of getHost(HttpUriRequest) below.
response = getHttpClient().execute(getHost(request), request);
}
// from here to the catch is never reached due to the NPE two lines above.
ClientResponse r = new ClientResponse(response.getStatusLine().getStatusCode(),
getInBoundHeaders(response),
new HttpClientResponseInputStream(response),
getMessageBodyWorkers());
if (!r.hasEntity()) {
r.bufferEntity();
r.close();
}
return r;
} catch (Exception e) {
// {15} - NPE is caught here and rethrown.
throw new ClientHandlerException(e);
}
}
// ... irrelevant code removed
// this is the method where the unexpected NPE is thrown.
private HttpHost getHost(final HttpUriRequest request) {
// {13} - calling request.getURI() here returns null.
// It is not possible for this to be happening.
// {14} - since getURI() returns null, a NPE will be thrown because .getHost() can't be called on a null return.
// WHY IS getURI() returning null ???!!!???
return new HttpHost(request.getURI().getHost(), request.getURI().getPort(), request.getURI().getScheme());
}
// {2} - this is called by handle(ClientRequest)
private HttpUriRequest getUriHttpRequest(final ClientRequest cr) {
final String strMethod = cr.getMethod();
final URI uri = cr.getURI();
// {3} - uri is not null, and logging produces the expected "https://{serverpath}" on toString()
final Boolean bufferingEnabled = isBufferingEnabled(cr);
final HttpEntity entity = getHttpEntity(cr, bufferingEnabled);
final HttpUriRequest request;
// {4} - uri is not null, and logging produces the expected "https://{serverpath}" on toString()
// even odds for a HttpGet, HttpPost, HttpPut, HttpDelete; HttpHead and HttpOptions do not get referenced by this scenario.
// it does not matter which class the request is, the uri will end up being null.
if (strMethod.equals("GET")) {
request = new HttpGet(uri);
} else if (strMethod.equals("POST")) {
request = new HttpPost(uri);
} else if (strMethod.equals("PUT")) {
request = new HttpPut(uri);
} else if (strMethod.equals("DELETE")) {
request = new HttpDelete(uri);
} else if (strMethod.equals("HEAD")) {
request = new HttpHead(uri);
} else if (strMethod.equals("OPTIONS")) {
request = new HttpOptions(uri);
} else {
// this block of code is never hit by the requests for this scenario.
request = new HttpEntityEnclosingRequestBase() {
@Override
public String getMethod() {
return strMethod;
}
@Override
public URI getURI() {
return uri;
}
};
}
// {5} - checking uri shows it is not null, and logging produces the expected "https://{serverpath}" on toString()
// calling request.getURI() here returns not null, and logging produces the expected "https://{serverpath}" on toString()
// {6} - sometimes this is called, sometimes it is not; regardless, the error scenario still happens
if(entity != null && request instanceof HttpEntityEnclosingRequestBase) {
((HttpEntityEnclosingRequestBase) request).setEntity(entity);
} else if (entity != null) {
throw new ClientHandlerException("Adding entity to http method " + cr.getMethod() + " is not supported.");
}
// ... irrelevant code removed
// {7} - calling request.getURI() here returns not null, and logging produces the expected "https://{serverpath}" on toString()
return request;
}
// ... irrelevant code removed
private void writeOutBoundHeaders(final MultivaluedMap<String, Object> headers, final HttpUriRequest request) {
// {9} - none of this code is relevant; it does not change the value of the uri
for (Map.Entry<String, List<Object>> e : headers.entrySet()) {
List<Object> vs = e.getValue();
if (vs.size() == 1) {
request.addHeader(e.getKey(), ClientRequest.getHeaderValue(vs.get(0)));
} else {
StringBuilder b = new StringBuilder();
for (Object v : e.getValue()) {
if (b.length() > 0) {
b.append(',');
}
b.append(ClientRequest.getHeaderValue(v));
}
request.addHeader(e.getKey(), b.toString());
}
}
// {10} - calling request.getURI() here returns not null, and logging produces the expected "https://{serverpath}" on toString()
}
// ... irrelevant code removed
}
界面的相关来源HttpUriRequest
v4.5.12
package org.apache.http.client.methods;
import java.net.URI;
import org.apache.http.HttpRequest;
public interface HttpUriRequest extends HttpRequest {
// ... irrelevant code removed
URI getURI();
// ... irrelevant code removed
}
摘要的相关来源 class HttpRequestBase
v4.5.12(HttpGet
、HttpPost
等扩展)
package org.apache.http.client.methods;
import java.net.URI;
// ... irrelevant code removed
public abstract class HttpRequestBase extends AbstractExecutionAwareRequest
implements HttpUriRequest, Configurable {
// ... irrelevant code removed
private URI uri;
// ... irrelevant code removed
@Override
public URI getURI() {
return this.uri;
}
// ... irrelevant code removed
public void setURI(final URI uri) {
this.uri = uri;
}
}
class HttpGet
v4.5.12 的相关来源(HttpPost
等类似实现)
package org.apache.http.client.methods;
import java.net.URI;
public class HttpGet extends HttpRequestBase {
public final static String METHOD_NAME = "GET";
public HttpGet() {
super();
}
public HttpGet(final URI uri) {
super();
setURI(uri);
}
/**
* @throws IllegalArgumentException if the uri is invalid.
*/
public HttpGet(final String uri) {
super();
setURI(URI.create(uri));
}
@Override
public String getMethod() {
return METHOD_NAME;
}
}
鉴于源代码演练和证明 Object 实例状态的可行性的支持评论,对 request.getURI()
的调用怎么可能 returning null
?
环境
- 此代码在 Linux 服务器 (JVM 8.0-5.40) 上的 IBM Liberty 下运行。
- 此代码也在 Windows 服务器 (JVM 1.7) 上的 Wildfly 下运行。
- 两种系统类型的代码相同。
- 代码在 Windows 服务器上从未出现过上述问题。
- 该代码在安装了 Liberty 管理控制台的 Liberty 服务器上没有上述问题。
观察结果
ApacheHttpClient4Hander.handle(ClientRequest)
的前 2-3 次传递在没有request.getURI()
returningnull
的情况下成功;仅在第 4 次和后续请求时 returnsnull
.
调查
以下是我目前采取的一些调查步骤:
1 - 我在 SO 上创建了这个案例以获取群体思维洞察力。
2 - 我在 IBM 开了一个案例。
3 - 自定义 ApacheHttpClient4Handler
- 我通过 SL4J 向所有方法添加调试日志记录。
结果
- 日志输出证明对
HttpUriRequest.getURI()
的调用仅在输入ApacheHttpClient4Handler.getHost(final HttpUriRequest request)
后才开始 returningnull
。 - 在
getHost(HttpUriRequest)
之前的任何调用显示getURI()
到 return 非空。 - 在添加调试日志记录之后,而不是在 2-3 次和后续请求之后(根据观察),
HttpUriRequest.getURI()
现在在 1-2 次(和后续)请求之后 returns 为空。我怀疑这是由于调试日志记录主动调用HttpUriRequest.getURI()
,但我无法确认这一点,也不知道为什么场景现在变短了。
4 - 进一步自定义 ApacheHttpClient4Handler
- 将
HttpUriRequest
的所有引用更改为HttpRequestBase
结果
- 这样做并没有解决问题;
getURI()
还是 returnsnull
.
5 - 自定义 HttpRequestBase
- 将调试日志记录添加到
getURI()
/setURI(...)
方法
结果
- 应用程序开始按设计工作,但有大量调试日志记录!!!
- 请参阅下面的 主要更新 部分。
6 - 自定义 HttpGet
、HttpPut
等
- 向构造函数添加调试日志记录。
结果
- 请参阅下面的 主要更新 部分。
7 - 将 IBM JVM 从 8.0-5.40 更新到 8.0-6.11
- IBM 发行说明中与 NPE 相关的错误修复列表
- IJ17563 | NPE WHEN WHEN CALLING JAVA.LANG.STRING.INDEXOF API WITH A LARGE OFFSET
- IJ19669 |使用 JAVA 8 读取空 PKCS12 密钥库时发生 NPE
- IJ20528 |对原始包装类型对象的方法调用的 NPE 或段错误
- IJ23079 |错误的 JIT 编译器优化导致意外的 NPE
- TODO:要在更新中发布的结果
结论
这是怎么回事?
Liberty 使用的 JVM 的 IBM 实现是否滥用了这段代码,还是有其他原因?
根据代码和演练,HttpUriRequest.getURI()
不可能 returning null
.
主要更新
根据调查中的第 5 步,我使用来自全球发行版的源代码自定义 httpclient
调试日志记录。将 JAR 添加到我的项目后,它开始工作了。我回滚了我的更改,从未修改的源重建了 JAR,将它添加到我的项目中,应用程序开始工作了。
我使用全球分布的 httpclient-4.5.12.jar
重新部署 我的应用程序失败了。
同样,我没有对代码进行任何更改;我获取了 httpclient-4.5.12-sources.jar
,在 Eclipse 中编译它并使用我新命名的 JAR httpclient-4.5.12.joshdm.jar
重新部署。并且有效。
我使用 JAD 反编译了两个 JAR 以检查代码。差异很小,仅在标签上,这意味着编译器不同。我比较了两个 JAR 的 MANIFEST.MF; Build-Jdk 条目不同。我使用 Jdk 1.8.0_191 编译。全局的是使用 Jdk 1.8.0_181.
编译的我已向 IBM 报告了这种奇怪的差异,同时提供了编译后的 JAR 和源 JAR,并且正在等待回复,以了解为什么我的可以使用他们的 JVM 而全局的不能。我现在 99% 确定这是一个 IBM JVM 或服务器配置问题,与部署的应用程序无关。
更新 #2
添加以下配置并回滚到全局 JAR 有效(也适用于重新编译的 JAR)。
-Xshareclasses:none
- 禁用共享 class 缓存和 AOT 编译。
-Xnojit
- 禁用 JIT 编译
禁用共享 class 缓存、禁用 AOT 编译和禁用 Liberty 上的 JIT 编译似乎已经解决了这个问题。一旦我从 IBM 那里收到为什么需要上述设置的回复,我将写下一个实际的答案。
IBM Liberty 运行 版本 ibm-java-sdk-8.0-5.40
将它更新为 ibm-java-sdk-8.0-6.11
解决了它。
IBM 回应,“自 Java 8 SR5 FP40 {...}以来修复了几个 JIT 问题{...} {i}很可能您没有命中工作环境中相同的代码路径。"