1

概括

我遇到了一个独特的场景,当实例中的变量不是时,对 httpcomponentsHttpUriRequest方法实例的调用getURI()正在返回。在这种情况下,不可能返回 a ,但这就是正在发生的事情。nullnullHttpUriRequest.getURI()null

代码演练

这是代码演练。它演示了该HttpUriRequest.getURI()方法如何不可能返回 a null,但它正在返回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.

就本问题而言,请求从ApacheHttpClient4Handler.handle(ClientRequest)以下来源的调用开始。

  • 编号的内嵌注释将指导演练。

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
    }

接口HttpUriRequestv4.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
    }

抽象类HttpRequestBasev4.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;
        }
    }

HttpGetv4.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()返回null

环境

  • 此代码在 Linux 服务器 (JVM 8.0-5.40) 上的 IBM Liberty 下运行。
  • 此代码还在 Windows 服务器 (JVM 1.7) 上的 Wildfly 下运行。
  • 两种系统类型的代码相同。
  • 该代码在 Windows 服务器上从未出现上述问题。
  • 该代码在安装了 Liberty 管理控制台的 Liberty 服务器上不存在上述问题。

观察结果

  • 前2-3次直通ApacheHttpClient4Hander.handle(ClientRequest)成功不request.getURI()返回null;它仅在第 4 次和后续请求时才返回null

调查

以下是我迄今为止采取的一些调查步骤:

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

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

3 - 自定义ApacheHttpClient4Handler

  • 我通过 SL4J 向所有方法添加调试日志记录。

结果

  • 日志输出证明对的调用仅在进入HttpUriRequest.getURI()后才开始返回。nullApacheHttpClient4Handler.getHost(final HttpUriRequest request)
  • getHost(HttpUriRequest)在显示getURI()返回非空之前的任何调用。
  • 在添加调试日志之后,而不是在 2-3 和后续请求(每个 OBSERVATIONS)之后,HttpUriRequest.getURI()现在在 1-2(和后续)请求之后返回 null。我怀疑这是由于 debug logging 主动调用HttpUriRequest.getURI(),但我无法确认这一点,也不知道为什么场景现在更短了。

ApacheHttpClient4Handler4 -进一步定制

  • HttpUriRequest更改对的所有引用HttpRequestBase

结果

  • 这样做并没有解决问题;getURI()仍然返回null

5 - 自定义HttpRequestBase

  • 将调试日志添加到getURI()/setURI(...)方法

结果

  • 应用程序开始按设计工作,除了大量的调试日志!!!
  • 请参阅下面的主要更新部分。

6 - 自定义HttpGet,HttpPut等。

  • 向构造函数添加调试日志记录。

结果

  • 请参阅下面的主要更新部分。

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

  • IBM 发行说明中与 NPE 相关的错误修复列表
  • IJ17563 | 调用具有大偏移量的 JAVA.LANG.STRING.INDEXOF API 时的 NPE
  • IJ19669 | 使用 JAVA 8 读取空的 PKCS12 密钥库时发生 NPE
  • IJ20528 | 原始包装类型对象的方法调用的 NPE 或 SEGFAULT
  • IJ23079 | 由不正确的 JIT 编译器优化导致的意外 NPE
  • TODO:结果将在更新中发布

结论

这里发生了什么?

Liberty 使用的 JVM 的 IBM 实现是否滥用了此代码,还是有其他原因?

根据代码和演练,没有办法HttpUriRequest.getURI()返回null.

重大更新

根据 INVESTIGATION 中的第 5 步,我httpclient使用来自全球发行版的源代码定制了调试日志记录。将 JAR 添加到我的项目后,它开始工作。我回滚了我的更改,从未修改的源代码重建了 JAR,将其添加到我的项目中,然后应用程序开始工作。

我使用全球分布httpclient-4.5.12.jar 的 My application failed 再次重新部署。

同样,我没有对代码进行任何更改;我httpclient-4.5.12-sources.jar使用 .Eclipse 并在 Eclipse 中编译它并使用我新命名的 JAR 重新部署,httpclient-4.5.12.joshdm.jar. 它有效。

我使用 JAD 反编译了两个 JAR 来检查代码。差异很小,仅在标签中,这意味着编译器不同。我比较了两个 JAR 的 MANIFEST.MF;Build-Jdk 条目不同。我使用 Jdk 1.8.0_191 编译。全局的使用 Jdk 1.8.0_1 8 1 编译。

我已经向 IBM 报告了这个奇怪的差异,提供了编译后的 JAR 和源 JAR,我正在等待回复为什么我的 JVM 可以与他们的 JVM 一起工作,以及为什么全局的 JAR 不能。我现在 99% 确定这是 IBM JVM 或服务器配置问题,与部署的应用程序无关。

更新#2

添加以下配置并回滚到全局 JAR 工作(也适用于重新编译的 JAR)。

-Xshareclasses:none- 禁用共享类缓存和 AOT 编译。

-Xnojit- 禁用 JIT 编译

在 Liberty 上禁用共享类缓存、禁用 AOT 编译和禁用 JIT 编译似乎已经解决了这个问题。一旦我从 IBM 收到为什么需要上述设置,我会写一个实际的答案。

4

1 回答 1

0

IBM Liberty 在版本上运行ibm-java-sdk-8.0-5.40

更新它以ibm-java-sdk-8.0-6.11解决它。

在这里可以找到错误修正

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

我对罪魁祸首的猜测:IJ20528IJ23079

于 2020-07-30T19:06:32.870 回答