从java11开始,JDK在java.net.httpHttpClient、HttpRequest和HttpResponse作为其主要类型。它是一个流畅、易于使用的API,完全支持HTTP/2,允许您异步处理响应,甚至可以以响应方式发送和接收主体。在这篇文章中,我将向您介绍新的API,并向您展示如何发送同步和异步请求。
简而言之,发送请求和接收响应遵循以下步骤:
- 使用构建器创建一个不可变的、可重用的HttpClient
- 使用构建器创建一个不可变的、可重用的HttpRequest
- 将请求传递给客户机以接收HttpResponse
您可以在任何地方配置客户机和请求,保留它们,并重用它们,而不必担心不同请求或线程之间的负面交互。尽管我最近一直在说构建器模式的坏话,但我认为这是一个很好的用例。
让我们一个接一个完成这些步骤。
配置一个HTTP Client
要创建HttpClient,只需调用HttpClient.newBuilder(),提前配置,然后使用build()完成:
HttpClient client = HttpClient.newBuilder()
// just to show off; HTTP/2 is the default
.version(HTTP_2)
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(SECURE)
.build();
除了HTTP版本、连接超时和重定向策略外,还可以配置代理、SSL上下文和参数、验证器和cookie处理程序。还有一个executor方法,但我稍后再处理。
所以我觉得只要一提到不可变线程,我就可以自由配置,这样我就可以随时随地使用了。
配置一个HTTP Request
HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("http://www.55mianshi.com"))
.header("Accept-Language", "en-US,en;q=0.5")
.build();
您不必在uri(uri)中设置URL,而可以直接将其传递给newBuilder(uri)。我想我更喜欢这样,因为你可以很好地把它读成“得到”codefx.org网站".
对于header(String,String),您可以向请求的头添加一个名称/值对。如果要覆盖标头名称的现有值,请使用setHeader。如果您有许多标题条目,并且不想重复整个页眉,请尝试使用headers(String…),您可以在名称和值之间进行选择:
HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("http://www.55mianshi.com"))
.headers(
"Accept-Language", "en-US,en;q=0.5",
"Accept-Encoding", "gzip, deflate, br")
.build();
除了头和更多的HTTP方法(PUT、POST和泛型方法),您可以在发送请求体(如果有)之前请求“100continue”,并覆盖客户端的首选HTTP版本和超时。
BodyPublisher requestBody = BodyPublishers
.ofString("{ request body }");
HttpRequest request = HttpRequest.newBuilder()
.POST(requestBody)
.uri(URI.create("http://www.55mianshi.com"))
.build();
你问BodyPublisher怎么了?(这与反应性地处理请求体有关,记住,我将在下一篇文章中介绍这一点。现在可以说,您可以从BodyPublishers获取它的实例—根据body的形式,可以对其调用以下(以及其他一些)静态方法:
- ofByteArray(byte[])
- ofFile(Path)
- ofString(String)
- ofInputStream(Supplier<InputStream>)
将返回的BodyPublisher传递给请求生成器的PUT、POST或方法,您就成功了。
获取Http响应
接收HttpResponse就像打电话一样简单HttpClient.send(...). 好吧,差不多了。您还必须提供一个所谓的BodyHandler<T>,它负责处理接收到的响应字节,并将它们转换为更有用的内容。就像BodyPublisher一样,我稍后再谈这个。
现在我就用保镖手(),这意味着传入字节将被解释为单个字符串。这将响应的泛型类型定义为字符串:
HttpResponse<String> response = client.send(
request,
BodyHandlers.ofString());
// `HttpResponse<T>.body()` returns a `T`
String respnseBody = response.body();
除了主体之外,响应还包含状态代码、头、SSL会话、对请求的引用以及处理重定向或身份验证的中间响应。
同步http请求
让我们把所有的东西放在一起,搜索维基百科中最长的十篇文章。由于即将进行的实验都使用相同的url和搜索词,并且还可以重用相同的客户端,因此我们可以在静态字段中声明它们:
private static final HttpClient CLIENT = HttpClient.newBuilder().build();
private static final List<URI> URLS = Stream.of(
"https://en.wikipedia.org/wiki/List_of_compositions_by_Franz_Schubert",
"https://en.wikipedia.org/wiki/2018_in_American_television",
"https://en.wikipedia.org/wiki/List_of_compositions_by_Johann_Sebastian_Bach",
"https://en.wikipedia.org/wiki/List_of_Australian_treaties",
"https://en.wikipedia.org/wiki/2016%E2%80%9317_Coupe_de_France_Preliminary_Rounds",
"https://en.wikipedia.org/wiki/Timeline_of_the_war_in_Donbass_(April%E2%80%93June_2018)",
"https://en.wikipedia.org/wiki/List_of_giant_squid_specimens_and_sightings",
"https://en.wikipedia.org/wiki/List_of_members_of_the_Lok_Sabha_(1952%E2%80%93present)",
"https://en.wikipedia.org/wiki/1919_New_Year_Honours",
"https://en.wikipedia.org/wiki/List_of_International_Organization_for_Standardization_standards"
).map(URI::create).collect(toList());
private static final String SEARCH_TERM = "Foo";
有了HTTP客户端、URL和搜索项,我们就可以构建请求(每个URL一个),发送它们,等待响应返回,然后检查搜索项的主体:
static void blockingSearch() {
URLS.forEach(url -> {
boolean found = blockingSearch(CLIENT, url, SEARCH_TERM);
System.out.println(
"Completed " + url + " / found: " + found);
});
}
static boolean blockingSearch(
HttpClient client, URI url, String term) {
try {
HttpRequest request = HttpRequest
.newBuilder(url).GET().build();
HttpResponse<String> response = client.send(
request, BodyHandlers.ofString());
return response.body().contains(term);
} catch (IOException | InterruptedException ex) {
// to my colleagues: I copy-pasted this code
// snippet from a blog post and didn't fix the
// horrible exception handling - punch me!
return false;
}
}
根据我的网络连接情况,运行该程序需要2到4秒。
很好,很好,但是反应的部分呢?!以上天真的实现对10个请求的每一个进行阻塞,浪费了宝贵的时间和资源!代码可以在三个位置更改为非阻塞:
异步发送请求
提供请求正文作为反应流
作为反应流的过程响应体
我要在这里解释第一个,其他两个留待以后再说。
异步http请求
使调用非阻塞的最直接的方法是异步发送它们,HttpClient有一个方法用于此: sendAync 发送请求并立即返回CompletableFuture<HttpResponse<T>>。
默认情况下,请求由JVM内部深处的executor服务处理,但是如果调用HttpClient.Builder·executor在构建客户端时,可以为这些调用定义一个自定义的executor。无论哪个执行器负责请求/响应,都可以使用线程继续处理更重要的内容。例如,请求下九个维基百科页面。
不过,不是那么快,首先我们需要向CompletableFuture追加一些计算,因此当请求返回时,我们看到预期的输出:
static CompletableFuture<Void> asyncSearch(
HttpClient client, URI url, String term) {
HttpRequest request = HttpRequest
.newBuilder(url).GET().build();
return client
.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenApply(body -> body.contains(term))
.exceptionally(__ -> false)
.thenAccept(found ->
System.out.println(
"Completed " + url + " / found: " + found));
}
如前所述,HttpClient::sendAync 返回一个CompletableFuture<HttpResponse<T>>并最终完成响应。(如果您不太了解CompletableFuture API,请将apply想象为Optional::map,然后将accept视为Optional::ifPresent。对于解释和更多的处理选项,请检查JavaDoc For CompletableFuture。)然后提取请求主体(一个字符串),检查它是否包含搜索项(因此转换为布尔值),最后将其打印到标准输出。我们使用异常来映射处理请求或响应“not found”结果时可能发生的任何错误。
注意,accept返回CompletableFuture<Void>:
- 它是 Void ,因为我们应该在指定的使用者中完成对内容的处理
- 它仍然是一个 CompletableFuture,所以我们可以等待它完成
因为,在这个演示中,这就是我们最终需要做的。运行我们请求的线程是守护进程线程,这意味着它们不会使我们的程序保持活动状态。如果main发送了10个异步请求而不等待它们完成,那么程序在这10个请求发送之后立即结束,我们永远看不到任何结果。因此
static void asyncSearch() {
CompletableFuture[] futures = URLS.stream()
.map(url -> asyncSearch(CLIENT, url, SEARCH_TERM))
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures).join();
}
这通常需要阻塞方法的75%的时间,我不得不承认,我发现速度非常慢。不过,这不是一个基准,所以没关系。主要的事实是,在后台发送请求和接收响应时,我们的线程可以自由地做其他事情。
异步处理请求/响应生命周期非常简单,但是它仍然有一个(潜在的)缺点:请求和响应的主体都必须在一个部分中处理。
总结:
发送请求需要两种成分:
- 与HttpClient.newBuilder().$configure().build()您将获得一个不可变且可重用的HttpClient。您可以$configure首选HTTP版本、超时、代理、cookie处理程序、异步请求的执行器等。
- 与HttpRequest.newBuilder().$configure().build()您将获得一个不可变且可重用的HttpRequest。您可以覆盖客户端的HTTP版本、超时等。如果请求有一个body,则将其作为BodyPublisher提供;您将主要使用BodyPublisher上的factory方法。
有了HttpClient和HttpResponse,就可以在前者上调用send或 sendAync 。你还必须提供一个BodyHandler,你可以从BodyHandlers得到它-它负责将响应字节转换为更容易接受的内容。
如果使用send,方法调用将阻塞,直到响应完成,然后返回HttpResponse<T>。如果调用 sendAync ,则调用将立即返回CompletableFuture<HttpResponse<T>>,然后您可以将进一步的处理步骤链接到。