com.wl4g.infra.common.remoting.Netty4ClientHttpRequestFactory Maven / Gradle / Ivy
/*
* Copyright 2017 ~ 2025 the original author or authors. James Wong
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wl4g.infra.common.remoting;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static com.wl4g.infra.common.lang.Assert2.*;
import static com.wl4g.infra.common.lang.TypeConverts.safeLongToInt;
import static java.lang.Runtime.getRuntime;
import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import javax.net.ssl.SSLException;
import javax.annotation.Nullable;
import com.wl4g.infra.common.remoting.standard.HttpHeaders;
import static com.wl4g.infra.common.remoting.standard.HttpMediaType.MULTIPART_FORM_DATA;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelConfig;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.SocketChannelConfig;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
/**
* {@link ClientHttpRequestFactory} implementation that uses
* Netty 4 to create requests.
*
* Allows to use a pre-configured {@link EventLoopGroup} instance: useful for
* sharing across multiple clients.
*
* Note that this implementation consistently closes the HTTP connection on each
* request.
*
* @author James Wong <[email protected]>
* @version 2020年7月01日 v1.0.0
* @see
*/
public class Netty4ClientHttpRequestFactory implements ClientHttpRequestFactory, Closeable {
private final boolean defaultEventLoopGroup;
private final EventLoopGroup eventLoopGroup;
@Nullable
private SslContext sslContext;
private boolean debug = false;
private long connectTimeout;
private long readTimeout;
private int maxResponseSize;
@Nullable
private volatile Bootstrap bootstrap;
/**
* Create a new {@code Netty4ClientHttpRequestFactory} with a default
* {@link NioEventLoopGroup}.
*/
public Netty4ClientHttpRequestFactory() {
this(false, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, DEFAULT_MAX_RESPONSE_SIZE);
}
/**
* Create a new {@code Netty4ClientHttpRequestFactory} with a default
* {@link NioEventLoopGroup}.
*/
public Netty4ClientHttpRequestFactory(boolean debug) {
// see:io.netty.channel.DefaultChannelConfig#DEFAULT_CONNECT_TIMEOUT
this(debug, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, DEFAULT_MAX_RESPONSE_SIZE);
}
/**
* Create a new {@code Netty4ClientHttpRequestFactory} with a default
* {@link NioEventLoopGroup}.
*
* @param debug
* @param connectTimeoutMs
* @param readTimeoutMs
* @param maxResponseSize
*/
public Netty4ClientHttpRequestFactory(boolean debug, int connectTimeoutMs, int readTimeoutMs, int maxResponseSize) {
this(new NioEventLoopGroup(getRuntime().availableProcessors() * 2), debug);
setConnectTimeout(connectTimeoutMs <= 0 ? DEFAULT_CONNECT_TIMEOUT : connectTimeoutMs);
setReadTimeout(readTimeoutMs <= 0 ? DEFAULT_READ_TIMEOUT : readTimeoutMs);
setMaxResponseSize(maxResponseSize <= 0 ? DEFAULT_MAX_RESPONSE_SIZE : maxResponseSize);
}
/**
* Create a new {@code Netty4ClientHttpRequestFactory} with the given
* {@link EventLoopGroup}.
*
* NOTE: the given group will not be
* {@linkplain EventLoopGroup#shutdownGracefully() shutdown} by this
* factory; doing so becomes the responsibility of the caller.
*
* @param debug
*/
public Netty4ClientHttpRequestFactory(EventLoopGroup eventLoopGroup, boolean debug) {
// notNull(eventLoopGroup, "EventLoopGroup must not be null");
this.eventLoopGroup = eventLoopGroup;
this.defaultEventLoopGroup = isNull(eventLoopGroup);
this.debug = debug;
}
/**
* Set the default maximum response size.
*
* By default this is set to {@link #DEFAULT_MAX_RESPONSE_SIZE}.
*
* @since 4.1.5
* @see HttpObjectAggregator#HttpObjectAggregator(int)
*/
public void setMaxResponseSize(int maxResponseSize) {
this.maxResponseSize = maxResponseSize;
}
/**
* Set the SSL context. When configured it is used to create and insert an
* {@link io.netty.handler.ssl.SslHandler} in the channel pipeline.
*
* A default client SslContext is configured if none has been provided.
*/
public void setSslContext(SslContext sslContext) {
this.sslContext = sslContext;
}
/**
* Set the underlying connect timeout (in milliseconds). A timeout value of
* 0 specifies an infinite timeout.
*
* @see ChannelConfig#setConnectTimeoutMillis(int)
*/
public void setConnectTimeout(long connectTimeout) {
this.connectTimeout = connectTimeout;
}
/**
* Set the underlying URLConnection's read timeout (in milliseconds). A
* timeout value of 0 specifies an infinite timeout.
*
* @see ReadTimeoutHandler
*/
public void setReadTimeout(long readTimeout) {
this.readTimeout = readTimeout;
}
/**
* Create nttp request of netty.
*
* @param uri
* @param httpMethod
* @param requestHeaders
* @return
* @throws IOException
*/
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, HttpHeaders requestHeaders) throws IOException {
return new Netty4ClientHttpRequest(getBootstrap(uri, requestHeaders), uri, httpMethod);
}
/**
* Template method for changing properties on the given
* {@link SocketChannelConfig}.
*
* The default implementation sets the connect timeout based on the set
* property.
*
* @param config
* the channel configuration
*/
protected void configureChannel(SocketChannelConfig config) {
if (connectTimeout >= 0) {
config.setConnectTimeoutMillis(safeLongToInt(connectTimeout));
}
}
private SslContext getSslContext() {
if (sslContext == null) {
sslContext = buildClientSslContext();
}
return sslContext;
}
private SslContext buildClientSslContext() {
try {
return SslContextBuilder.forClient().build();
} catch (SSLException ex) {
throw new IllegalStateException("Could not create default client SslContext", ex);
}
}
private Bootstrap getBootstrap(URI uri, HttpHeaders requestHeaders) {
boolean isSecure = (uri.getPort() == 443 || "https".equalsIgnoreCase(uri.getScheme()));
// if (isSecure) {
// return createBootstrap(uri, true, requestHeaders);
// } else if (isNull(bootstrap)) {
// this.bootstrap = createBootstrap(uri, false, requestHeaders);
// }
// return bootstrap;
return createBootstrap(uri, isSecure, requestHeaders);
}
private Bootstrap createBootstrap(final URI uri, final boolean isSecure, final HttpHeaders requestHeaders) {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new HttpChannelInitializer(uri, isSecure, requestHeaders));
return bootstrap;
}
@Override
public void close() throws IOException {
if (defaultEventLoopGroup) {
// Clean up the EventLoopGroup if we created it in the constructor
try {
eventLoopGroup.shutdownGracefully().sync();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
/**
* {@link HttpChannelInitializer}
*/
private class HttpChannelInitializer extends ChannelInitializer {
final private URI uri;
final private boolean isSecure;
final private HttpHeaders requestHeaders;
HttpChannelInitializer(final URI uri, final boolean isSecure, final HttpHeaders requestHeaders) {
this.uri = uri;
this.isSecure = isSecure;
this.requestHeaders = requestHeaders;
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
configureChannel(ch.config());
ChannelPipeline pipe = ch.pipeline();
if (debug) {
pipe.addLast(new LoggingHandler(LogLevel.INFO));
}
if (isSecure) {
notNull(getSslContext(), "sslContext should not be null");
pipe.addLast(getSslContext().newHandler(ch.alloc(), uri.getHost(), uri.getPort()));
}
pipe.addLast(new HttpClientCodec());
if (nonNull(requestHeaders) && MULTIPART_FORM_DATA.isCompatibleWith(requestHeaders.getContentType())) {
// Remove the following line if you don't want automatic
// content decompression.
pipe.addLast("inflater", new HttpContentDecompressor());
// to be used since huge file transfer
pipe.addLast("chunkedWriter", new ChunkedWriteHandler());
} else {
pipe.addLast(new HttpObjectAggregator(maxResponseSize));
}
if (readTimeout > 0) {
pipe.addLast(new ReadTimeoutHandler(safeLongToInt(readTimeout), MILLISECONDS));
}
}
}
public static final int DEFAULT_MAX_RESPONSE_SIZE = 1024 * 1024 * 10;
// see:io.netty.channel.DefaultChannelConfig#DEFAULT_CONNECT_TIMEOUT
public static final int DEFAULT_CONNECT_TIMEOUT = 10_000;
public static final int DEFAULT_READ_TIMEOUT = 30_000;
}