我正在测试Java 11中的新HttpClient
,并遇到以下行为:
我正在向公共REST API发出两个Async请求以进行测试,并使用一个客户端和两个单独的请求进行了尝试。这个过程没有引发任何异常。
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
我也尝试通过方法中的参数传递单独的客户端和请求,但结果相同。这是怎么回事?
答案 0 :(得分:2)
显然,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();
}