JDK11新特性 jkd HttpClient

简介

由于项目中用到了JDK11,所以对JDK11学习了一下,这里分享一下我的学习成果。本文主要介绍JDK11新特性 jkd HttpClient的使用。

变化

  • 从java9的jdk.incubator.httpclient模块迁移到java.net.http模块,包名由jdk.incubator.http改为java.net.http
  • 原来的诸如HttpResponse.BodyHandler.asString()方法变更为HttpResponse.BodyHandlers.ofString(),变化一为BodyHandler改为BodyHandlers,变化二为asXXX()之类的方法改为ofXXX(),由as改为of

 

HttpClient在 Apache HttpClient 中,一般会创建一个 HttpClient 对象来作为门面。java.net.http.HttpClient 的逻辑也差不多,只是创建方式更加时髦了:

//创建 builder
HttpClient.Builder builder = HttpClient.newBuilder();
 
//链式调用
HttpClient client = builder
 
//http 协议版本 1.1 或者 2
.version(HttpClient.Version.HTTP_2) //.version(HttpClient.Version.HTTP_1_1)
 
//连接超时时间,单位为毫秒
.connectTimeout(Duration.ofMillis(5000)) //.connectTimeout(Duration.ofMinutes(1))
 
//连接完成之后的转发策略
.followRedirects(HttpClient.Redirect.NEVER) //.followRedirects(HttpClient.Redirect.ALWAYS)
 
//指定线程池
.executor(Executors.newFixedThreadPool(5))
 
//认证,默认情况下 Authenticator.getDefault() 是 null 值,会报错
//.authenticator(Authenticator.getDefault())
 
//代理地址
//.proxy(ProxySelector.of(new InetSocketAddress("http://www.baidu.com", 8080)))
 
//缓存,默认情况下 CookieHandler.getDefault() 是 null 值,会报错
//.cookieHandler(CookieHandler.getDefault())
 
//创建完成
.build();

在 builder() 方法中,最终会调用到 HttpClientImpl 的构造器,完成 HttpClient 的创建工作:

//HttpClientImpl.class
    private HttpClientImpl(HttpClientBuilderImpl builder,
                           SingleFacadeFactory facadeFactory) {
        //CLIENT_IDS 是 AtomicLong 类型的变量,使用 incrementAndGet() 方法实现自增长的 id
        id = CLIENT_IDS.incrementAndGet();
        //记录下存有 id 的字符串
        dbgTag = "HttpClientImpl(" + id + ")";

        //ssl 认证
        if (builder.sslContext == null) {
            try {
                sslContext = SSLContext.getDefault();
            } catch (NoSuchAlgorithmException ex) {
                throw new InternalError(ex);
            }
        } else {
            sslContext = builder.sslContext;
        }

        //线程池,没有的话就默认创建一个
        Executor ex = builder.executor;
        if (ex == null) {
            ex = Executors.newCachedThreadPool(new DefaultThreadFactory(id));
            isDefaultExecutor = true;
        } else {
            isDefaultExecutor = false;
        }
        delegatingExecutor = new DelegatingExecutor(this::isSelectorThread, ex);
        facadeRef = new WeakReference<>(facadeFactory.createFacade(this));

        //处理 http 2 的 client 类
        client2 = new Http2ClientImpl(this);‘
        //缓存操作
        cookieHandler = builder.cookieHandler;
        //超时时间
        connectTimeout = builder.connectTimeout;
        //转发策略,默认为 NEVER
        followRedirects = builder.followRedirects == null ?
                Redirect.NEVER : builder.followRedirects;
        //代理设置
        this.userProxySelector = Optional.ofNullable(builder.proxy);
        this.proxySelector = userProxySelector
                .orElseGet(HttpClientImpl::getDefaultProxySelector);
        if (debug.on())
            debug.log("proxySelector is %s (user-supplied=%s)",
                    this.proxySelector, userProxySelector.isPresent());
        //认证设置
        authenticator = builder.authenticator;
        //设置 http 协议版本
        if (builder.version == null) {
            version = HttpClient.Version.HTTP_2;
        } else {
            version = builder.version;
        }
        if (builder.sslParams == null) {
            sslParams = getDefaultParams(sslContext);
        } else {
            sslParams = builder.sslParams;
        }
        //连接线程池
        connections = new ConnectionPool(id);
        connections.start();
        timeouts = new TreeSet<>();

        //SelectorManager 本质上是 Thread 类的封装
        //selmgr 会开启一条线程,HttpClient 的主要逻辑运行在此线程中
        //所以说 HttpClient 是非阻塞的,因为并不跑在主线程中
        try {
            selmgr = new SelectorManager(this);
        } catch (IOException e) {
            throw new InternalError(e);
        }
        //设置为守护线程
        selmgr.setDaemon(true);
        filters = new FilterFactory();
        initFilters();
        assert facadeRef.get() != null;
    }

主要是一些储存操作,大致理解即可,不细究。

HttpRequest

 HttpRequest 是发起请求的主体配置:

//创建 builder
HttpRequest.Builder reBuilder = HttpRequest.newBuilder();
 
//链式调用
HttpRequest request = reBuilder
 
//存入消息头
//消息头是保存在一张 TreeMap 里的
.header("Content-Type", "application/json")
 
//http 协议版本
.version(HttpClient.Version.HTTP_2)
 
//url 地址
.uri(URI.create("http://openjdk.java.net/"))
 
//超时时间
.timeout(Duration.ofMillis(5009))
 
//发起一个 post 消息,需要存入一个消息体
.POST(HttpRequest.BodyPublishers.ofString("hello"))
 
//发起一个 get 消息,get 不需要消息体
//.GET()
 
//method(...) 方法是 POST(...) 和 GET(...) 方法的底层,效果一样
//.method("POST",HttpRequest.BodyPublishers.ofString("hello"))
 
//创建完成
.build();

 

 

实例

设置超时时间

    @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实例

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实例

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

    @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());
    }
  • authenticator可以用来设置HTTP authentication,比如Basic authentication
  • 虽然Basic authentication也可以自己设置header,不过通过authenticator省得自己去构造header 

设置header

    @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

GET

  • 同步
        @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());
        }
    

     

  • 异步
        @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表单

    @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

    @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格式 

文件上传

    @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());
    }
  • 官方的HttpClient并没有提供类似WebClient那种现成的BodyInserters.fromMultipartData方法,因此这里需要自己转换
  • 这里使用org.apache.httpcomponents(httpclient及httpmime)的MultipartEntityBuilder构建multipartEntity,最后通过HttpRequest.BodyPublishers.ofInputStream来传递内容
  • 这里header要指定Content-Type值为multipart/form-data以及boundary的值,否则服务端可能无法解析

文件下载

    @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来接收文件

并发请求

    @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完成

错误处理

    @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>,其自带exceptionally方法可以用来做fallback处理
  • 另外值得注意的是HttpClient不像WebClient那样,它没有对4xx或5xx的状态码抛出异常,需要自己根据情况来处理,手动检测状态码抛出异常或者返回其他内容

HTTP2

    @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

    @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

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;
        }
    }
  • BodySubscriber接口继承了Flow.Subscriber<List>接口
  • 这里的Subscription来自Flow类,该类是java9引入的,里头包含了支持Reactive Streams的实现

小结

HttpClient在Java11从incubator变为正式版,相对于传统的HttpUrlConnection其提升可不是一点半点,不仅支持异步,也支持reactive streams,同时也支持了HTTP2以及WebSocket,非常值得大家使用。

参考文档:https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/class-use/HttpClient.html

                  http://www.yesdata.net/2018/10/09/http-client-in-jdk11/

 

 

 

 

 

 

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值