HttpClient
概述
作为JDK11中正式推出的新Http连接器,支持的功能还是比较新的,主要的特性有:
- 完整支持HTTP 2.0 或者HTTP 1.1
- 支持 HTTPS/TLS
- 有简单的阻塞使用方法
- 支持异步发送,异步时间通知
- 支持WebSocket
- 支持响应式流
HTTP2.0其他的客户端也能支持,
而HttpClient
使用CompletableFuture
作为异步的返回数据。
WebSocket的支持则是HttpClient
的优势。响应式流的支持是HttpClient
的一大优势。
而HttpClient中的NIO模型、函数式编程、CompletableFuture
异步回调、
响应式流让HttpClient
拥有极强的并发处理能力,所以其性能极高,而内存占用则更少。
HttpClient的主要类有:
-
java.net.http.HttpClient
-
java.net.http.HttpRequest
-
java.net.http.HttpResponse
-
java.net.http.WebSocket
本文就不介绍这个了
HttpClient
的核心类主要就是HttpClient
、HttpRequest
以及HttpResponse
,
它们都是位于java.net.http
包。
HttpClient
HttpClient
类是最核心的类,它支持使用建造者模式进行复杂对象的构建,主要的参数有:
- Http 协议的版本 (HTTP 1.1 或者 HTTP 2.0),默认是 2.0。
- 是否遵从服务器发出的重定向
- 连接超时时间
- 代理
- 认证
直接全部默认的便捷创建:
HttpClient clientSimple = HttpClient.newHttpClient();
调整常用参数:
HttpClient client = HttpClient.newBuilder() .version(Version.HTTP_1_1) .followRedirects(Redirect.NORMAL) .connectTimeout(Duration.ofSeconds(20)) .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 8080))) .authenticator(Authenticator.getDefault()) .build();
当创建了HttpClient
实例后,可以通过其发送多条请求,不用重复创建。
HttpRequest
HttpRequest
是用语描述请求体的类,也支持通过建造者模式构建复杂对象,主要的参数有:
- 请求地址
-
请求方法:
GET
,POST
,DELETE
等(默认是GET
) -
请求体 (按需设置,例如
GET
不用body
,但是POST
要设置) - 请求超时时间(默认)
- 请求头
直接默认设置GET
访问:
HttpRequest requestSimple = HttpRequest.newBuilder(URI.create( "http://www.baidu.com")).build();
使用参数组合进行对象构建,读取文件作为POST
请求体:
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://www.baidu.com")) .timeout(Duration.ofSeconds(20)) .header("Content-type","application/json") .POST(HttpRequest.BodyPublishers.ofFile(Paths.get("data.json"))) .build();
HttpRequest
是一个不可变类,可以被多次发送。
HttpResponse
HttpResponse
没有提供外部可以创建的实现类,它是一个接口,
从client的返回值中创建获得。接口中的主要方法为:
public interface HttpResponse<T> { public int statusCode(); public HttpRequest request(); public Optional<HttpResponse<T>> previousResponse(); public HttpHeaders headers(); public T body(); public URI uri(); public Optional<SSLSession> sslSession(); public HttpClient.Version version(); }
HttpResponse
在请求发送后由HttpClient.send()
或
HttpClient.sendAsync()
发送请求后返回。
信息发送
HttpClient中可以使用同步发送或者异步发送。
同步send()
同步发送后,请求会一直阻塞到收到response
为止。
final HttpResponse<String> send = client.send( httpRequest, HttpResponse.BodyHandlers.ofString()); System.out.println(send.body());
其中send()
的第二个参数是通过HttpResponse.BodyHandlers
的静态工厂来返回一个可以将response
转换为目标类型T的处理器(handler
),
本例子中的类型是String
。
HttpResponse.BodyHandlers.ofString()
的实现方法为:
public static BodyHandler<String> ofString() { return (responseInfo) -> BodySubscribers.ofString( charsetFrom(responseInfo.headers())); }
其中,BodySubscribers.ofString()
的方法实现是:
public static BodySubscriber<String> ofString(Charset charset) { Objects.requireNonNull(charset); return new ResponseSubscribers.ByteArraySubscriber<>( bytes -> new String(bytes, charset)); }
可以看到最终是返回了一个ResponseSubscribers
,而Subscribers
则是JDK9响应式编程的订阅者。
这个构造方法的入参Function
定义了订阅者中的finisher
属性,
而这个属性将在响应式流完成订阅的时在onComplete()
方法中调用。
异步sendAsync()
异步请求发送之后,会立刻返回CompletableFuture
,
然后可以使用CompletableFuture
中的方法来设置异步处理器。
client.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body) .thenAccept(System.out::println) .join();
而就如同JDK中响应式流中发布者的submit()
方法与offer()
方法一样,
HttpClient
中的send()
方法只是sendAsync()
方法的特例,
在send()
方法中是先调用sendAsync()
方法,然后直接阻塞等待响应结束再返回,
部分核心代码为:
@Override public <T> HttpResponse<T> send( HttpRequest req, BodyHandler<T> responseHandler ) throws IOException, InterruptedException { CompletableFuture<HttpResponse<T>> cf = null; // if the thread is already interrupted no need to go further. // cf.get() would throw anyway. if (Thread.interrupted()) throw new InterruptedException(); try { cf = sendAsync(req, responseHandler, null, null); return cf.get(); } catch (InterruptedException ie) { if (cf != null ) cf.cancel(true); throw ie; } ... }
响应式流
HttpClient
作为Request
的发布者(publisher),将Request
发布到服务器,
作为Response
的订阅者(subscriber),从服务器接收Response
。
而上文中我们在send()
的部分发现,
调用链的最底端返回的是一个ResponseSubscribers
订阅者。
当然,就如同HttpResponse.BodyHandlers.ofString()
,
HttpClient
默认提供了一系列的默认订阅者,用语处理数据的转换:
HttpRequest.BodyPublishers::ofByteArray(byte[]) HttpRequest.BodyPublishers::ofByteArrays(Iterable) HttpRequest.BodyPublishers::ofFile(Path) HttpRequest.BodyPublishers::ofString(String) HttpRequest.BodyPublishers::ofInputStream(Supplier<InputStream>) HttpResponse.BodyHandlers::ofByteArray() HttpResponse.BodyHandlers::ofString() HttpResponse.BodyHandlers::ofFile(Path) HttpResponse.BodyHandlers::discarding()
所以在HttpClient的时候我们也可以自己创建一个实现了
Flow.Subscriber
接口的订阅者,用于消费数据。
响应式流完整的简单的例子如下:
public class HttpClientTest { public static void main(String[] args) throws IOException, InterruptedException { final HttpClient client = HttpClient.newHttpClient(); final HttpRequest httpRequest = HttpRequest.newBuilder( // URI.create("http://www.baidu.com")).build(); HttpResponse.BodySubscriber<String> subscriber = // HttpResponse.BodySubscribers.fromSubscriber( new StringSubscriber(), StringSubscriber::getBody); client.sendAsync(httpRequest,responseInfo -> subscriber) .thenApply(HttpResponse::body) .thenAccept(System.out::println) .join(); } static class StringSubscriber implements Flow.Subscriber<List<ByteBuffer>> { Flow.Subscription subscription; List<ByteBuffer> response = new ArrayList<>(); String body; public String getBody() { return body; } @Override public void onSubscribe(Flow.Subscription subscription) { this.subscription = subscription; subscription.request(1); } @Override public void onNext(List<ByteBuffer> item) { response.addAll(item); subscription.request(1); } @Override public void onError(Throwable throwable) { System.err.println(throwable); } @Override public void onComplete() { byte[] data = new byte[ response.stream().mapToInt(ByteBuffer::remaining).sum() ]; int offset = 0; for(ByteBuffer buffer:response) { int remain = buffer.remaining(); buffer.get(data,offset,remain); offset += remain; } body = new String(data); } } }
HttpClient
是JDK11正式上线的高性能Http客户端。其底层基于响应式流,
通过上层封装还提供了异步信息发送、同步信息发送,以及其他完成的HTTP协议内容。
在进行响应式编程的方面,HttpClient
也是一个十分优秀的参照目标。
常见问题
URL中的Host与请求头里的不一致:
HttpRequest request = HttpRequest.newBuilder("http://www.aaa.com") .header("Host", "www.bbb.com") .POST(HttpRequest.BodyPublishers.ofString(data.toString())) .build();
以上的例子默认会报错:
java.lang.IllegalArgumentException: restricted header name: "Host"
放开不一致限制要在jvn启动参数中设置 :
java -Djdk.httpclient.allowRestrictedHeaders=connection,content-length,host ...
如果是在Eclipse中,则在界面中:menu > Run > Run Cunfigurations
如果是在docker中,使用参数:
docker run --env SDC_JAVA_OPTS="-Dsun.net.http.allowRestrictedHeaders=true"
HTTPS的TLS验证
有些自制证书没有公证过,会报错PKIX path building failed
。
sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
所以要自定义忽略HTPS证书错误:
/* 忽略证书验证 */ private static TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted( java.security.cert.X509Certificate[] certs, String authType) { } public void checkServerTrusted( java.security.cert.X509Certificate[] certs, String authType) { } } }; public void sendRequest() { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustAllCerts, new SecureRandom()); /* 合建http client */ HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1) .followRedirects(Redirect.NORMAL).connectTimeout(Duration.ofSeconds(20)) .sslContext(sslContext) // 使用忽略的逻辑 .build(); ... }