Jade Dungeon

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的核心类主要就是HttpClientHttpRequest以及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();
		
	...

}