不能在 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
我正在测试 Java 11 的新 HttpClient
并遇到以下行为:
我正在向 public 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
我也尝试通过方法中的参数传递单独的客户端和请求,但它产生了相同的结果。这是怎么回事?
显然,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();
}
我正在测试 Java 11 的新 HttpClient
并遇到以下行为:
我正在向 public 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
我也尝试通过方法中的参数传递单独的客户端和请求,但它产生了相同的结果。这是怎么回事?
显然,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();
}