使用PoolingHttpClientConnectionManager优化HTTP连接管理的技巧,HttpClient 5迁移

使用PoolingHttpClientConnectionManager优化HTTP连接管理的技巧

一、PoolingHttpClientConnectionManager简介

Apache HttpClient是Java开发者使用最广泛的HTTP客户端库之一。它提供各种功能,包括多线程比较容易的应用、请求连接自动管理、SSL安全连接、HTTP状态不正常解决、Cookies管理等。PoolingHttpClientConnectionManager是Apache HttpClient中的一个连接管理器,它提供了一个基于连接池技术的HTTP连接管理器。使用连接池技术是为了避免为每个请求都创建和销毁一个连接的开销,而是通过连接池维护一个可重复使用的连接池,大大提高了性能。

二、创建HttpClient对象

在使用PoolingHttpClientConnectionManager优化HTTP连接管理之前,我们需要先创建一个HttpClient对象。在创建HttpClient对象时,我们需要使用PoolingHttpClientConnectionManager作为HttpClient对象的连接管理器参数。

CloseableHttpClient httpClient = HttpClients.custom()
        .setConnectionManager(new PoolingHttpClientConnectionManager())
        .build();

上面的代码创建了一个基于连接池技术的连接管理器,然后通过setConnectionManager方法将其设置为HttpClient对象的连接管理器,并且通过build方法创建了一个CloseableHttpClient对象。

三、连接池管理参数的配置

与其它连接池一样,PoolingHttpClientConnectionManager也提供了一些参数,可以调整池实现的外观和行为,这些参数可以通过调用某些方法来设置。下面是几个常用的池参数的设置方法:

1.设置每个路由的最大连接数

每个路由的最大连接可以被设成一个单独的值,这种情况下,最大连接数量不会超过这个值,也不会超过PoolConcurrencyPolicy所设定的上限。可以通过setDefaultMaxPerRoute方法来设置:

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// Increase max total connection to 200
cm.setMaxTotal(200);
// Increase default max connection per route to 20
cm.setDefaultMaxPerRoute(20);

2.设置最大连接数

通过setMaxTotal方法来设置池的最大连接数量:

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// Increase max total connection to 200
cm.setMaxTotal(200);

3.设置请求超时时间

可以通过setConnectionRequestTimeout方法来设置请求连接超时时间:

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// Increase max total connection to 200
cm.setMaxTotal(200);
// Set connection request timeout
cm.setConnectionRequestTimeout(5000);

4.设置连接超时时间和读取超时时间

可以通过setConnectTimeout和setSocketTimeout方法设置连接超时时间和读取超时时间:

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// Increase max total connection to 200
cm.setMaxTotal(200);
// Set connection timeout and socket timeout
cm.setConnectTimeout(5000);
cm.setSocketTimeout(10000);

四、使用连接池

完成以上设置后,我们就可以利用PoolingHttpClientConnectionManager的连接池技术,来优化HTTP连接管理。下面是使用连接池的代码:

// Create a new HTTP GET request
HttpGet httpGet = new HttpGet("http://www.example.com");

// Create HttpClient with a connection pool manager
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();

// Set the maximum number of total connections
cm.setMaxTotal(100);

// Create an HttpClient object
CloseableHttpClient httpClient = HttpClients.custom()
        .setConnectionManager(cm)
        .build();

// Execute the request
CloseableHttpResponse response = httpClient.execute(httpGet);

// Do something with response

// Release the connection
response.close();

五、结论

通过使用PoolingHttpClientConnectionManager优化HTTP连接管理,可以大大提高应用程序的性能。它采用连接池技术来维护一个可重复使用的连接池,避免为每个请求都创建和销毁一个连接的开销。在创建HttpClient对象时,需要使用PoolingHttpClientConnectionManager作为连接管理器参数。同时,也可以通过一些参数的设置,来调整连接池的行为。

Apache HttpClient5 RestTemplate 连接池_mob64ca140f67e3的技术博客_51CTO博客

一、背景

HTTP协议是无状态的协议,即每一次请求都是互相独立的。因此它的最初实现是,每一个http请求都会打开一个tcp socket连接,当交互完毕后会关闭这个连接。

HTTP协议是全双工的协议,所以建立连接与断开连接是要经过三次握手与四次挥手的。显然在这种设计中,每次发送Http请求都会消耗很多的额外资源,即连接的建立与销毁。

于是,HTTP协议的也进行了发展,通过持久连接的方法来进行socket连接复用。

Apache HttpClient5 RestTemplate 连接池_httpclient复用tcp

从图中可以看到:

  • 在串行连接中,每次交互都要打开关闭连接
  • 在持久连接中,第一次交互会打开连接,交互结束后连接并不关闭,下次交互就省去了建立连接的过程。

持久连接的实现有两种:HTTP/1.0+的keep-alive与HTTP/1.1的持久连接。

二、HTTP/1.0+的Keep-Alive

从1996年开始,很多HTTP/1.0浏览器与服务器都对协议进行了扩展,那就是“keep-alive”扩展协议。

注意,这个扩展协议是作为1.0的补充的“实验型持久连接”出现的。keep-alive已经不再使用了,最新的HTTP/1.1规范中也没有对它进行说明,只是很多应用延续了下来。

使用HTTP/1.0的客户端在首部中加上"Connection:Keep-Alive",请求服务端将一条连接保持在打开状态。服务端如果愿意将这条连接保持在打开状态,就会在响应中包含同样的首部。如果响应中没有包含"Connection:Keep-Alive"首部,则客户端会认为服务端不支持keep-alive,会在发送完响应报文之后关闭掉当前连接。

Apache HttpClient5 RestTemplate 连接池_连接池_02

通过keep-alive补充协议,客户端与服务器之间完成了持久连接,然而仍然存在着一些问题:

  • 在HTTP/1.0中keep-alive不是标准协议,客户端必须发送Connection:Keep-Alive来激活keep-alive连接。
  • 代理服务器可能无法支持keep-alive,因为一些代理是"盲中继",无法理解首部的含义,只是将首部逐跳转发。所以可能造成客户端与服务端都保持了连接,但是代理不接受该连接上的数据。

三、HTTP/1.1的持久连接

HTTP/1.1采取持久连接的方式替代了Keep-Alive。

HTTP/1.1的连接默认情况下都是持久连接。如果要显式关闭,需要在报文中加上Connection:Close首部。即在HTTP/1.1中,所有的连接都进行了复用。

然而如同Keep-Alive一样,空闲的持久连接也可以随时被客户端与服务端关闭。不发送Connection:Close不意味着服务器承诺连接永远保持打开。

四、HttpClient如何生成持久连接

HttpClien中使用了连接池来管理持有连接,同一条TCP链路上,连接是可以复用的。HttpClient通过连接池的方式进行连接持久化。

其实“池”技术是一种通用的设计,其设计思想并不复杂:

  • 当有连接第一次使用的时候建立连接
  • 结束时对应连接不关闭,归还到池中
  • 下次同个目的的连接可从池中获取一个可用连接
  • 定期清理过期连接

所有的连接池都是这个思路,不过我们看HttpClient源码主要关注两点:

  • 连接池的具体设计方案,以供以后自定义连接池参考
  • 如何与HTTP协议对应上,即理论抽象转为代码的实现

4.1 HttpClient连接池的实现

HttpClient关于持久连接的处理在下面的代码中可以集中体现,下面从MainClientExec摘取了和连接池相关的部分,去掉了其他部分:

public class MainClientExec implements ClientExecChain {

 @Override
 public CloseableHttpResponse execute(
  final HttpRoute route,
  final HttpRequestWrapper request,
  final HttpClientContext context,
  final HttpExecutionAware execAware) throws IOException, HttpException {
     //从连接管理器HttpClientConnectionManager中获取一个连接请求ConnectionRequest
 final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);final HttpClientConnection managedConn;
 final int timeout = config.getConnectionRequestTimeout(); //从连接请求ConnectionRequest中获取一个被管理的连接HttpClientConnection
 managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
     //将连接管理器HttpClientConnectionManager与被管理的连接HttpClientConnection交给一个ConnectionHolder持有
 final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn);
 try {
  HttpResponse response;
  if (!managedConn.isOpen()) {          //如果当前被管理的连接不是出于打开状态,需要重新建立连接
  establishRoute(proxyAuthState, managedConn, route, request, context);
  }
       //通过连接HttpClientConnection发送请求
  response = requestExecutor.execute(request, managedConn, context);
       //通过连接重用策略判断是否连接可重用  
  if (reuseStrategy.keepAlive(response, context)) {
  //获得连接有效期
  final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
  //设置连接有效期
  connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);          //将当前连接标记为可重用状态
  connHolder.markReusable();
  } else {
  connHolder.markNonReusable();
  }
 }
 final HttpEntity entity = response.getEntity();
 if (entity == null || !entity.isStreaming()) {
  //将当前连接释放到池中,供下次调用
  connHolder.releaseConnection();
  return new HttpResponseProxy(response, null);
 } else {
  return new HttpResponseProxy(response, connHolder);
 }
}
 
 

这里看到了在Http请求过程中对连接的处理是和协议规范是一致的,这里要展开讲一下具体实现。

PoolingHttpClientConnectionManager是HttpClient默认的连接管理器,首先通过requestConnection()获得一个连接的请求,注意这里不是连接。

public ConnectionRequest requestConnection(
  final HttpRoute route,
  final Object state) {final Future<CPoolEntry> future = this.pool.lease(route, state, null);
 return new ConnectionRequest() {
  @Override
  public boolean cancel() {
  return future.cancel(true);
  }
  @Override
  public HttpClientConnection get(
   final long timeout,
   final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {
  final HttpClientConnection conn = leaseConnection(future, timeout, tunit);
  if (conn.isOpen()) {
   final HttpHost host;
   if (route.getProxyHost() != null) {
   host = route.getProxyHost();
   } else {
   host = route.getTargetHost();
   }
   final SocketConfig socketConfig = resolveSocketConfig(host);
   conn.setSocketTimeout(socketConfig.getSoTimeout());
  }
  return conn;
  }
 };
 }
 
 

可以看到返回的ConnectionRequest对象实际上是一个持有了Future<CPoolEntry>,CPoolEntry是被连接池管理的真正连接实例。

从上面的代码我们应该关注的是:

Future<CPoolEntry> future = this.pool.lease(route, state, null)

  如何从连接池CPool中获得一个异步的连接,Future<CPoolEntry>

HttpClientConnection conn = leaseConnection(future, timeout, tunit)

  如何通过异步连接Future<CPoolEntry>获得一个真正的连接HttpClientConnection

4.2 Future<CPoolEntry>

看一下CPool是如何释放一个Future<CPoolEntry>的,AbstractConnPool核心代码如下:

private E getPoolEntryBlocking(
  final T route, final Object state,
  final long timeout, final TimeUnit tunit,
  final Future<E> future) throws IOException, InterruptedException, TimeoutException {
     //首先对当前连接池加锁,当前锁是可重入锁ReentrantLockthis.lock.lock();
 try {        //获得一个当前HttpRoute对应的连接池,对于HttpClient的连接池而言,总池有个大小,每个route对应的连接也是个池,所以是“池中池”
  final RouteSpecificPool<T, C, E> pool = getPool(route);
  E entry;
  for (;;) {
  Asserts.check(!this.isShutDown, "Connection pool shut down");          //死循环获得连接
  for (;;) {            //从route对应的池中拿连接,可能是null,也可能是有效连接
   entry = pool.getFree(state);            //如果拿到null,就退出循环
   if (entry == null) {
   break;
   }            //如果拿到过期连接或者已关闭连接,就释放资源,继续循环获取
   if (entry.isExpired(System.currentTimeMillis())) {
   entry.close();
   }
   if (entry.isClosed()) {
   this.available.remove(entry);
   pool.free(entry, false);
   } else {              //如果拿到有效连接就退出循环
   break;
   }
  }          //拿到有效连接就退出
  if (entry != null) {
   this.available.remove(entry);
   this.leased.add(entry);
   onReuse(entry);
   return entry;
  }
          //到这里证明没有拿到有效连接,需要自己生成一个  
  final int maxPerRoute = getMax(route);
  //每个route对应的连接最大数量是可配置的,如果超过了,就需要通过LRU清理掉一些连接
  final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);
  if (excess > 0) {
   for (int i = 0; i < excess; i++) {
   final E lastUsed = pool.getLastUsed();
   if (lastUsed == null) {
    break;
   }
   lastUsed.close();
   this.available.remove(lastUsed);
   pool.remove(lastUsed);
   }
  }
          //当前route池中的连接数,没有达到上线
  if (pool.getAllocatedCount() < maxPerRoute) {
   final int totalUsed = this.leased.size();
   final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);            //判断连接池是否超过上线,如果超过了,需要通过LRU清理掉一些连接
   if (freeCapacity > 0) {
   final int totalAvailable = this.available.size();               //如果空闲连接数已经大于剩余可用空间,则需要清理下空闲连接
   if (totalAvailable > freeCapacity - 1) {
    if (!this.available.isEmpty()) {
    final E lastUsed = this.available.removeLast();
    lastUsed.close();
    final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());
    otherpool.remove(lastUsed);
    }
   }              //根据route建立一个连接
   final C conn = this.connFactory.create(route);              //将这个连接放入route对应的“小池”中
   entry = pool.add(conn);              //将这个连接放入“大池”中
   this.leased.add(entry);
   return entry;
   }
  }
         //到这里证明没有从获得route池中获得有效连接,并且想要自己建立连接时当前route连接池已经到达最大值,即已经有连接在使用,但是对当前线程不可用
  boolean success = false;
  try {
   if (future.isCancelled()) {
   throw new InterruptedException("Operation interrupted");
   }            //将future放入route池中等待
   pool.queue(future);            //将future放入大连接池中等待
   this.pending.add(future);            //如果等待到了信号量的通知,success为true
   if (deadline != null) {
   success = this.condition.awaitUntil(deadline);
   } else {
   this.condition.await();
   success = true;
   }
   if (future.isCancelled()) {
   throw new InterruptedException("Operation interrupted");
   }
  } finally {
   //从等待队列中移除
   pool.unqueue(future);
   this.pending.remove(future);
  }
  //如果没有等到信号量通知并且当前时间已经超时,则退出循环
  if (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) {
   break;
  }
  }       //最终也没有等到信号量通知,没有拿到可用连接,则抛异常
  throw new TimeoutException("Timeout waiting for connection");
 } finally {       //释放对大连接池的锁
  this.lock.unlock();
 }
 }
 
 

上面的代码逻辑有几个重要点:

  • 连接池有个最大连接数,每个route对应一个小连接池,也有个最大连接数
  • 不论是大连接池还是小连接池,当超过数量的时候,都要通过LRU释放一些连接
  • 如果拿到了可用连接,则返回给上层使用
  • 如果没有拿到可用连接,HttpClient会判断当前route连接池是否已经超过了最大数量,没有到上限就会新建一个连接,并放入池中
  • 如果到达了上限,就排队等待,等到了信号量,就重新获得一次,等待不到就抛超时异常
  • 通过线程池获取连接要通过ReetrantLock加锁,保证线程安全

到这里为止,程序已经拿到了一个可用的CPoolEntry实例,或者抛异常终止了程序。

4.3 HttpClientConnection

protected HttpClientConnection leaseConnection(
  final Future<CPoolEntry> future,
  final long timeout,
  final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {
 final CPoolEntry entry;
 try {       //从异步操作Future<CPoolEntry>中获得CPoolEntry
  entry = future.get(timeout, tunit);
  if (entry == null || future.isCancelled()) {
  throw new InterruptedException();
  }
  Asserts.check(entry.getConnection() != null, "Pool entry with no connection");
  if (this.log.isDebugEnabled()) {
  this.log.debug("Connection leased: " + format(entry) + formatStats(entry.getRoute()));
  }       //获得一个CPoolEntry的代理对象,对其操作都是使用同一个底层的HttpClientConnection
  return CPoolProxy.newProxy(entry);
 } catch (final TimeoutException ex) {
  throw new ConnectionPoolTimeoutException("Timeout waiting for connection from pool");
 }
 }
 
 

五、HttpClient如何复用持久连接?

在上一章中,我们看到了HttpClient通过连接池来获得连接,当需要使用连接的时候从池中获得。

对应着第三章的问题:

  • 当有连接第一次使用的时候建立连接
  • 结束时对应连接不关闭,归还到池中
  • 下次同个目的的连接可从池中获取一个可用连接
  • 定期清理过期连接

我们在第四章中看到了HttpClient是如何处理1、3的问题的,那么第2个问题是怎么处理的呢?

即HttpClient如何判断一个连接在使用完毕后是要关闭,还是要放入池中供他人复用?再看一下MainClientExec的代码

//发送Http连接  response = requestExecutor.execute(request, managedConn, context);
  //根据重用策略判断当前连接是否要复用
  if (reuseStrategy.keepAlive(response, context)) {
   //需要复用的连接,获取连接超时时间,以response中的timeout为准
   final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
   if (this.log.isDebugEnabled()) {
   final String s;               //timeout的是毫秒数,如果没有设置则为-1,即没有超时时间
   if (duration > 0) {
    s = "for " + duration + " " + TimeUnit.MILLISECONDS;
   } else {
    s = "indefinitely";
   }
   this.log.debug("Connection can be kept alive " + s);
   }            //设置超时时间,当请求结束时连接管理器会根据超时时间决定是关闭还是放回到池中
   connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
   //将连接标记为可重用            connHolder.markReusable();
  } else {            //将连接标记为不可重用
   connHolder.markNonReusable();
  }
 
 

可以看到,当使用连接发生过请求之后,有连接重试策略来决定该连接是否要重用,如果要重用就会在结束后交给HttpClientConnectionManager放入池中。

那么连接复用策略的逻辑是怎么样的呢?

public class DefaultClientConnectionReuseStrategy extends DefaultConnectionReuseStrategy {

 public static final DefaultClientConnectionReuseStrategy INSTANCE = new DefaultClientConnectionReuseStrategy();

 @Override
 public boolean keepAlive(final HttpResponse response, final HttpContext context) {
     //从上下文中拿到request
  final HttpRequest request = (HttpRequest) context.getAttribute(HttpCoreContext.HTTP_REQUEST);
  if (request != null) {       //获得Connection的Header
   final Header[] connHeaders = request.getHeaders(HttpHeaders.CONNECTION);
   if (connHeaders.length != 0) {
    final TokenIterator ti = new BasicTokenIterator(new BasicHeaderIterator(connHeaders, null));
    while (ti.hasNext()) {
     final String token = ti.nextToken();            //如果包含Connection:Close首部,则代表请求不打算保持连接,会忽略response的意愿,该头部这是HTTP/1.1的规范
     if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) {
      return false;
     }
    }
   }
  }     //使用父类的的复用策略
  return super.keepAlive(response, context);
 }
}
 
 

看一下父类的复用策略

if (canResponseHaveBody(request, response)) {
    final Header[] clhs = response.getHeaders(HTTP.CONTENT_LEN);
    //如果reponse的Content-Length没有正确设置,则不复用连接          //因为对于持久化连接,两次传输之间不需要重新建立连接,则需要根据Content-Length确认内容属于哪次请求,以正确处理“粘包”现象    //所以,没有正确设置Content-Length的response连接不能复用
    if (clhs.length == 1) {
     final Header clh = clhs[0];
     try {
      final int contentLen = Integer.parseInt(clh.getValue());
      if (contentLen < 0) {
       return false;
      }
     } catch (final NumberFormatException ex) {
      return false;
     }
    } else {
     return false;
    }
   }
  if (headerIterator.hasNext()) {
   try {
    final TokenIterator ti = new BasicTokenIterator(headerIterator);
    boolean keepalive = false;
    while (ti.hasNext()) {
     final String token = ti.nextToken();            //如果response有Connection:Close首部,则明确表示要关闭,则不复用
     if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) {
      return false;            //如果response有Connection:Keep-Alive首部,则明确表示要持久化,则复用
     } else if (HTTP.CONN_KEEP_ALIVE.equalsIgnoreCase(token)) {
      keepalive = true;
     }
    }
    if (keepalive) {
     return true;
    }
   } catch (final ParseException px) {
    return false;
   }
  }
     //如果response中没有相关的Connection首部说明,则高于HTTP/1.0版本的都复用连接 
  return !ver.lessEquals(HttpVersion.HTTP_1_0);
 
 

总结一下:

  • 如果request首部中包含Connection:Close,不复用
  • 如果response中Content-Length长度设置不正确,不复用
  • 如果response首部包含Connection:Close,不复用
  • 如果reponse首部包含Connection:Keep-Alive,复用
  • 都没命中的情况下,如果HTTP版本高于1.0则复用

从代码中可以看到,其实现策略与我们第二、三章协议层的约束是一致的。

 六、HttpClient如何清理过期连接

在HttpClient4.4版本之前,在从连接池中获取重用连接的时候会检查下是否过期,过期则清理。

之后的版本则不同,会有一个单独的线程来扫描连接池中的连接,发现有离最近一次使用超过设置的时间后,就会清理。默认的超时时间是2秒钟。

public CloseableHttpClient build() {   //如果指定了要清理过期连接与空闲连接,才会启动清理线程,默认是不启动的
   if (evictExpiredConnections || evictIdleConnections) {          //创造一个连接池的清理线程
    final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm,
      maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS,
      maxIdleTime, maxIdleTimeUnit);
    closeablesCopy.add(new Closeable() {
     @Override
     public void close() throws IOException {
      connectionEvictor.shutdown();
      try {
       connectionEvictor.awaitTermination(1L, TimeUnit.SECONDS);
      } catch (final InterruptedException interrupted) {
       Thread.currentThread().interrupt();
      }
     }

    });          //执行该清理线程
    connectionEvictor.start();
}
 
 

可以看到在HttpClientBuilder进行build的时候,如果指定了开启清理功能,会创建一个连接池清理线程并运行它。

public IdleConnectionEvictor(
   final HttpClientConnectionManager connectionManager,
   final ThreadFactory threadFactory,
   final long sleepTime, final TimeUnit sleepTimeUnit,
   final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
  this.connectionManager = Args.notNull(connectionManager, "Connection manager");
  this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory();
  this.sleepTimeMs = sleepTimeUnit != null ? sleepTimeUnit.toMillis(sleepTime) : sleepTime;
  this.maxIdleTimeMs = maxIdleTimeUnit != null ? maxIdleTimeUnit.toMillis(maxIdleTime) : maxIdleTime;
  this.thread = this.threadFactory.newThread(new Runnable() {
   @Override
   public void run() {
    try {            //死循环,线程一直执行
     while (!Thread.currentThread().isInterrupted()) {              //休息若干秒后执行,默认10秒
      Thread.sleep(sleepTimeMs);               //清理过期连接
      connectionManager.closeExpiredConnections();               //如果指定了最大空闲时间,则清理空闲连接
      if (maxIdleTimeMs > 0) {
       connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS);
      }
     }
    } catch (final Exception ex) {
     exception = ex;
    }

   }
  });
 }
 
 

总结一下:

  • 只有在HttpClientBuilder手动设置后,才会开启清理过期与空闲连接
  • 手动设置后,会启动一个线程死循环执行,每次执行sleep一定时间,调用HttpClientConnectionManager的清理方法清理过期与空闲连接。

七、本文总结

  • HTTP协议通过持久连接的方式,减轻了早期设计中的过多连接问题
  • 持久连接有两种方式:HTTP/1.0+的Keep-Avlive与HTTP/1.1的默认持久连接
  • HttpClient通过连接池来管理持久连接,连接池分为两个,一个是总连接池,一个是每个route对应的连接池
  • HttpClient通过异步的Future<CPoolEntry>来获取一个池化的连接
  • 默认连接重用策略与HTTP协议约束一致,根据response先判断Connection:Close则关闭,在判断Connection:Keep-Alive则开启,最后版本大于1.0则开启
  • 只有在HttpClientBuilder中手动开启了清理过期与空闲连接的开关后,才会清理连接池中的连接
  • HttpClient4.4之后的版本通过一个死循环线程清理过期与空闲连接,该线程每次执行都sleep一会,以达到定期执行的效果

八、httpcleint线程池重要设置

Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create().register("http", plainsf).register("https", getSslFactory()).build();
        //创建连接池管理器
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(registry);
        // 最大连接数
        cm.setMaxTotal(maxTotal);
        // 默认的每个路由的最大连接数(也就是相同主机最大连接数)
        cm.setDefaultMaxPerRoute(maxPerRoute);
        //        HttpHost httpHost = new HttpHost(hostname, port);
        //        // 设置到某个路由的最大连接数,会覆盖defaultMaxPerRoute
        //        cm.setMaxPerRoute(new HttpRoute(httpHost), maxRoute);
 
 

setMaxTotal 和 setDefaultMaxPerRoute

HttpUtils工具类(一)常见的HttpUtils工具类及如何自定义java的http连接池-CSDN博客

HttpUtils工具类(二)Apache HttpClient 5 使用详细教程_apache client5-CSDN博客

HttpUtils工具类(三)OKHttpClient使用详细教程-CSDN博客

一、几种常见的Http调用方式

  1. Apache HttpClient
  2. OKhttpClient
  3. Hutool封装的HttpUtils
  4. Spring RestTemplate
  5. Java 原生的HttpURLConnection

 

1. 使用 Apache HttpClient

Apache HttpClient 是一个功能强大的 HTTP 客户端库,支持同步和异步请求。它适用于处理更加复杂的场景,如认证、连接池、多线程、上传文件等。

 

特点

  • 功能强大:Apache HttpClient 是一个久经考验的库,支持多种复杂的场景,包括连接池、代理、认证、重定向、Cookie 管理等。
  • 扩展性好:可以通过丰富的 API 进行灵活配置,满足复杂的企业级应用需求。
  • 同步阻塞:Apache HttpClient 默认是同步阻塞模式,适用于同步请求。

优点

  • 成熟稳定,经过长时间的验证,企业级项目中广泛使用。
  • 适合需要复杂 HTTP 操作的场景,如带有重试、认证和状态维护的请求。

缺点

  • 比较重量级,学习曲线稍陡峭。

一、Apache HttpClient 5介绍

Apache HttpClient 5 是一个功能齐全且高度可定制的 HTTP 客户端库, 其专门用于发送 HTTP 请求、处理 HTTP 响应并支持各种 HTTP 协议特性。特别适合处理复杂的 HTTP 请求需求,如多协议支持、认证、连接池、代理等。它适合中大型项目或需要高级 HTTP 特性的应用开发。

 

(1)核心特性

  1. 功能强大:

    • 同步与异步支持:支持同步和异步的 HTTP 请求处理,异步请求在处理大量并发请求时能够显著提高效率。
    • 连接池管理:内置的连接池机制能提高并发处理性能,减少资源消耗。
    • 多种认证机制:支持多种认证方式,包括 Basic、Digest、NTLM、Kerberos 等,能够处理多种安全场景。
    • 支持Cookie管理:内置 Cookie 管理功能,能够自动处理服务端返回的 Cookie 并在后续请求中使用,模拟浏览器行为。
  2. 协议支持广泛:

    • HTTP/1.1 和 HTTP/2 支持:支持现代 HTTP 协议,包括 HTTP/2 的多路复用和流优先级等特性,提升了网络请求效率;特别是 HTTP/2 多路复用的优势。
    • SSL/TLS 支持:支持 HTTPS,提供自定义 SSL/TLS 配置,确保通信的安全性。HttpClient 5 可以方便地处理 HTTPS 请求,支持定制化的 SSL/TLS 配置。
  3. 灵活性和可扩展性:

    • 易于扩展和定制:允许开发者根据需要进行灵活的定制,HttpClient 5 的设计高度模块化,用户可以根据需要对连接管理、重定向策略、请求重试策略、代理设置等进行灵活定制。
    • 代理支持:可以轻松配置 HTTP 或 SOCKS 代理,用于跨网络访问和隐私保护。
  4. 健壮的错误处理机制:

    • 自动重试和重定向处理:内置的重试机制和自动重定向处理,可以减少由于网络问题导致的失败请求。开发者可以定制是否启用自动重定向。

 

(2)Apache HttpClient 5 的新特性

与之前的 4.x 版本相比,HttpClient 5 进行了大量的改进和优化,特别是在性能和安全性方面:

  • HTTP/2 支持:提供了完整的 HTTP/2 支持,包括多路复用、流优先级等特性。
  • 更好的异步支持:在处理并发请求时,通过异步模型极大提升了响应速度和吞吐量。
  • 灵活的响应处理:通过改进的响应处理 API,可以更方便地处理大型响应体,避免内存溢出问题。

 

(3)在 Java 项目的主要使用场景及缺点

使用场景:

  1. RESTful API 客户端:与第三方 API 交互,发送各种 HTTP 请求。
  2. Web 爬虫:抓取网页内容,处理重定向、Cookie 等。
  3. 分布式系统通信:用于微服务间的 HTTP 通信。
  4. 测试与自动化:模拟 HTTP 请求,进行集成和自动化测试。
  5. 代理与网关:处理请求代理和认证机制。
  6. 文件上传下载:实现大文件的传输。
  7. 认证交互:处理 OAuth2、JWT 等认证协议。
  8. 安全通信:处理 HTTPS 请求,确保数据安全。

缺点:

  1. 学习曲线陡峭:高级特性(如自定义连接池、SSL 配置、异步请求等)较为复杂,新手需要时间学习和掌握。
  2. 体积较大:相比轻量级客户端,如 OkHttp,HttpClient 依赖库较多,可能不适合小型项目。
  3. 性能消耗高:默认配置下的内存和 CPU 占用较高,需调整才能达到最佳性能。
  4. 配置繁琐:高级定制(如连接管理、认证、代理)需要较多配置,增加开发复杂度。
  5. 异步编程复杂:异步请求的回调、错误处理等逻辑复杂,增加代码难度。

 

二、在实际项目中的应用

(1)引入maven配置

首先,你需要将 HttpClient 5 的依赖加入到项目中。pom.xml 文件如下:

 
  1.  
    <dependency>
  2.  
    <groupId>org.apache.httpcomponents.client5</groupId>
  3.  
    <artifactId>httpclient5</artifactId>
  4.  
    <version>5.1</version>
  5.  
    </dependency>
 

(2)自定义HttpUtils工具类---实现连接管理、重试策略等

 
  1.  
    import lombok.extern.slf4j.Slf4j;
  2.  
    import org.apache.hc.client5.http.DnsResolver;
  3.  
    import org.apache.hc.client5.http.classic.methods.HttpPost;
  4.  
    import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
  5.  
    import org.apache.hc.client5.http.config.RequestConfig;
  6.  
    import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
  7.  
    import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
  8.  
    import org.apache.hc.client5.http.impl.classic.HttpClients;
  9.  
    import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
  10.  
    import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
  11.  
    import org.apache.hc.core5.http.HttpEntity;
  12.  
    import org.apache.hc.core5.http.io.SocketConfig;
  13.  
    import org.apache.hc.core5.http.message.BasicHeader;
  14.  
    import org.apache.hc.core5.util.Timeout;
  15.  
    import org.springframework.stereotype.Component;
  16.  
     
  17.  
    import java.io.Closeable;
  18.  
    import java.io.IOException;
  19.  
    import java.net.InetAddress;
  20.  
    import java.net.URI;
  21.  
    import java.net.UnknownHostException;
  22.  
    import java.util.Map;
  23.  
     
  24.  
     
  25.  
    /**
  26.  
    * @author 响叮当
  27.  
    * @since 2024/8/15 14:10
  28.  
    **/
  29.  
    @Slf4j
  30.  
    @Component
  31.  
    public class ApacheHttpClientUtil {
  32.  
     
  33.  
    private CloseableHttpClient httpClient;
  34.  
     
  35.  
    @PostConstruct
  36.  
    public void init() {
  37.  
    SocketConfig socketConfig = SocketConfig.custom()
  38.  
    .setSoTimeout(Timeout.ofMilliseconds(1000))
  39.  
    .build();
  40.  
     
  41.  
    PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder
  42.  
    .create()
  43.  
    .setDefaultSocketConfig(socketConfig)
  44.  
    .setMaxConnTotal(1000)
  45.  
    .setMaxConnPerRoute(50)
  46.  
    .build();
  47.  
     
  48.  
    RequestConfig requestConfig = RequestConfig.custom()
  49.  
    .setConnectionRequestTimeout(Timeout.ofMilliseconds(8000))
  50.  
    .setResponseTimeout(Timeout.ofMilliseconds(8000))
  51.  
    .setConnectTimeout(Timeout.ofMilliseconds(8000))
  52.  
    .build();
  53.  
     
  54.  
    httpClient = HttpClients
  55.  
    .custom()
  56.  
    .disableContentCompression()
  57.  
    .setConnectionManager(connectionManager)
  58.  
    .setDefaultRequestConfig(requestConfig)
  59.  
    .build();
  60.  
    }
  61.  
     
  62.  
     
  63.  
    public CloseableHttpResponse getOrHead(String url, String method, Map<String, String> headers) throws IOException {
  64.  
    HttpUriRequestBase request = new HttpUriRequestBase(method, URI.create(url));
  65.  
    BasicHeader[] head = mapToHeaders(headers);
  66.  
    request.setHeaders(head);
  67.  
    return httpClient.execute(request);
  68.  
    }
  69.  
     
  70.  
     
  71.  
    public CloseableHttpResponse post(String url, Map<String, String> headers, HttpEntity httpEntity) throws IOException {
  72.  
    HttpPost request = new HttpPost(url);
  73.  
    BasicHeader[] head = mapToHeaders(headers);
  74.  
    request.setHeaders(head);
  75.  
    request.setEntity(httpEntity);
  76.  
    return httpClient.execute(request);
  77.  
    }
  78.  
     
  79.  
    public static BasicHeader[] mapToHeaders(Map<String, String> map) {
  80.  
    BasicHeader[] headers = new BasicHeader[map.size()];
  81.  
    int i = 0;
  82.  
    for (Map.Entry<String, String> entry : map.entrySet()) {
  83.  
    headers[i++] = new BasicHeader(entry.getKey(), entry.getValue());
  84.  
    }
  85.  
    return headers;
  86.  
    }
  87.  
     
  88.  
    public static void closeQuietly(Closeable is) {
  89.  
    if (is != null) {
  90.  
    try {
  91.  
    is.close();
  92.  
    } catch (Exception ex) {
  93.  
    log.error("Resources encounter an exception when closing,ex:{}", ex.getMessage());
  94.  
    }
  95.  
    }
  96.  
    }
  97.  
     
  98.  
    }
 

功能概述

  1. 初始化 HTTP 客户端init() 方法初始化 CloseableHttpClient,配置连接池、请求超时和套接字超时。
    • 连接池配置:通过 PoolingHttpClientConnectionManagerBuilder 设置最大连接数(1000)和每路由的最大连接数(50),用于高并发场景。
    • 请求配置:包括连接超时、请求超时和响应超时,确保在合理的时间内处理请求。
  2. HTTP 请求处理
    • GET/HEAD 请求getOrHead() 方法可发送 GET 或 HEAD 请求,并接受自定义请求头。
    • POST 请求post() 方法可发送 POST 请求,支持自定义请求头和实体。
  3. 资源管理closeQuietly() 方法用于关闭资源,避免抛出异常。

 

(3)代码中的具体实现

1、Get请求

 
  1.  
    @GetMapping("/get")
  2.  
    public void testGet() {
  3.  
    String url = "http://xxx.com.cn";
  4.  
    try {
  5.  
    // 1、构建入参、添加 headers
  6.  
    Map<String, String> headers = new HashMap<>();
  7.  
     
  8.  
    // 2、发起http请求
  9.  
    CloseableHttpResponse httpResponse = apacheHttpClientUtil.getOrHead(url, "GET", headers);
  10.  
     
  11.  
    // 3、返回结果,异常处理
  12.  
    if (httpResponse.getCode() == 200) {
  13.  
    String resStr = EntityUtils.toString(httpResponse.getEntity());
  14.  
    log.info("request_success, response:{}, httpResponse_Code:{}, reasonPhrase:{}", resStr, httpResponse.getCode(), httpResponse.getReasonPhrase());
  15.  
    } else {
  16.  
    log.error("request_fail, httpResponse_Code:{}, reasonPhrase:{}", httpResponse.getCode(), httpResponse.getReasonPhrase());
  17.  
    }
  18.  
    } catch (Exception e) {
  19.  
    log.error("request_udmp_fail, ex:{}", e);
  20.  
    }
  21.  
    }
 

 

2、POST请求-常规

 
  1.  
    @PostMapping("/post")
  2.  
    public void testPost(@RequestBody PostReq req) {
  3.  
    String url = "http://xxx.com.cn";
  4.  
    try {
  5.  
    // 1、构建入参、添加 headers
  6.  
    StringEntity stringEntity = new StringEntity(JSONUtil.toJsonStr(req), ContentType.APPLICATION_JSON);
  7.  
    Map<String, String> headers = new HashMap<>();
  8.  
     
  9.  
    // 2、发起http请求
  10.  
    CloseableHttpResponse httpResponse = apacheHttpClientUtil.post(url, headers, stringEntity);
  11.  
     
  12.  
    // 3、返回结果,异常处理
  13.  
    if (httpResponse.getCode() == 200) {
  14.  
    String resStr = EntityUtils.toString(httpResponse.getEntity());
  15.  
    log.info("request_success, response:{}, httpResponse_Code:{}, reasonPhrase:{}", resStr, httpResponse.getCode(), httpResponse.getReasonPhrase());
  16.  
    } else {
  17.  
    log.error("request_fail, httpResponse_Code:{}, reasonPhrase:{}", httpResponse.getCode(), httpResponse.getReasonPhrase());
  18.  
    }
  19.  
    } catch (Exception e) {
  20.  
    log.error("request_udmp_fail, ex:{}", e);
  21.  
    }
  22.  
    }
 

 

3、POST请求-上传文件

 
  1.  
    /**
  2.  
    * 上传文件
  3.  
    * @param multipartFile
  4.  
    * @param token
  5.  
    * @param key
  6.  
    * @return
  7.  
    */
  8.  
    @PostMapping("/uploadFile")
  9.  
    public UploadRes testPostFile(
  10.  
    @RequestParam("file") MultipartFile multipartFile,
  11.  
    @RequestParam("token") String token,
  12.  
    @RequestParam("key") String key
  13.  
    ) {
  14.  
    UploadRes res = null;
  15.  
    String url = "http://xxx.com.cn";
  16.  
    try {
  17.  
    // 1、构建File参数
  18.  
    File file = new File(multipartFile.getOriginalFilename());
  19.  
    try (FileOutputStream fos = new FileOutputStream(file)) {
  20.  
    fos.write(multipartFile.getBytes());
  21.  
    }
  22.  
    MultipartEntityBuilder builder = MultipartEntityBuilder.create();
  23.  
    builder.addBinaryBody("file", file, ContentType.MULTIPART_FORM_DATA, "ex.xlsx");
  24.  
     
  25.  
    // 2、构建其他参数
  26.  
    builder.addTextBody("token", token, ContentType.TEXT_PLAIN);
  27.  
    builder.addTextBody("key", key, ContentType.TEXT_PLAIN);
  28.  
    HttpEntity multipartEntity = builder.build();
  29.  
     
  30.  
    // 3、需要加 header,在这里加
  31.  
    Map<String, String> headers = new HashMap<>();
  32.  
     
  33.  
    // 4、发起http请求
  34.  
    CloseableHttpResponse httpResponse = apacheHttpClientUtil.post(url, headers, multipartEntity);
  35.  
     
  36.  
    // 5、返回结果,异常处理
  37.  
    if (httpResponse.getCode() == 200) {
  38.  
    String resStr = EntityUtils.toString(httpResponse.getEntity());
  39.  
    res = JSONUtil.toBean(resStr, UploadRes.class);
  40.  
    log.info("request_success, response:{}, httpResponse_Code:{}, reasonPhrase:{}", resStr, httpResponse.getCode(), httpResponse.getReasonPhrase());
  41.  
    } else {
  42.  
    log.error("request_fail, httpResponse_Code:{}, reasonPhrase:{}", httpResponse.getCode(), httpResponse.getReasonPhrase());
  43.  
    }
  44.  
    } catch (Exception e) {
  45.  
    log.error("request_udmp_fail, ex:{}", e);
  46.  
    }
  47.  
    return res;
  48.  
    }
 

 

(4)高级用法

1、处理重定向

HttpClient 5 默认会处理 3XX 重定向,但你也可以自定义行为。

 
  1.  
    CloseableHttpClient httpClient = HttpClients.custom()
  2.  
    .disableRedirectHandling() // 禁用自动重定向
  3.  
    .build();
 

 

2、SSL/TLS 支持

使用 HttpClient 5 可以轻松处理 HTTPS 请求,下面展示如何自定义 SSL 配置。

 
  1.  
    import org.apache.hc.core5.ssl.SSLContextBuilder;
  2.  
    import org.apache.hc.client5.http.impl.classic.HttpClients;
  3.  
    import javax.net.ssl.SSLContext;
  4.  
     
  5.  
    SSLContext sslContext = SSLContextBuilder.create()
  6.  
    .loadTrustMaterial((chain, authType) -> true) // 信任所有证书
  7.  
    .build();
  8.  
     
  9.  
    CloseableHttpClient httpClient = HttpClients.custom()
  10.  
    .setSSLContext(sslContext)
  11.  
    .build();
 

 

3、处理异步请求

如果你需要发送异步 HTTP 请求,可以使用 HttpAsyncClient

 
  1.  
    import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
  2.  
    import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
  3.  
    import org.apache.hc.core5.concurrent.FutureCallback;
  4.  
    import org.apache.hc.client5.http.classic.methods.HttpGet;
  5.  
     
  6.  
    CloseableHttpAsyncClient asyncClient = HttpAsyncClients.createDefault();
  7.  
     
  8.  
    asyncClient.start();
  9.  
     
  10.  
    HttpGet request = new HttpGet("https://jsonplaceholder.typicode.com/posts/1");
  11.  
     
  12.  
    asyncClient.execute(request, new FutureCallback<>() {
  13.  
    @Override
  14.  
    public void completed(CloseableHttpResponse response) {
  15.  
    System.out.println("Response received: " + response.getCode());
  16.  
    }
  17.  
     
  18.  
    @Override
  19.  
    public void failed(Exception ex) {
  20.  
    System.out.println("Request failed: " + ex.getMessage());
  21.  
    }
  22.  
     
  23.  
    @Override
  24.  
    public void cancelled() {
  25.  
    System.out.println("Request cancelled");
  26.  
    }
  27.  
    });
 

4、处理 Cookie

 
  1.  
    import org.apache.hc.client5.http.cookie.BasicCookieStore;
  2.  
    import org.apache.hc.client5.http.impl.classic.HttpClients;
  3.  
    import org.apache.hc.client5.http.cookie.CookieStore;
  4.  
     
  5.  
    CookieStore cookieStore = new BasicCookieStore();
  6.  
    CloseableHttpClient httpClient = HttpClients.custom()
  7.  
    .setDefaultCookieStore(cookieStore)
  8.  
    .build();
 

5、HTTPDNS支持

 
  1.  
    // 当域名为www.baidu.com结尾时候,转到local的机器上
  2.  
    String Target_IP= "127.0.0.1";
  3.  
    String[] domains = new String[]{"www.baidu.com"};
  4.  
     
  5.  
    DnsResolver dnsResolver = new DnsResolver() {
  6.  
    @Override
  7.  
    public InetAddress[] resolve(String host) throws UnknownHostException {
  8.  
    if (host.endsWith(domains[0]))) {
  9.  
    return new InetAddress[]{InetAddress.getByName(Target_IP)};
  10.  
    }
  11.  
    return new InetAddress[0];
  12.  
    }
  13.  
     
  14.  
    @Override
  15.  
    public String resolveCanonicalHostname(String s) {
  16.  
    return null;
  17.  
    }
  18.  
    };
  19.  
     
  20.  
     
  21.  
    // @PostConstruct
  22.  
    public void init() {
  23.  
    SocketConfig socketConfig = SocketConfig.custom()
  24.  
    .setSoTimeout(Timeout.ofMilliseconds(1000))
  25.  
    .build();
  26.  
     
  27.  
    PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder
  28.  
    .create()
  29.  
    .setDnsResolver(dnsResolver) // 这里是HttpDns配置
  30.  
    .setDefaultSocketConfig(socketConfig)
  31.  
    .setMaxConnTotal(1000)
  32.  
    .setMaxConnPerRoute(50)
  33.  
    .build();

    OkHttpClient 是一个由 Square 公司开发的 HTTP 客户端库,用于在 Android 和 Java 应用中进行网络请求。它支持同步和异步请求、连接池、超时设置、拦截器等功能,适合用于高性能网络请求,特别是在需要处理复杂的网络操作时。

     

    一、OKHttpClient介绍

    主要特点

    1. 同步和异步请求

      • 同步请求会在当前线程等待响应,适合不需要并发的简单请求。

      • 异步请求会将网络操作交由后台线程处理,不会阻塞主线程,适合需要并发处理或在 Android 等环境中使用。

    2. 连接池: OkHttp 默认会使用连接池来复用 HTTP 连接,从而提高性能,减少连接的建立和关闭的开销。

    3. 拦截器 (Interceptor): 拦截器允许在请求和响应时进行操作,例如可以在请求发送前添加认证信息,或在响应到达后进行日志记录。

    4. 自动处理 HTTP/2 和 SPDY: OkHttp 默认支持 HTTP/2 协议,可以提升多路复用性能,使多个请求共享一个 TCP 连接。

    5. 缓存机制: OkHttp 提供了默认的缓存机制,可以根据 HTTP 响应头自动缓存请求结果,减少重复网络请求。

    6. 超时控制: 可以对连接、读取和写入操作分别设置超时,避免长时间无响应的请求卡住应用。

     

     

    二、在实际项目中的应用

    (1)引入maven配置

     
    1.  
      <!-- ok的Http连接池 -->    
    2.  
      <dependency>
    3.  
         <groupId>com.squareup.okhttp3</groupId>
    4.  
         <artifactId>okhttp</artifactId>
    5.  
         <version>4.9.3</version>
    6.  
      </dependency>
     

    (2)自定义HttpUtils工具类

     
    1.  
      import com.alibaba.fastjson.JSONObject;
    2.  
      import com.yan.project.httpUtils.okHttp2.HttpRequestBody;
    3.  
      import lombok.extern.slf4j.Slf4j;
    4.  
      import okhttp3.*;
    5.  
       
    6.  
      import java.io.Closeable;
    7.  
      import java.io.File;
    8.  
      import java.io.IOException;
    9.  
      import java.io.InputStream;
    10.  
      import java.util.Map;
    11.  
      import java.util.Objects;
    12.  
      import java.util.Set;
    13.  
      import java.util.concurrent.TimeUnit;
    14.  
       
    15.  
      @Slf4j
    16.  
      public class OkHttpUtils {
    17.  
      private OkHttpUtils() {
    18.  
      throw new IllegalArgumentException("Utility class");
    19.  
      }
    20.  
       
    21.  
      private static final String MIME_JSON = "application/json
       

    在《扫清盲点,如何正确的从HttpClient 3.x系统升级到HttpClient 4.x》一文中已经详细的指明 HttpClient 3.x 迁移到 HttpClient 4.x 相关的变更点以及升级替换策略。本文针对HttpClient 4.5.x 之后 被 @Deprecated 注解后的废弃 / 过期 API(SSL证书验证相关),如何进行替换进行相关总结。

    1.   针对 SSLContext 和 SSLContextBuilder 过期的替换

    如果是在 HttpClient 4.5.x 之前,访问https的时候使用SSLContextBuilder来建立对象,那么在代码中会看到过期的语法提示,
    在IDEA中使用 Alt + Enter 会出现提示在4.5.x的SSLContextBuilder已经过期,替换策略为:删除原来的import,重新导入 httpcore jar中的SSLContextBuilder进行替换。

    经调查发现:SSLContext 和 SSLContextBuilder 的API从原来的 org.apache.http.conn.ssl 包挪到了 org.apache.http.ssl 包,基本的用法什么的都没有变化。

     替换代码如下,删除掉旧的import,导入新的 org.apache.http.ssl 下的引用

     
    1.  
      // Fixing deprecated code to use current HttpClient implementations Sekito.Lv 01/30/2019 11:29 Start
    2.  
      import org.apache.http.ssl.SSLContexts;
    3.  
      import org.apache.http.ssl.SSLContextBuilder;
    4.  
       
    5.  
      //import org.apache.http.conn.ssl.SSLContexts;
    6.  
      //import org.apache.http.conn.ssl.SSLContextBuilder;
    7.  
      // Fixing deprecated code to use current HttpClient implementations Sekito.Lv 01/30/2019 11:29 End
     

    2.    针对 4.4.x 之后版本 SSLConnectionSocketFactory 中静态变量过期的替换

    在 SSLConnectionSocketFactory 中静态变量有如下3个

    • STRICT_HOSTNAME_VERIFIER
    • BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
    • ALLOW_ALL__HOSTNAME_VERIFIER

    在代码源码中可以看到该三个静态变量都已经被 @Deprecated 注解了,在方法中使用会出现过期提示。

    替换策略如下

    在《扫清盲点,如何正确的从HttpClient 3.x系统升级到HttpClient 4.x》一文中HttpClient 3.x 和 4.x 废弃API一览里已经给出过期API的官方链接,在这里面可以查到新的替换API

    ALLOW_ALL_HOSTNAME_VERIFIER → NoopHostnameVerifier
    BROWSER_COMPATIBLE_HOSTNAME_VERIFIER → DefaultHostnameVerifier
    STRICT_HOSTNAME_VERIFIER →  DefaultHostnameVerifier

    示例代码

     
    1.  
      // Fixing deprecated code to use current HttpClient implementations Sekito.Lv 01/30/2019 11:29 Start
    2.  
       
    3.  
      // 4.4 之前用法,已经过期的API
    4.  
      // SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(
    5.  
      // sslContext, new String[] { "TLSv1" }, null,
    6.  
      // SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
    7.  
       
    8.  
      SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(
    9.  
      sslContext, new String[] { "TLSv1" }, null,
    10.  
      new DefaultHostnameVerifier());
    11.  
      // Fixing deprecated code to use current HttpClient implementations Sekito.Lv 01/30/2019 11:29 End
     

    注:本文原创由`bluetata`发布于blog.csdn.net、转载请务必注明出处。

     

    // 不重试 new DefaultHttpRequestRetryStrategy(0,TimeValue.ZERO_MILLISECONDS)
    //默认重试的 异常 和 返回code:
      Arrays.asList(
          InterruptedIOException.class,
          UnknownHostException.class,
          ConnectException.class,
          ConnectionClosedException.class,
          NoRouteToHostException.class,
          SSLException.class),
    Arrays.asList(
        HttpStatus.SC_TOO_MANY_REQUESTS,
        HttpStatus.SC_SERVICE_UNAVAILABLE)
        
    package org.apache.hc.client5.http;
    
    import java.io.IOException;
    import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
    import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
    import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
    import org.apache.hc.core5.http.ClassicHttpRequest;
    import org.apache.hc.core5.http.HttpRequest;
    import org.apache.hc.core5.http.HttpResponse;
    import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
    import org.apache.hc.core5.http.protocol.HttpContext;
    import org.apache.hc.core5.util.Args;
    import org.apache.hc.core5.util.TimeValue;
    
    /**
     * @Description: RetryTest
     * @Author: gaoyb
     * @Version: 2025/4/30 15:50
     */
    public class RetryTest {
    
      public static void main(String[] args) throws IOException {
        final ClassicHttpRequest httpGet = ClassicRequestBuilder.get("https://httpbin.org/status/503")
            .build();
    
        CloseableHttpClient httpClient = HttpClientBuilder
            .create()
            // .setRetryStrategy(new CustomRetryStrategy(3, TimeValue.ofSeconds(3)))
            .setRetryStrategy(
                // new DefaultHttpRequestRetryStrategy(
                //     0,
                //     TimeValue.ZERO_MILLISECONDS
                // )
                new DefaultHttpRequestRetryStrategy()
            )
            .build();
    
        httpClient.execute(httpGet, response -> {
          System.out.println(response.getCode() + " " + response.getReasonPhrase());
          return null;
        });
      }
    
      static class CustomRetryStrategy implements HttpRequestRetryStrategy {
    
        private final int maxRetries;
        private final TimeValue retryInterval;
    
        public CustomRetryStrategy(final int maxRetries, final TimeValue retryInterval) {
          this.maxRetries = maxRetries;
          this.retryInterval = retryInterval;
        }
    
        @Override
        public boolean retryRequest(
            final HttpRequest request,
            final IOException exception,
            final int execCount,
            final HttpContext context) {
          Args.notNull(request, "request");
          Args.notNull(exception, "exception");
    
          if (execCount > this.maxRetries) {
            // Do not retry if over max retries
            return false;
          }
          return true;
        }
    
        @Override
        public boolean retryRequest(
            final HttpResponse response,
            final int execCount,
            final HttpContext context) {
          Args.notNull(response, "response");
    
          return execCount <= this.maxRetries;
        }
    
        @Override
        public TimeValue getRetryInterval(HttpResponse response, int execCount, HttpContext context) {
          System.out.println("Retrying HTTP request after " + retryInterval.toString());
          return retryInterval;
        }
      }
    }
    package org.apache.hc.client5.http;
    
    import javax.net.ssl.SSLSession;
    import org.apache.hc.client5.http.classic.methods.HttpGet;
    import org.apache.hc.client5.http.config.ConnectionConfig;
    import org.apache.hc.client5.http.config.RequestConfig;
    import org.apache.hc.client5.http.config.TlsConfig;
    import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
    import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
    import org.apache.hc.client5.http.impl.classic.HttpClients;
    import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
    import org.apache.hc.client5.http.protocol.HttpClientContext;
    import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
    import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy;
    import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
    import org.apache.hc.client5.http.ssl.TrustAllStrategy;
    import org.apache.hc.core5.http.io.entity.EntityUtils;
    import org.apache.hc.core5.http.message.StatusLine;
    import org.apache.hc.core5.http.ssl.TLS;
    import org.apache.hc.core5.ssl.SSLContexts;
    import org.apache.hc.core5.util.TimeValue;
    import org.apache.hc.core5.util.Timeout;
    
    /**
     * @Description: RetryTest
     * @Author: gaoyb
     * @Version: 2025/4/30 15:50
     */
    public class SSLTest {
    
      public static void main(final String[] args) throws Exception {
    
        try (final CloseableHttpClient httpclient = HttpClients.custom()
            .setConnectionManager(
                /* HttpClientConnectionManager */ PoolingHttpClientConnectionManagerBuilder.create()
                    .setDefaultConnectionConfig(
                        ConnectionConfig.custom()
                            .setConnectTimeout(Timeout.ofSeconds(3))
                            .build()
                    )
                    .setTlsSocketStrategy( // setTlsSocketStrategy(TlsSocketStrategy) 替代 setSSLSocketFactory
                        /* TlsSocketStrategy  */   new DefaultClientTlsStrategy(
                            SSLContexts.custom() // 替代 SSLContextBuilder.create()
                                .loadTrustMaterial(TrustAllStrategy.INSTANCE)
                                .build(),
                            HostnameVerificationPolicy.CLIENT,
                            NoopHostnameVerifier.INSTANCE
                        )
                        // org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder.createConnectionOperator
                        // 替代 RegistryBuilder.<ConnectionSocketFactory>create()
                        //        .register("http", PlainConnectionSocketFactory.INSTANCE)
                        //        .register("https", new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE))
                    )
                    .setMaxConnTotal(200)// maxTotal > 0 ? maxTotal : defaultMaxTotal=setMaxTotal
                    .setMaxConnPerRoute(25)// maxPerRoute > 0 ? maxPerRoute : defaultMaxPerRoute=setDefaultMaxPerRoute
                    .setDefaultTlsConfig(
                        /* TlsConfig */   TlsConfig.custom()
                            .setHandshakeTimeout(Timeout.ofSeconds(3))
                            .setSupportedProtocols(TLS.V_1_0, TLS.V_1_1, TLS.V_1_2, TLS.V_1_3)
                            .build()
                    )
                    .build())
            .setDefaultRequestConfig(
                RequestConfig.custom()
                    .setConnectionRequestTimeout(Timeout.ofMinutes(3))
                    .setResponseTimeout(Timeout.ZERO_MILLISECONDS)// 超时值为零将解释为无限超时。
                    .build()
            )
            .setConnectionManagerShared(true)
            .setRoutePlanner(/* DefaultProxyRoutePlanner */ null)
            .setRetryStrategy(new DefaultHttpRequestRetryStrategy(0, TimeValue.ZERO_MILLISECONDS))//不重试
            .build();
    
            // HttpComponentsClientHttpRequestFactory requestFactory =//org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
            //     new HttpComponentsClientHttpRequestFactory();
            // requestFactory.setHttpClient(httpClient);// this method is not accepting the CloseableHttpClient object
            // requestFactory.setConnectTimeout(30000);
            // requestFactory.setReadTimeout(30000); //This method is deprecated in spring boot 3.0
            // requestFactory.setConnectionRequestTimeout(30000);
            // return new RestTemplate(requestFactory);
    
        ) {
    
          final HttpGet httpget = new HttpGet("https://httpbin.org/");
    
          System.out.println("Executing request " + httpget.getMethod() + " " + httpget.getUri());
          // 执行请求 [请求方法] [请求URI]
    
          final HttpClientContext clientContext = HttpClientContext.create();
          httpclient.execute(httpget, clientContext, response -> {
            System.out.println("----------------------------------------");
            System.out.println(httpget + "->" + new StatusLine(response));
            EntityUtils.consume(response.getEntity());
            final SSLSession sslSession = clientContext.getSSLSession();
            if (sslSession != null) {
              System.out.println("SSL protocol " + sslSession.getProtocol());
              // SSL 协议 [协议版本]
              System.out.println("SSL cipher suite " + sslSession.getCipherSuite());
              // SSL 加密套件 [加密套件名称]
            }
            return null;
          });
        }
    
      }
    
    }

     

posted @ 2025-04-28 15:01  CharyGao  阅读(1263)  评论(0)    收藏  举报