当 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.

使用的相关库有

我们的内部应用程序调用第三方代码,该代码使用 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(HttpGetHttpPost 等扩展)

    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?

环境

观察结果

调查

以下是我目前采取的一些调查步骤:

1 - 我在 SO 上创建了这个案例以获取群体思维洞察力。

2 - 我在 IBM 开了一个案例。

3 - 自定义 ApacheHttpClient4Handler

结果

4 - 进一步自定义 ApacheHttpClient4Handler

结果

5 - 自定义 HttpRequestBase

结果

6 - 自定义 HttpGetHttpPut

结果

7 - 将 IBM JVM 从 8.0-5.40 更新到 8.0-6.11

结论

这是怎么回事?

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 解决了它。

Bugfixes are found here

IBM 回应,“自 Java 8 SR5 FP40 {...}以来修复了几个 JIT 问题{...} {i}很可能您没有命中工作环境中相同的代码路径。"

我对罪魁祸首的猜测:IJ20528, IJ23079