6

我正在测试HttpClientJava 11 的新功能,并遇到了以下行为:

我正在向公共 REST API 发出两个异步请求以进行测试,并使用一个客户端和两个单独的请求进行了尝试。这个过程没有抛出任何异常。

String singleCommentUrl = "https://jsonplaceholder.typicode.com/comments/1";
String commentsUrl = "https://jsonplaceholder.typicode.com/comments";

Consumer<String> handleOneComment = s -> {
    Gson gson = new Gson();
    Comment comment = gson.fromJson(s, Comment.class);
    System.out.println(comment);
};
Consumer<String> handleListOfComments = s -> {
    Gson gson = new Gson();
    Comment[] comments = gson.fromJson(s, Comment[].class);
    List<Comment> commentList = Arrays.asList(comments);
    commentList.forEach(System.out::println);
};

HttpClient client = HttpClient.newBuilder().build();

client.sendAsync(HttpRequest.newBuilder(URI.create(singleCommentUrl)).build(), HttpResponse.BodyHandlers.ofString())
        .thenApply(HttpResponse::body)
        .thenAccept(handleOneComment)
        .join();

client.sendAsync(HttpRequest.newBuilder(URI.create(commentsUrl)).build(), HttpResponse.BodyHandlers.ofString())
        .thenApply(HttpResponse::body)
        .thenAccept(handleListOfComments)
        .join();

然后我尝试将其重构HttpClient为一个方法,当它尝试发出第二个请求时出现以下异常:

public void run() {
    String singleCommentUrl = "https://jsonplaceholder.typicode.com/comments/1";
    String commentsUrl = "https://jsonplaceholder.typicode.com/comments";

    Consumer<String> handleOneComment = s -> {
        Gson gson = new Gson();
        Comment comment = gson.fromJson(s, Comment.class);
        System.out.println(comment);
    };
    Consumer<String> handleListOfComments = s -> {
        Gson gson = new Gson();
        Comment[] comments = gson.fromJson(s, Comment[].class);
        List<Comment> commentList = Arrays.asList(comments);
        commentList.forEach(System.out::println);
    };

    sendRequest(handleOneComment, singleCommentUrl);
    sendRequest(handleListOfComments, commentsUrl);
}

private void sendRequest(Consumer<String> onSucces, String url) {
    HttpClient client = HttpClient.newBuilder().build();
    HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();

    client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
            .thenApply(HttpResponse::body)
            .thenAccept(onSucces)
            .join();
}

在成功执行第一个请求并在第二个请求失败后,这会产生以下异常:

Exception in thread "main" java.util.concurrent.CompletionException: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
    at java.base/java.util.concurrent.CompletableFuture.encodeRelay(CompletableFuture.java:367)
    at java.base/java.util.concurrent.CompletableFuture.completeRelay(CompletableFuture.java:376)
    at java.base/java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1074)
    at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
    at java.base/java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:2088)
    at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate.handleError(SSLFlowDelegate.java:904)
    at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader.processData(SSLFlowDelegate.java:450)
    at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader$ReaderDownstreamPusher.run(SSLFlowDelegate.java:263)
    at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SynchronizedRestartableTask.run(SequentialScheduler.java:175)
    at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:147)
    at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:198)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
    at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:128)
    at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:117)
    at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:308)
    at java.base/sun.security.ssl.Alert$AlertConsumer.consume(Alert.java:279)
    at java.base/sun.security.ssl.TransportContext.dispatch(TransportContext.java:181)
    at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:164)
    at java.base/sun.security.ssl.SSLEngineImpl.decode(SSLEngineImpl.java:672)
    at java.base/sun.security.ssl.SSLEngineImpl.readRecord(SSLEngineImpl.java:627)
    at java.base/sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:443)
    at java.base/sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:422)
    at java.base/javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:634)
    at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader.unwrapBuffer(SSLFlowDelegate.java:480)
    at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader.processData(SSLFlowDelegate.java:389)
    ... 7 more

我也尝试通过方法中的参数传递单独的客户端和请求,但它产生了相同的结果。这里发生了什么?

4

1 回答 1

6

显然,SSLContext对象不是线程安全的。(假设合约没有明确保证线程安全的任何可变对象不是线程安全的,通常是正确的。)

如果没有明确给出上下文, HttpClients使用默认的 SSLContext 。因此,您的两个请求似乎正在尝试同时共享该默认上下文。

解决方案是为每个 HttpClient 指定一个全新的 SSLContext:

private void sendRequest(Consumer<String> onSucces, String url) {
    SSLContext context;
    try {
        context = SSLContext.getInstance("TLSv1.3");
        context.init(null, null, null);
    } catch (GeneralSecurityException e) {
        throw new RuntimeException(e);
    }

    HttpClient client = HttpClient.newBuilder().sslContext(context).build();
    HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();

    client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
            .thenApply(HttpResponse::body)
            .thenAccept(onSucces)
            .join();
}
于 2018-12-13T18:04:22.283 回答