面试知识点总结
1. 一篇文章说清 netty 的线程模型 2. 一篇说尽 java 线程池 3. 一篇就够了系列 - LinkedHashMap 4. 使用LinkedHashMap构建LRU的Cache 5. 并发编程之-Excutor框架 6. Java工具类提供的排序功能 7. Java的众多log库都是什么关系? 8. Java常用日志框架历史 9. 网页特殊符号(HTML字符实体)大全 10. JavaFX项目打包为独立的macOS应用程序和dmg文件 11. Java 11 支持的 基于 HTTP/2 的响应式请求 12. Java 11 中 HttpClient 的使用(HTTP/2协议) 13. Java 11 模块化入门教程 14. 五五面试网-带你 理解 java 模块系统 (一) 15. 五五面试网-带你 理解 java 模块系统 (二) 16. 五五面试网-带你 理解 java 模块系统 (三) 手动新建一个java模块 17. Java中的TreeMap 18. gradle:现代高效的java构建工具 19. Spring + MyBatis 框架下处理数据库异常 20. 通过开源项目,免费获取Idea的开源授权 21. IntelliJ IDEA 2020最新激活码(亲测有效,可激活至 2089 年)

Java 11 中 HttpClient 的使用(HTTP/2协议)

从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版本和超时。

如果只发送GET以外的任何内容,则在配置HTTP方法时需要包含BodyPublisher:

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>>,然后您可以将进一步的处理步骤链接到。