文章目录
一、简介
java的Http客户端库,不说五花八门吧也算是百花齐放了。归根结底的原因就是JDK自带的http工具URLConnection太难用了!
JDK11之后
,JDk推出了一个全新的HttpClient库,基于了响应式流(Reactive Stream)模型,使用了JAVA 9引入的Flow api,并运用completableFuture使调用流程异步化(在IO层面实现非阻塞化-NIO),带来了性能的提升和焕然一新的使用体验。
HttpClient 支持同步和异步
两种请求发送模式。
同步请求方式使用的是 send
方法,参数是 HttpRequest 和 BodyHandler
,返回值是 HttpResponse<T>
。
异步请求方式使用的是sendAsync
方法,参数同样是 HttpRequest 和 BodyHandler
,返回值是 CompletableFuture<HttpResponse<T>>
。
注意!
Http连接池不支持手动配置,默认是无限复用的
重试次数不支持手动配置
不指定Http客户端或请求的版本,会默认使用Http2模式进行连接,受挫后会进行降级
请求的同步发送模式(send)实际上会后台另开线程
二、使用
1、发送get、post请求
HttpClient client = HttpClient.newBuilder()
.version(Version.HTTP_1_1) //可以手动指定客户端的版本,如果不指定,那么默认是Http2
.followRedirects(Redirect.NORMAL) //设置重定向策略
.connectTimeout(Duration.ofSeconds(20)) //连接超时时间
.proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 80))) //代理地址设置
.authenticator(Authenticator.getDefault())
//.executor(Executors.newFixedThreadPoolExecutor(8)) //可手动配置线程池
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://foo.com/")) //设置url地址
.GET()
.build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString()); //同步发送
System.out.println(response.statusCode()); //打印响应状态码
System.out.println(response.body());
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://foo.com/"))
.timeout(Duration.ofMinutes(2)) //设置连接超时时间
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofFile(Paths.get("file.json"))) //设置请求体来源
.build();
client.sendAsync(request, BodyHandlers.ofString()) //异步发送
.thenApply(HttpResponse::body) //发送结束打印响应体
.thenAccept(System.out::println);
2、会话保持
//使用默认的CookieManager,并且接受所有第三方Cookie
cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(5000))
.cookieHandler(cookieManager)//注意在此步骤送送入
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
3、发送multipart/form-data数据
遗憾的是,JDK 11 HttpClient对这类数据支持不太友好。
private val httpClient: HttpClient = HttpClient.newHttpClient()
fun main() {
val file = File(
LocalTest::class.java.classLoader.getResource("my_pic_from_resources.jpg").file
)
val token = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
val data: MutableMap<String, Any> = LinkedHashMap()
data["chat_id"] = "123456789"
data["photo"] = file
val boundary: String = BigInteger(35, Random()).toString()
val request = HttpRequest.newBuilder()
.uri(URI.create("https://api.telegram.org/bot$token/sendPhoto"))
.postMultipartFormData(boundary, data)
.build()
val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
println(response)
}
fun HttpRequest.Builder.postMultipartFormData(boundary: String, data: Map<String, Any>): HttpRequest.Builder {
val byteArrays = ArrayList<ByteArray>()
val separator = "--$boundary\r\nContent-Disposition: form-data; name=".toByteArray(StandardCharsets.UTF_8)
for (entry in data.entries) {
byteArrays.add(separator)
when(entry.value) {
is File -> {
val file = entry.value as File
val path = Path.of(file.toURI())
val mimeType = Files.probeContentType(path)
byteArrays.add("\"${entry.key}\"; filename=\"${path.fileName}\"\r\nContent-Type: $mimeType\r\n\r\n".toByteArray(StandardCharsets.UTF_8))
byteArrays.add(Files.readAllBytes(path))
byteArrays.add("\r\n".toByteArray(StandardCharsets.UTF_8))
}
else -> byteArrays.add("\"${entry.key}\"\r\n\r\n${entry.value}\r\n".toByteArray(StandardCharsets.UTF_8))
}
}
byteArrays.add("--$boundary--".toByteArray(StandardCharsets.UTF_8))
this.header("Content-Type", "multipart/form-data;boundary=$boundary")
.POST(HttpRequest.BodyPublishers.ofByteArrays(byteArrays))
return this
}
var body = new MultipartFormData()
.add(StringPart("name", "Hello,")
.add(StringPart("value", "World!")
.addFile("f", Path.of("index.html"), "text/html")
.addFile("cpuinfo", Path.of("/proc/cpuinfo"), "text/html");
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("http://localhost:8080/"))
.header("Content-Type", body.contentType())
.POST(body)
.build();
var response = client.send(request, BodyHandlers.ofLines());
response.body().forEach(line -> System.out.println(line));
或者:
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1/user"))
.timeout(Duration.ofMinutes(2))
.header("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
.POST(HttpRequest.BodyPublishers.ofString("id=1&userName=赵云"))
.build();
String body = client.send(request, HttpResponse.BodyHandlers.ofString()).body();
System.out.println("阻塞请求结果:" + body);
4、websocket长连接
使用HttpClient创建WebSocket实时通信
HttpClient client = HttpClient.newHttpClient();
WebSocket webSocket = client.newWebSocketBuilder()
.buildAsync(URI.create("ws://localhost:8080/hello"), new WebSocket.Listener() {
@Override
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
webSocket.request(1);
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();
三、源码分析
1、HttpClient客户端构建
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())
//创建完成
// 这里使用了典型的建造者模式(Builder Pattern)。使用建造者模式的好处是:Httpclient相对复杂的构建过程被隐藏了起来,使用者无需知道具体的构建细节。
.build();
最终构建的逻辑是在build方法中执行的,最后的构建过程交给了HttpClientImpl。
/**
* Client implementation. Contains all configuration information and also
* the selector manager thread which allows async events to be registered
* and delivered when they occur. See AsyncEvent.
*/
final class HttpClientImpl extends HttpClient implements Trackable {
static final AtomicLong CLIENT_IDS = new AtomicLong();
//此处列出大部分成员变量
private final CookieHandler cookieHandler;
private final Duration connectTimeout;
private final Redirect followRedirects;
private final ProxySelector userProxySelector;
private final ProxySelector proxySelector;
private final Authenticator authenticator;
private final Version version;
private final ConnectionPool connections;
private final DelegatingExecutor delegatingExecutor;
private final boolean isDefaultExecutor;
// Security parameters
private final SSLContext sslContext;
private final SSLParameters sslParams;
private final SelectorManager selmgr;
private final FilterFactory filters;
//HttpClient2客户端
private final Http2ClientImpl client2;
private final long id;
private final String dbgTag;
private final SSLDirectBufferSupplier sslBufferSupplier
= new SSLDirectBufferSupplier(this);
//对HttpClient外观实现类的弱引用
private final WeakReference<HttpClientFacade> facadeRef;
//对待处理的请求的计数
private final AtomicLong pendingOperationCount = new AtomicLong();
private final AtomicLong pendingWebSocketCount = new AtomicLong();
private final AtomicLong pendingHttpRequestCount = new AtomicLong();
private final AtomicLong pendingHttp2StreamCount = new AtomicLong();
/** 过期时间 */
private final TreeSet<TimeoutEvent> timeouts;
/**
* This is a bit tricky:
* 1. an HttpClientFacade has a final HttpClientImpl field.
* 2. an HttpClientImpl has a final WeakReference<HttpClientFacade> field,
* where the referent is the facade created for that instance.
* 3. We cannot just create the HttpClientFacade in the HttpClientImpl
* constructor, because it would be only weakly referenced and could
* be GC'ed before we can return it.
* The solution is to use an instance of SingleFacadeFactory which will
* allow the caller of new HttpClientImpl(...) to retrieve the facade
* after the HttpClientImpl has been created.
*/
//外观工厂
private static final class SingleFacadeFactory {
HttpClientFacade facade;
HttpClientFacade createFacade(HttpClientImpl impl) {
assert facade == null;
return (facade = new HttpClientFacade(impl));
}
}
//我们要分析的方法
static HttpClientFacade create(HttpClientBuilderImpl builder) {
//这是个Factory?这是在做什么呢?
//其实,这里是HttpClient的外观代理实现类HttpClientFacade的构建工厂。
SingleFacadeFactory facadeFactory = new SingleFacadeFactory();
//构建方法的重点:实例化HttpClientImpl。稍后详细分析
HttpClientImpl impl = new HttpClientImpl(builder, facadeFactory);
//启动NIO选择子管理者守护线程,接收并响应I/O事件
impl.start();
assert facadeFactory.facade != null;
assert impl.facadeRef.get() == facadeFactory.facade;
//返回外观实现类
return facadeFactory.facade;
}
//HttpClientImpl的初始化构造方法
//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;
}
}
调用build()方法返回的是一个HttpClientFacade的外观实现类。HttpClientFacade是什么?它和HttpClientImpl的关系是什么?SingleFacadeFactory又是什么用途呢?因此,我们通过UML类图来说明。
2、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();