Spring5.1开始,WebFlux的WebClient支持连接池功能了。
默认情况下,WebClient使用global Reactor Netty资源,也可以不使用全局资源:
@Bean
ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setUseGlobalResources(false);
factory.setConnectionProvider(ConnectionProvider.fixed("httpClient", maxConns, acquireTimeout));
factory.setLoopResources(LoopResources.create("httpClient", workCounts, true));
return factory;
}
可以这样配置WebClient:
@Bean
WebClient webClient() {
Function<HttpClient, HttpClient> mapper = client ->
client.tcpConfiguration(c ->
c.option(CONNECT_TIMEOUT_MILLIS, connTimeout)
.option(TCP_NODELAY, true)
.doOnConnected(conn -> {
conn.addHandlerLast(new ReadTimeoutHandler(readTimeout));
conn.addHandlerLast(new WriteTimeoutHandler(writeTimeout));
}));
ClientHttpConnector connector =
new ReactorClientHttpConnector(resourceFactory(), mapper);
return WebClient.builder().clientConnector(connector).build();
}
测试一下:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("constants")
public class WebClientTest {
@Autowired
private WebClient client;
private static String blog = "https://blog.csdn.net/weixin_43364172";
private static String weibo = "http://www.weibo.com";
@Test
public void test() {
single(blog);
}
private void single(String url) {
Mono<ClientResponse> mono = client.get().uri(url)
.accept(MediaType.TEXT_HTML)
.exchange();
StepVerifier.create(mono)
.assertNext(clientResponse -> {
Assert.assertThat(clientResponse.statusCode(),
anyOf(equalTo(OK), equalTo(MOVED_PERMANENTLY)));
if (clientResponse.rawStatusCode() == OK.value()) {
/* Mono res = clientResponse.bodyToMono(String.class);
StepVerifier.create(res)
.assertNext(s -> Assert.assertThat(s.length(), greaterThan(0)))
.verifyComplete();*/
clientResponse.bodyToMono(String.class).subscribe(s -> System.out.println(s.length()));
}
})
.verifyComplete();
}
@Test(timeout = 10000)
public void tests() {
concurrency(3, weibo);
concurrency(10, blog);
concurrency(2, blog);
concurrency(2, weibo);
concurrency(3, weibo);
concurrency(2, blog);
}
private void concurrency(int num, String url) {
ExecutorService es = Executors.newFixedThreadPool(num);
for (int i = 0; i < num; i++) {
es.execute(() -> single(url));
}
es.shutdown();
try {
es.awaitTermination(10, TimeUnit.SECONDS);
System.out.println(num + " completes");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行发现,获取weibo和blog页面,各使用了独立的Bootstrap和对应的连接池。
如果想让不同网站使用不用的配置参数,可以让它们各自使用独立的ReactorResourceFactory类型的bean,并使用不同的WebClient。
WebClient好像还不支持失败自动重试功能,需要码更多的代码。
看ConnectionProvider代码,可以发现,目前系统支持三种连接池:
FixedChannelPool和SimpleChannelPool都是Netty实现的。
新的连接请求到来,先经过TcpClientConnect类的connect方法:
public Mono<? extends Connection> connect(Bootstrap b) {
if (b.config()
.group() == null) {
TcpClientRunOn.configure(b,
LoopResources.DEFAULT_NATIVE,
TcpResources.get(),
maxConnections != -1);
}
return provider.acquire(b);
}
PooledConnectionProvider类的disposableAcquire方法:
static void disposableAcquire(MonoSink<Connection> sink, ConnectionObserver obs, Pool pool) {
Future<Channel> f = pool.acquire();
DisposableAcquire disposableAcquire =
new DisposableAcquire(sink, f, pool, obs);
// Returned value is deliberately ignored
f.addListener(disposableAcquire);
sink.onCancel(disposableAcquire);
}
调用FixedChannelPool类的acquire方法:
public Future<Channel> acquire(final Promise<Channel> promise) {
try {
if (executor.inEventLoop()) {
acquire0(promise);
} else {
executor.execute(new Runnable() {
@Override
public void run() {
acquire0(promise);
}
});
}
} catch (Throwable cause) {
promise.setFailure(cause);
}
return promise;
}
重点在acquire0方法:
private void acquire0(final Promise<Channel> promise) {
assert executor.inEventLoop();
if (closed) {
promise.setFailure(POOL_CLOSED_ON_ACQUIRE_EXCEPTION);
return;
}
if (acquiredChannelCount.get() < maxConnections) {
assert acquiredChannelCount.get() >= 0;
// We need to create a new promise as we need to ensure the AcquireListener runs in the correct
// EventLoop
Promise<Channel> p = executor.newPromise();
AcquireListener l = new AcquireListener(promise);
l.acquired();
p.addListener(l);
super.acquire(p);
} else {
if (pendingAcquireCount >= maxPendingAcquires) {
promise.setFailure(FULL_EXCEPTION);
} else {
AcquireTask task = new AcquireTask(promise);
if (pendingAcquireQueue.offer(task)) {
++pendingAcquireCount;
if (timeoutTask != null) {
task.timeoutFuture = executor.schedule(timeoutTask, acquireTimeoutNanos, TimeUnit.NANOSECONDS);
}
} else {
promise.setFailure(FULL_EXCEPTION);
}
}
assert pendingAcquireCount > 0;
}
}
最终通过父类的acquireHealthyFromPoolOrNew方法返回连接:
private Future<Channel> acquireHealthyFromPoolOrNew(final Promise<Channel> promise) {
try {
final Channel ch = pollChannel();
if (ch == null) {
// No Channel left in the pool bootstrap a new Channel
Bootstrap bs = bootstrap.clone();
bs.attr(POOL_KEY, this);
ChannelFuture f = connectChannel(bs);
if (f.isDone()) {
notifyConnect(f, promise);
} else {
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
notifyConnect(future, promise);
}
});
}
return promise;
}
EventLoop loop = ch.eventLoop();
if (loop.inEventLoop()) {
doHealthCheck(ch, promise);
} else {
loop.execute(new Runnable() {
@Override
public void run() {
doHealthCheck(ch, promise);
}
});
}
} catch (Throwable cause) {
promise.tryFailure(cause);
}
return promise;
}
测试的时候,需要注意的一点是:只有读取了LastHttpContent,才会关闭连接。
可以看HttpClientOperations类的源码:
if (msg instanceof LastHttpContent) {
if (!started) {
if (log.isDebugEnabled()) {
log.debug(format(channel(), "HttpClientOperations received an incorrect end " +
"delimiter (previously used connection?)"));
}
ReferenceCountUtil.release(msg);
return;
}
if (log.isDebugEnabled()) {
log.debug(format(channel(), "Received last HTTP packet"));
}
if (msg != LastHttpContent.EMPTY_LAST_CONTENT) {
super.onInboundNext(ctx, msg);
}
//force auto read to enable more accurate close selection now inbound is done
channel().config().setAutoRead(true);
if (markSentBody()) {
markPersistent(false);
}
terminate();
return;
}
其中,terminate方法见ChannelOperations类:
protected final void terminate() {
if (rebind(connection)) {
if (log.isTraceEnabled()) {
log.trace(format(channel(), "Disposing ChannelOperation from a channel"),
new Exception("ChannelOperation terminal stack"));
}
Operators.terminate(OUTBOUND_CLOSE, this);
listener.onStateChange(this, ConnectionObserver.State.DISCONNECTING);
// Do not call directly inbound.onInboundComplete()
// HttpClientOperations need to notify with error
// when there is no response state
onInboundComplete();
if (isPersistent()) {
channel().writeAndFlush(TERMINATED_OPS, this);
}
else {
// Returned value is deliberately ignored
setSuccess(null);
}
}
}
向listener发送了DISCONNECTING消息,监听器就是PooledConnectionProvider类的静态子类PooledConnection:
if (newState == State.DISCONNECTING) {
if (!isPersistent() && channel.isActive()) {
//will be released by closeFuture internals
channel.close();
owner().onStateChange(connection, State.DISCONNECTING);
return;
}
if (!channel.isActive()) {
owner().onStateChange(connection, State.DISCONNECTING);
//will be released by poolResources internals
return;
}
if (log.isDebugEnabled()) {
log.debug(format(connection.channel(), "Releasing channel"));
}
ConnectionObserver obs = channel.attr(OWNER)
.getAndSet(ConnectionObserver.emptyListener());
pool.release(channel)
.addListener(f -> {
if (log.isDebugEnabled() && !f.isSuccess()) {
log.debug("Failed cleaning the channel from pool", f.cause());
}
onTerminate.onComplete();
obs.onStateChange(connection, State.RELEASED);
});
return;
}
通过pool.release(channel)释放了Channel资源。