不能在 java.net.http.HttpClient 上发出多个请求,否则将收到:javax.net.ssl.SSLHandshakeException

Posted

技术标签:

【中文标题】不能在 java.net.http.HttpClient 上发出多个请求,否则将收到:javax.net.ssl.SSLHandshakeException【英文标题】:Can't make more than one request on java.net.http.HttpClient or will receive: javax.net.ssl.SSLHandshakeException 【发布时间】:2019-05-14 21:34:37 【问题描述】:

我正在测试 Java 11 中的新 HttpClient 并遇到以下行为:

我正在向公共 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

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

【问题讨论】:

您的应用程序无法保留单个 HttpClient 实例并将其用于所有请求是否有原因? (至少,所有需要相同 HttpClient 设置的请求。) 并非如此,这将是理想的行为。我刚刚从 java 11 测试了新的 HttpClient 并遇到了这个问题。如果它是一个实际的应用程序,我很可能会使用单个 HttpClient 作为服务。 【参考方案1】:

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

HttpClients use the default 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();

【讨论】:

您是否在线程安全上下文中测试了init,然后在并发上下文中使用客户端?可能只有init 方法不安全?当多个线程JDK-8197807 使用时,我遇到了与不安​​全init 相关的问题。 @NicolasHenneaux 我不确定你的意思。在上面的代码中,init 的使用是线程安全的,因为 SSLContext 对任何其他方法或线程都不可见。您是在谈论在默认 SSLContext 上调用 init 吗? 像问题中那样调用两次sendAsync,看看它是否有效? @NicolasHenneaux 当我尝试它时它似乎工作。这是我所期望的,因为我希望 HttpClient 读取 SSLContext 的状态但不修改它。 我期待着同样的结果,但你能够确认真是太好了!

以上是关于不能在 java.net.http.HttpClient 上发出多个请求,否则将收到:javax.net.ssl.SSLHandshakeException的主要内容,如果未能解决你的问题,请参考以下文章

政府要求在隐私弹窗允许之前,不能联网,且不能读隐私信息

政府要求在隐私弹窗允许之前,不能联网,且不能读隐私信息

属性不能被标记为@NSManaged,因为它的类型不能在 Objective-C 中表示

致命异常:NSInternalInconsistencyException 此请求已被绝育 - 您不能调用 -sendResponse: 两次,也不能在编码后调用

为啥不能在 Java 中将类声明为静态?

如果构造函数在私有部分,为啥我们不能创建对象?