一、背景
在使用httpclient框架请求http接口的时候,我们往往会需要自定义配置httpclient,而非直接使用。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
本文仅以自定义httpclient为例,目的是看在springboot应用程序中,如何写一个AutoConfiguration配置类。
涉及到的类有以下六个:
- HttpClientAutoConfiguration
- HttpClientProperties
- ApacheHttpClientConnectionManagerFactory
- ApacheHttpClientFactory
- DefaultApacheHttpClientConnectionManagerFactory
- DefaultApacheHttpClientFactory
二、写一个AutoConfiguration
1、HttpClientAutoConfiguration
HttpClientAutoConfiguration类提供了一个自动化配置HTTP客户端的解决方案,使得开发者可以方便地在Spring Boot应用中使用Apache HttpClient库。通过配置文件和自动配置,它创建了HTTP连接管理器和HTTP客户端,并且安排了定时任务来维护这些连接。
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PreDestroy;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Configuration
// 使用配置类HttpClientProperties
@EnableConfigurationProperties(value = {HttpClientProperties.class})
public class HttpClientAutoConfiguration {
private final ScheduledExecutorService scheduledService;
private CloseableHttpClient httpClient;
@Autowired
public HttpClientAutoConfiguration() {
// 定义一个单线程池
this.scheduledService = new ScheduledThreadPoolExecutor(
1,
new ThreadFactoryBuilder().setNameFormat("http-client-manager").build(),
new ThreadPoolExecutor.AbortPolicy()
);
}
@Bean
@ConditionalOnMissingBean
public ApacheHttpClientConnectionManagerFactory connManFactory() {
return new DefaultApacheHttpClientConnectionManagerFactory();
}
@Bean
@ConditionalOnMissingBean
public HttpClientBuilder apacheHttpClientBuilder() {
return HttpClientBuilder.create();
}
@Bean
@ConditionalOnMissingBean
public ApacheHttpClientFactory apacheHttpClientFactory(
HttpClientBuilder builder) {
return new DefaultApacheHttpClientFactory(builder);
}
/**
* Connection manager
*/
@Bean
@ConditionalOnMissingBean
public HttpClientConnectionManager connectionManager(
ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
HttpClientProperties httpClientProperties) {
final HttpClientConnectionManager connectionManager = connectionManagerFactory
.newConnectionManager(httpClientProperties.getDisableSslValidation(),
httpClientProperties.getMaxConnections(),
httpClientProperties.getMaxConnectionsPerRoute(),
httpClientProperties.getTimeToLive(),
httpClientProperties.getTimeToLiveUnit());
// 定时关闭过期的HTTP连接
this.scheduledService.scheduleWithFixedDelay(connectionManager::closeExpiredConnections, 1000,
httpClientProperties.getConnectionTimerRepeat(), TimeUnit.MILLISECONDS);
return connectionManager;
}
/**
* Http client
*/
@Bean
@ConditionalOnMissingBean
public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
HttpClientConnectionManager httpClientConnectionManager,
HttpClientProperties httpClientProperties) {
RequestConfig defaultRequestConfig = RequestConfig.custom()
.setConnectTimeout(httpClientProperties.getConnectionTimeout())
.setRedirectsEnabled(httpClientProperties.getFollowRedirects())
.build();
this.httpClient = httpClientFactory
.createBuilder()
.setDefaultRequestConfig(defaultRequestConfig)
.setConnectionManager(httpClientConnectionManager)
.build();
return this.httpClient;
}
/**
* Destroy
*/
@PreDestroy
// @PreDestroy注解的destroy()方法,用于在应用关闭时执行清理工作,关闭线程池scheduledService和httpClient
public void destroy() throws Exception {
this.scheduledService.shutdown();
if (this.httpClient != null) {
this.httpClient.close();
}
}
}
2、HttpClientProperties配置文件
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.concurrent.TimeUnit;
@Data
@ConfigurationProperties(prefix = "httpclient")
public class HttpClientProperties {
/**
* Value for disabling SSL validation.
*/
private Boolean disableSslValidation = false;
/**
* Value for max number od connections.
*/
private Integer maxConnections = 200;
/**
* Value for max number od connections per route.
*/
private Integer maxConnectionsPerRoute = 50;
/**
* Value for time to live.
*/
private Long timeToLive = 900L;
/**
* Time to live unit.
*/
private TimeUnit timeToLiveUnit = TimeUnit.SECONDS;
/**
* Value for following redirects.
*/
private Boolean followRedirects = true;
/**
* Value for connection timeout.
*/
private Integer connectionTimeout = 2000;
/**
* Value for connection timer repeat.
*/
private Integer connectionTimerRepeat = 3000;
}
3、定义两个工厂接口
- ApacheHttpClientConnectionManagerFactory
import org.apache.http.conn.HttpClientConnectionManager;
import java.util.concurrent.TimeUnit;
public interface ApacheHttpClientConnectionManagerFactory {
String HTTP_SCHEME = "http";
/**
* Scheme for HTTPS based communication.
*/
String HTTPS_SCHEME = "https";
/**
* Creates a new {@link HttpClientConnectionManager}.
*
* @param disableSslValidation If true, SSL validation will be disabled.
* @param maxTotalConnections The total number of connections.
* @param maxConnectionsPerRoute The total number of connections per route.
* @param timeToLive The time a connection is allowed to exist.
* @param timeUnit The time unit for the time-to-live value.
* manager.
* @return A new {@link HttpClientConnectionManager}.
*/
HttpClientConnectionManager newConnectionManager(boolean disableSslValidation,
int maxTotalConnections, int maxConnectionsPerRoute, long timeToLive,
TimeUnit timeUnit);
}
- ApacheHttpClientFactory
import org.apache.http.impl.client.HttpClientBuilder;
public interface ApacheHttpClientFactory {
/**
* Create builder
*
* @return HttpClientBuilder
*/
HttpClientBuilder createBuilder();
4、工厂接口的实现
- DefaultApacheHttpClientConnectionManagerFactory
import lombok.extern.slf4j.Slf4j;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
@Slf4j
public class DefaultApacheHttpClientConnectionManagerFactory implements ApacheHttpClientConnectionManagerFactory {
@Override
public HttpClientConnectionManager newConnectionManager(boolean disableSslValidation,
int maxTotalConnections, int maxConnectionsPerRoute, long timeToLive,
TimeUnit timeUnit) {
RegistryBuilder<ConnectionSocketFactory> registryBuilder = RegistryBuilder.<ConnectionSocketFactory>create()
.register(HTTP_SCHEME, PlainConnectionSocketFactory.INSTANCE);
if (disableSslValidation) {
try {
final SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null,
new TrustManager[]{new DisabledValidationTrustManager()},
new SecureRandom());
registryBuilder.register(HTTPS_SCHEME, new SSLConnectionSocketFactory(
sslContext, NoopHostnameVerifier.INSTANCE));
} catch (NoSuchAlgorithmException | KeyManagementException e) {
log.warn("Error creating SSLContext", e);
}
} else {
registryBuilder.register("https", SSLConnectionSocketFactory.getSocketFactory());
}
final Registry<ConnectionSocketFactory> registry = registryBuilder.build();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(
registry, null, null, null, timeToLive, timeUnit);
connectionManager.setMaxTotal(maxTotalConnections);
connectionManager.setDefaultMaxPerRoute(maxConnectionsPerRoute);
return connectionManager;
}
static class DisabledValidationTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
}
- DefaultApacheHttpClientFactory
import org.apache.http.impl.client.HttpClientBuilder;
public class DefaultApacheHttpClientFactory implements ApacheHttpClientFactory {
private final HttpClientBuilder builder;
public DefaultApacheHttpClientFactory(HttpClientBuilder builder) {
this.builder = builder;
}
@Override
public HttpClientBuilder createBuilder() {
return this.builder.disableContentCompression().disableCookieManagement()
.useSystemProperties();
}
}
三、使用自定义httpclient对象
protected final CloseableHttpClient httpClient;
protected AbstractChannel(CloseableHttpClient httpClient) {
this.httpClient = httpClient;
}
protected String postJson(String url, String body) throws IOException {
RequestConfig requestConfig = RequestConfig.custom()
// 200 ms
.setConnectionRequestTimeout(1000)
// 1000ms
.setSocketTimeout(1000)
.build();
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(requestConfig);
httpPost.setHeader("Accept", "application/json");
httpPost.setHeader("Content-Type", "application/json");
// Execute
StringEntity entity = new StringEntity(body, StandardCharsets.UTF_8);
entity.setContentType("application/json");
httpPost.setEntity(entity);
CloseableHttpResponse response = this.httpClient.execute(httpPost);
// Response data.
String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
int statusCode = response.getStatusLine().getStatusCode();
response.close();
// Http code
if (statusCode != 200) {
throw new RuntimeException(String.format("http post failed! status=%d %s", statusCode, responseBody));
}
return responseBody;
}