不能在 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: 两次,也不能在编码后调用