Java11新特性之HttpClient 使用

JDK變化

  1. 從java9的jdk.incubator.httpclient模塊遷移到java.net.http模塊,包名由jdk.incubator.http改爲java.net.http
  2. 原來的諸如HttpResponse.BodyHandler.asString()方法變更爲HttpResponse.BodyHandlers.ofString(),變化一爲BodyHandler改爲BodyHandlers,變化二爲asXXX()之類的方法改爲ofXXX(),由as改爲of
  3. 官方文檔:https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/package-summary.html

實例

設置超時時間

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

@Test

public void testTimeout() throws IOException, InterruptedException {

 //1.set connect timeout

 HttpClient client = HttpClient.newBuilder()

   .connectTimeout(Duration.ofMillis(5000))

   .followRedirects(HttpClient.Redirect.NORMAL)

   .build();

 

 //2.set read timeout

 HttpRequest request = HttpRequest.newBuilder()

   .uri(URI.create("http://openjdk.java.net/"))

   .timeout(Duration.ofMillis(5009))

   .build();

 

 HttpResponse<String> response =

   client.send(request, HttpResponse.BodyHandlers.ofString());

 

 System.out.println(response.body());

 

}

HttpConnectTimeoutException實例

1

2

3

4

5

6

7

Caused by: java.net.http.HttpConnectTimeoutException: HTTP connect timed out

 at java.net.http/jdk.internal.net.http.ResponseTimerEvent.handle(ResponseTimerEvent.java:68)

 at java.net.http/jdk.internal.net.http.HttpClientImpl.purgeTimeoutsAndReturnNextDeadline(HttpClientImpl.java:1248)

 at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.run(HttpClientImpl.java:877)

Caused by: java.net.ConnectException: HTTP connect timed out

 at java.net.http/jdk.internal.net.http.ResponseTimerEvent.handle(ResponseTimerEvent.java:69)

 ... 2 more

HttpTimeoutException實例

1

2

3

4

5

java.net.http.HttpTimeoutException: request timed out

 

 at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:559)

 at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:119)

 at com.example.HttpClientTest.testTimeout(HttpClientTest.java:40)

設置authenticator

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

@Test

public void testBasicAuth() throws IOException, InterruptedException {

 HttpClient client = HttpClient.newBuilder()

   .connectTimeout(Duration.ofMillis(5000))

   .authenticator(new Authenticator() {

    @Override

    protected PasswordAuthentication getPasswordAuthentication() {

     return new PasswordAuthentication("admin","password".toCharArray());

    }

   })

   .build();

 

 HttpRequest request = HttpRequest.newBuilder()

   .uri(URI.create("http://localhost:8080/json/info"))

   .timeout(Duration.ofMillis(5009))

   .build();

 

 HttpResponse<String> response =

   client.send(request, HttpResponse.BodyHandlers.ofString());

 

 System.out.println(response.statusCode());

 System.out.println(response.body());

}

  1. authenticator可以用來設置HTTP authentication,比如Basic authentication
  2. 雖然Basic authentication也可以自己設置header,不過通過authenticator省得自己去構造header

設置header

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

@Test

public void testCookies() throws IOException, InterruptedException {

 HttpClient client = HttpClient.newBuilder()

   .connectTimeout(Duration.ofMillis(5000))

   .build();

 HttpRequest request = HttpRequest.newBuilder()

   .uri(URI.create("http://localhost:8080/json/cookie"))

   .header("Cookie","JSESSIONID=4f994730-32d7-4e22-a18b-25667ddeb636; userId=java11")

   .timeout(Duration.ofMillis(5009))

   .build();

 HttpResponse<String> response =

   client.send(request, HttpResponse.BodyHandlers.ofString());

 

 System.out.println(response.statusCode());

 System.out.println(response.body());

}

通過request可以自己設置header,多個header可直接傳入可變參數headers(String數組),或者傳入傳統的Map型的header直接流轉換爲headers

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

@Test

public void testRequestWithHeaders(Map<String,String> headers) throws IOException, InterruptedException {

 HttpClient client = HttpClient.newBuilder()

   .connectTimeout(Duration.ofMillis(5000))

   .build();

 HttpRequest request = HttpRequest.newBuilder()

   .uri(URI.create("http://localhost:8080/json/cookie"))

   .headers(headers.entrySet().stream().flatMap(e->Stream.of(e.getKey(),e.getValue())).collect(Collectors.toList()).stream().toArray(String[]::new))

   .timeout(Duration.ofMillis(5009))

   .build();

 HttpResponse<String> response =

   client.send(request, HttpResponse.BodyHandlers.ofString());

 

 System.out.println(response.statusCode());

 System.out.println(response.body());

}

 

 

 

GET

同步

1

2

3

4

5

6

7

8

9

10

11

12

@Test

public void testSyncGet() throws IOException, InterruptedException {

 HttpClient client = HttpClient.newHttpClient();

 HttpRequest request = HttpRequest.newBuilder()

   .uri(URI.create("https://www.baidu.com"))

   .build();

 

 HttpResponse<String> response =

   client.send(request, HttpResponse.BodyHandlers.ofString());

 

 System.out.println(response.body());

}

異步

 

1

2

3

4

5

6

7

8

9

10

11

@Test

public void testAsyncGet() throws ExecutionException, InterruptedException {

 HttpClient client = HttpClient.newHttpClient();

 HttpRequest request = HttpRequest.newBuilder()

   .uri(URI.create("https://www.baidu.com"))

   .build();

 

 CompletableFuture<String> result = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())

   .thenApply(HttpResponse::body);

 System.out.println(result.get());

}

POST表單

1

2

3

4

5

6

7

8

9

10

11

12

@Test

public void testPostForm() throws IOException, InterruptedException {

 HttpClient client = HttpClient.newBuilder().build();

 HttpRequest request = HttpRequest.newBuilder()

   .uri(URI.create("http://www.w3school.com.cn/demo/demo_form.asp"))

   .header("Content-Type","application/x-www-form-urlencoded")

   .POST(HttpRequest.BodyPublishers.ofString("name1=value1&name2=value2"))

   .build();

 

 HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

 System.out.println(response.statusCode());

}

header指定內容是表單類型,然後通過BodyPublishers.ofString傳遞表單數據,需要自己構建表單參數

POST JSON

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

@Test

public void testPostJsonGetJson() throws ExecutionException, InterruptedException, JsonProcessingException {

 ObjectMapper objectMapper = new ObjectMapper();

 StockDto dto = new StockDto();

 dto.setName("hj");

 dto.setSymbol("hj");

 dto.setType(StockDto.StockType.SH);

 String requestBody = objectMapper

   .writerWithDefaultPrettyPrinter()

   .writeValueAsString(dto);

 

 HttpRequest request = HttpRequest.newBuilder(URI.create("http://localhost:8080/json/demo"))

   .header("Content-Type", "application/json")

   .POST(HttpRequest.BodyPublishers.ofString(requestBody))

   .build();

 

 CompletableFuture<StockDto> result = HttpClient.newHttpClient()

   .sendAsync(request, HttpResponse.BodyHandlers.ofString())

   .thenApply(HttpResponse::body)

   .thenApply(body -> {

    try {

     return objectMapper.readValue(body,StockDto.class);

    } catch (IOException e) {

     return new StockDto();

    }

   });

 System.out.println(result.get());

}

post json的話,body自己json化爲string,然後header指定是json格式

文件上傳

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

@Test

public void testUploadFile() throws IOException, InterruptedException, URISyntaxException {

 HttpClient client = HttpClient.newHttpClient();

 Path path = Path.of(getClass().getClassLoader().getResource("body.txt").toURI());

 File file = path.toFile();

 

 String multipartFormDataBoundary = "Java11HttpClientFormBoundary";

 org.apache.http.HttpEntity multipartEntity = MultipartEntityBuilder.create()

   .addPart("file", new FileBody(file, ContentType.DEFAULT_BINARY))

   .setBoundary(multipartFormDataBoundary) //要設置,否則阻塞

   .build();

 

 HttpRequest request = HttpRequest.newBuilder()

   .uri(URI.create("http://localhost:8080/file/upload"))

   .header("Content-Type", "multipart/form-data; boundary=" + multipartFormDataBoundary)

   .POST(HttpRequest.BodyPublishers.ofInputStream(() -> {

    try {

     return multipartEntity.getContent();

    } catch (IOException e) {

     e.printStackTrace();

     throw new RuntimeException(e);

    }

   }))

   .build();

 

 HttpResponse<String> response =

   client.send(request, HttpResponse.BodyHandlers.ofString());

 

 System.out.println(response.body());

}

  1. 官方的HttpClient並沒有提供類似WebClient那種現成的BodyInserters.fromMultipartData方法,因此這裏需要自己轉換
  2. 這裏使用org.apache.httpcomponents(httpclient及httpmime)的MultipartEntityBuilder構建multipartEntity,最後通過HttpRequest.BodyPublishers.ofInputStream來傳遞內容
  3. 這裏header要指定Content-Type值爲multipart/form-data以及boundary的值,否則服務端可能無法解析

文件下載

1

2

3

4

5

6

7

8

9

10

11

@Test

public void testAsyncDownload() throws ExecutionException, InterruptedException {

 HttpClient client = HttpClient.newHttpClient();

 HttpRequest request = HttpRequest.newBuilder()

   .uri(URI.create("http://localhost:8080/file/download"))

   .build();

 

 CompletableFuture<Path> result = client.sendAsync(request, HttpResponse.BodyHandlers.ofFile(Paths.get("/tmp/body.txt")))

   .thenApply(HttpResponse::body);

 System.out.println(result.get());

}

使用HttpResponse.BodyHandlers.ofFile來接收文件

併發請求

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

@Test

public void testConcurrentRequests(){

 HttpClient client = HttpClient.newHttpClient();

 List<String> urls = List.of("http://www.baidu.com","http://www.alibaba.com/","http://www.tencent.com");

 List<HttpRequest> requests = urls.stream()

   .map(url -> HttpRequest.newBuilder(URI.create(url)))

   .map(reqBuilder -> reqBuilder.build())

   .collect(Collectors.toList());

 

 List<CompletableFuture<HttpResponse<String>>> futures = requests.stream()

   .map(request -> client.sendAsync(request, HttpResponse.BodyHandlers.ofString()))

   .collect(Collectors.toList());

 futures.stream()

   .forEach(e -> e.whenComplete((resp,err) -> {

    if(err != null){

     err.printStackTrace();

    }else{

     System.out.println(resp.body());

     System.out.println(resp.statusCode());

    }

   }));

 CompletableFuture.allOf(futures

   .toArray(CompletableFuture<?>[]::new))

   .join();

}

  • sendAsync方法返回的是CompletableFuture,可以方便地進行轉換、組合等操作
  • 這裏使用CompletableFuture.allOf組合在一起,最後調用join等待所有future完成

錯誤處理

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

@Test

 public void testHandleException() throws ExecutionException, InterruptedException {

  HttpClient client = HttpClient.newBuilder()

    .connectTimeout(Duration.ofMillis(5000))

    .build();

  HttpRequest request = HttpRequest.newBuilder()

    .uri(URI.create("https://twitter.com"))

    .build();

 

  CompletableFuture<String> result = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())

//    .whenComplete((resp,err) -> {

//     if(err != null){

//      err.printStackTrace();

//     }else{

//      System.out.println(resp.body());

//      System.out.println(resp.statusCode());

//     }

//    })

    .thenApply(HttpResponse::body)

    .exceptionally(err -> {

     err.printStackTrace();

     return "fallback";

    });

  System.out.println(result.get());

 }

  • HttpClient異步請求返回的是CompletableFuture<HttpResponse<T>>,其自帶exceptionally方法可以用來做fallback處理
  • 另外值得注意的是HttpClient不像WebClient那樣,它沒有對4xx或5xx的狀態碼拋出異常,需要自己根據情況來處理,手動檢測狀態碼拋出異常或者返回其他內容

HTTP2

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

@Test

public void testHttp2() throws URISyntaxException {

 HttpClient.newBuilder()

   .followRedirects(HttpClient.Redirect.NEVER)

   .version(HttpClient.Version.HTTP_2)

   .build()

   .sendAsync(HttpRequest.newBuilder()

       .uri(new URI("https://http2.akamai.com/demo"))

       .GET()

       .build(),

     HttpResponse.BodyHandlers.ofString())

   .whenComplete((resp,t) -> {

    if(t != null){

     t.printStackTrace();

    }else{

     System.out.println(resp.version());

     System.out.println(resp.statusCode());

    }

   }).join();

}

執行之後可以看到返回的response的version爲HTTP_2

WebSocket

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

@Test

public void testWebSocket() throws InterruptedException {

 HttpClient client = HttpClient.newHttpClient();

 WebSocket webSocket = client.newWebSocketBuilder()

   .buildAsync(URI.create("ws://localhost:8080/echo"), new WebSocket.Listener() {

 

    @Override

    public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {

     // request one more

     webSocket.request(1);

 

     // Print the message when it's available

     return CompletableFuture.completedFuture(data)

       .thenAccept(System.out::println);

    }

   }).join();

 webSocket.sendText("hello ", false);

 webSocket.sendText("world ",true);

 

 TimeUnit.SECONDS.sleep(10);

 webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok").join();

}

  • HttpClient支持HTTP2,也包含了WebSocket,通過newWebSocketBuilder去構造WebSocket
  • 傳入listener進行接收消息,要發消息的話,使用WebSocket來發送,關閉使用sendClose方法

reactive streams

HttpClient本身就是reactive的,支持reactive streams,這裏舉ResponseSubscribers.ByteArraySubscriber的源碼看看:
java.net.http/jdk/internal/net/http/ResponseSubscribers.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

public static class ByteArraySubscriber<T> implements BodySubscriber<T> {

  private final Function<byte[], T> finisher;

  private final CompletableFuture<T> result = new MinimalFuture<>();

  private final List<ByteBuffer> received = new ArrayList<>();

 

  private volatile Flow.Subscription subscription;

 

  public ByteArraySubscriber(Function<byte[],T> finisher) {

   this.finisher = finisher;

  }

 

  @Override

  public void onSubscribe(Flow.Subscription subscription) {

   if (this.subscription != null) {

    subscription.cancel();

    return;

   }

   this.subscription = subscription;

   // We can handle whatever you've got

   subscription.request(Long.MAX_VALUE);

  }

 

  @Override

  public void onNext(List<ByteBuffer> items) {

   // incoming buffers are allocated by http client internally,

   // and won't be used anywhere except this place.

   // So it's free simply to store them for further processing.

   assert Utils.hasRemaining(items);

   received.addAll(items);

  }

 

  @Override

  public void onError(Throwable throwable) {

   received.clear();

   result.completeExceptionally(throwable);

  }

 

  static private byte[] join(List<ByteBuffer> bytes) {

   int size = Utils.remaining(bytes, Integer.MAX_VALUE);

   byte[] res = new byte[size];

   int from = 0;

   for (ByteBuffer b : bytes) {

    int l = b.remaining();

    b.get(res, from, l);

    from += l;

   }

   return res;

  }

 

  @Override

  public void onComplete() {

   try {

    result.complete(finisher.apply(join(received)));

    received.clear();

   } catch (IllegalArgumentException e) {

    result.completeExceptionally(e);

   }

  }

 

  @Override

  public CompletionStage<T> getBody() {

   return result;

  }

 }

  1. BodySubscriber接口繼承了Flow.Subscriber<List<ByteBuffer>>接口
  2. 這裏的Subscription來自Flow類,該類是java9引入的,裏頭包含了支持Reactive Streams的實現

小結

HttpClient在Java11從incubator變爲正式版,相對於傳統的HttpUrlConnection其提升可不是一點半點,不僅支持異步,也支持reactive streams,同時也支持了HTTP2以及WebSocket,非常值得大家使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章