之前有篇文章介绍过这个甜甜的java工具类库hutool,也用过它导出excel,对它印象还不错。
今天需要开发一个小工具jar,小工具的目的简单来说就是要去疯狂的远程调用几个http接口,开发之前首先就想到了用hutool试一下它里面的HttpUtil,因为小工具去调用远程http接口是要大量循环地进行的,是疯狂的,所以http连接池是必不可少的,为此特意去看了一下hutool的HttpUtil有没有应用连接池。
打开了hutool的HttpUtil类,有很多重载的get()和post()方法,想知道有没有使用连接池,直接看execute()方法就好了:
既然都是HttpRequest的execute()方法,随便点开一个:
原来还有布尔型变量控制同步请求还是异步请求,不过这个我不关心,我一眼就看到了这个this.initConnection()方法,似乎已经找到答案了,看这意思就是每次调用一次execute()方法都要去初始化一下连接喽,为了确认一下点开了这个 initConnection()方法:
上来先判断一下是不是已经有httpConnection连接了,然后厉害了,如果有连接先disconnectQuietly()断开连接,然后再HttpConnection.create()创建新的连接。本来我是想来看hutool有没有用连接池的,这里断开连接的操作简直让我感觉一点活路都不留。接下来就不用看了,HttpConnection.create()我也点进去看过,确实是去JDK的rt.net包里创建了一个新连接,这里就不继续贴了。
既然这样的话,这次我就不能用hutool了,之前一直都是用的org.apache.httpcomponents包下的httpclient,用了这么长时间自己也封装了一个HttpUtil,经历多次封装完善优化后也是传到了自己的GitHub上准备祖传了。忽然想到自己封装的HttpUtil虽然使用了连接池PoolingHttpClientConnectionManager,自己却不知道这个连接池是如何实现的,借着这次的突如其来的兴趣就撸了httpclient的部分源码。
最初的想法就是,连接池嘛,那还不是好多http连接放在内存里缓存着,要么就是Map要么就是List,难不成还存出花来啊。带着猜测就继续看了。
先贴一张自己封装的HttpUtil的部分内容:
这里我把httpclient作为入参就是为了复用连接,httpclient就是通过图中的openClient()方法拿到了,可以看到HttpClientBuilder.create()创建httpclient的时候是设置了连接池PoolingHttpClientConnectionManager。
和刚刚一样,想看httpclient是怎么从连接池中获取连接的,直接看execute()方法:
determineTarget(request)这个方法是为了拿到目标主机host,比如一个http://127.0.0.1/test这个请求目标主机就是127.0.0.1这样。直接看doExecute()方法:
这个doExecute()方法太长了贴不完,不过只要抓住这个方法的返回值CloseableHttpResponse是在哪里初始化的在哪里获取的就好了,其他的我也不关心,就直接定位到了图中标注的地方,这个director.execute()方法是拿到目标主机target和请求request后还准备去执行这个请求,到这里为止还没有出现httpconnection有关的东西,那就直接看这个director.execute()方法:
这个director.execute()方法也是长的很贴不完,不过在这里面还真找到了连接con是怎么来的,也就是这个managedConn,可以看到managedConn是通过connRequest.getConnection()来的,看来是需要这个connRequest,图中也可以看到connRequest = connManager.requestConnection(route, userToken),route可以理解成用来标记目标主机,可以看一下这个HttpRoute:
可以看到route里是有目标主机host的信息的,这样比较好理解。 而userToken可以理解成用来标记请求客户端,那这就可以理解成相同的请求客户端和目标主机之间的请求是相同的connRequest,再通过connRequest去getConnection()就很好理解了,一个客户端和一个目标主机之间的连接是被缓存的,这就是连接池最原始的意义。
那就继续看这个connRequest.getConnection(),看看到底是怎么那连接的。ClientConnectionRequest是一个接口,不同的连接池管理类都各自以内部类的方式实现了这个接口,我这里用的是PoolingHttpClientConnectionManager:
leaseConnection,顾名思义就是租借一个连接,看来里答案已经很近了,继续看这个leaseConnection()方法:
虽然在一句Asserts.check()里面,不过一眼就可以看到这个entry.getConnection(),这句也是为了检查连接池中有没有连接,看来连接就是通过这个entry来的,而这个entry就是入参future中的HttpPoolEntry。虽然后面用entry新建了一个ManagedClientConnectionImpl挺容易让人乱想咋新建了一个对象,不过这个我也点进去看过,这里面也是用entry去entry.getConnection()获取连接,这里就不贴了。现在的关键在于这个HttpPoolEntry,有了这个HttpPoolEntry就能拿到连接,它是个入参,这样的话又要回头看了,图再贴一遍:
可以看到future是this.pool.lease(route,state)获取的,这个this.pool是一个HttpConnPool对象,而route刚也分析了可以用来标识目标主机和请求客户端,直接看这个lease()方法:
/**
* {@inheritDoc}
* <p>
* Please note that this class does not maintain its own pool of execution
* {@link Thread}s. Therefore, one <b>must</b> call {@link Future#get()}
* or {@link Future#get(long, TimeUnit)} method on the {@link Future}
* returned by this method in order for the lease operation to complete.
*/
@Override
public Future<E> lease(final T route, final Object state, final FutureCallback<E> callback) {
Args.notNull(route, "Route");
Asserts.check(!this.isShutDown, "Connection pool shut down");
return new Future<E>() {
private final AtomicBoolean cancelled = new AtomicBoolean(false);
private final AtomicBoolean done = new AtomicBoolean(false);
private final AtomicReference<E> entryRef = new AtomicReference<E>(null);
@Override
public boolean cancel(final boolean mayInterruptIfRunning) {
if (done.compareAndSet(false, true)) {
cancelled.set(true);
lock.lock();
try {
condition.signalAll();
} finally {
lock.unlock();
}
if (callback != null) {
callback.cancelled();
}
return true;
}
return false;
}
@Override
public boolean isCancelled() {
return cancelled.get();
}
@Override
public boolean isDone() {
return done.get();
}
@Override
public E get() throws InterruptedException, ExecutionException {
try {
return get(0L, TimeUnit.MILLISECONDS);
} catch (final TimeoutException ex) {
throw new ExecutionException(ex);
}
}
@Override
public E get(final long timeout, final TimeUnit timeUnit) throws InterruptedException, ExecutionException, TimeoutException {
for (;;) {
synchronized (this) {
try {
final E entry = entryRef.get();
if (entry != null) {
return entry;
}
if (done.get()) {
throw new ExecutionException(operationAborted());
}
final E leasedEntry = getPoolEntryBlocking(route, state, timeout, timeUnit, this);
if (validateAfterInactivity > 0) {
if (leasedEntry.getUpdated() + validateAfterInactivity <= System.currentTimeMillis()) {
if (!validate(leasedEntry)) {
leasedEntry.close();
release(leasedEntry, false);
continue;
}
}
}
if (done.compareAndSet(false, true)) {
entryRef.set(leasedEntry);
done.set(true);
onLease(leasedEntry);
if (callback != null) {
callback.completed(leasedEntry);
}
return leasedEntry;
} else {
release(leasedEntry, true);
throw new ExecutionException(operationAborted());
}
} catch (final IOException ex) {
if (done.compareAndSet(false, true)) {
if (callback != null) {
callback.failed(ex);
}
}
throw new ExecutionException(ex);
}
}
}
}
};
}
这个lease()方法全在这里了,里面是用了一个Future异步任务,我关注返回值也就是源码中的泛型E,重点就到了这个get()方法:
可以看到HttpPoolEntry是通过这个getPoolEntryBlocking()方法获取的,看来还是个阻塞方法,也就是异步任务中的一个阻塞方法,入参也是有route的,直接看这个方法:
关注返回值就好, 可以看到先用route调用了getPool方法拿到了一个连接池,这就可以理解成相同的目标主机和客户端之间的连接是放在同一个连接池里面的即RouteSpecificPool对象,也合情合理,不然也太乱了。然后通过RouteSpecificPool对象的getFree()方法拿到一个HttpPoolEntry对象,也就是一个连接,看着方法名应该是去连接池中拿空闲的连接,看一下这个getFree()方法:
看来空闲可用的HttpPoolEntry对象是放在this.available这个集合里的,真相就在眼前了,往上找这个this.available:
原来是一个LinkedList, 应验了一开始的猜想,还不是这一套嘛。
可以思考一下为啥要用LinkedList,LinkedList与ArrayList的区别就在于链表结构增删快,查询由于要遍历链表比ArrayList的数组结构慢。这里的使用场景时LinkedList里面放的都是一台目标主机和一台客户端之间的空闲连接对象HttpPoolEntry,需要使用一个连接或者释放连接就是从链表中删除一个对象,缓存连接就是增加一个对象,并不涉及查询这种场景,所以这种增删比较多的情况确实应该用LinkedList。