Apache HttpComponents学习笔记(四):HttpClient里的HttpRoute

本文探讨了Apache HttpClient中HttpRoute的概念,解释了如何在连接池设置中理解max per route参数。内容涉及RouteTracker和HttpRoute的区别,以及HttpClient如何根据目标主机、请求和上下文计算HttpRoute。文章指出HttpClient默认仅支持单个代理,并分析了建立连接过程中的RouteTracker作用。最后,文章留思考题关于routerPlanner在RetryExec中的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

讨论这个主题的起因是源于在学习Apache hc的连接池时,碰见max per route参数,即每路由最大连接数。对这里的“路由”产生了疑惑。

连接池设置这个参数的方法为:

public void setMaxPerRoute(final HttpRoute route, final int max)

从HttpRoute入手,其类图如下。HttpRoute对象是immutable的,包含的数据有目标主机、本地地址、代理链、是否tunnulled、是否layered、是否是安全路由。

RouteTracker和HttpRoute实现相同的接口RouteInfo,而且RouteTracker的字段和HttpRoute几乎相同,但RouteTracker提供了一些修改字段值得方法。仅从RouteTracker的类图来看,第一感觉RouteTracker是HttpRoute的构建器(toRoute是个典型的构建方法)。但是为什么叫RouteTracker而不是RouteBuilder?(问题1)


在继续后文的阅读前,建议先了解HttpClient的请求执行链。可以参考我之前的笔记: Apache HttpComponents学习笔记(二):HttpClient 接口

在HttpClient的请求执行过程中,HttpRoute首先出现在请求执行链的入口处,摘取InternalHttpClient的代码片断如下。在进入请求执行链之前,使用routePlanner来根据目标主机、请求和请求上下文来计算出HttpRoute.

@Override
protected CloseableHttpResponse doExecute(
        final HttpHost target,
        final HttpRequest request,
        final HttpContext context) throws IOException, ClientProtocolException {
		
	...
		
    <strong>final HttpRoute route = determineRoute(target, wrapper, localcontext);</strong>
	// 开始调用请求执行链
    return this.execChain.execute(<strong>route</strong>, wrapper, localcontext, execAware);
	
	...
}

private HttpRoute determineRoute(
        final HttpHost target,
        final HttpRequest request,
        final HttpContext context) throws HttpException {
    HttpHost host = target;
    if (host == null) {
        host = (HttpHost) request.getParams().getParameter(ClientPNames.DEFAULT_HOST);
    }
    <strong>return this.routePlanner.determineRoute(host, request, context);</strong>
}

routePlanner是HttpClientBuilder在构建HttpClient时装配的,摘取HttpClientBuilder相关代码片断如下。Apache hc默认提供了三种routePlanner的实现,它们的源码比较简单。Apache hc默认的计算HttpRoute的算法大致为:

  • 如果在请求上下文context里能找到代理,直接用来创建HttpRoute对象;
  • 如果没有设置代理,并且没有使用系统属性,用DefaultRoutePlanner,计算出的HttpRoute的proxyChain为null;
  • 如果设置了代理,用DefaultProxyRoutePlanner,计算出的HttpRoute的proxyChain里只有一个,就是设置的代理;
  • 如果没有设置代理,但使用系统数据,用SystemDefaultRoutePlanner,它使用JDK的ProxySelector,从JVM配置和系统配置里获取代理链,再从代理链里寻找第一个HTTP类型的代理,来创建HttpRoute对象。如果没有找到HTTP类型的代理,计算出的HttpRoute的proxyChain为null.

我们可以发现,Apache hc默认的计算HttpRoute的逻辑所产生的HttpRoute对象,其proxyChain里最多只有一个代理。为什么会是这样?HttpRoute的proxyChain字段被设计为List类型,但只含有一个代理。难道Apache hc不支持多代理?(问题2)

// 设置代理
public final HttpClientBuilder setProxy(final HttpHost proxy) {
    this.<strong>proxy</strong> = proxy;
    return this;
}

// 装配routePlanner
public final HttpClientBuilder setRoutePlanner(final HttpRoutePlanner routePlanner) {
    this.<strong>routePlanner </strong>= routePlanner;
    return this;
}

// 使用系统属性
public final HttpClientBuilder useSystemProperties() {
    <strong>systemProperties</strong> = true;
    return this;
}

// 构建HttpClient
public CloseableHttpClient build() {

    ...

    HttpRoutePlanner routePlanner = this.routePlanner;
    if (routePlanner == null) {
        SchemePortResolver schemePortResolver = this.schemePortResolver;
        if (schemePortResolver == null) {
            schemePortResolver = DefaultSchemePortResolver.INSTANCE;
        }
        if (proxy != null) {
            routePlanner = new <strong>DefaultProxyRoutePlanner</strong>(<strong>proxy</strong>, schemePortResolver);
        } else if (<strong>systemProperties</strong>) {
            routePlanner = new <strong>SystemDefaultRoutePlanner</strong>(
                    schemePortResolver, ProxySelector.getDefault());
        } else {
            routePlanner = new <strong>DefaultRoutePlanner</strong>(schemePortResolver);
        }
    }
	
	...

    return new InternalHttpClient(
            execChain,
            connManager,
            <strong>routePlanner</strong>,
            cookieSpecRegistry,
            authSchemeRegistry,
            defaultCookieStore,
            defaultCredentialsProvider,
            defaultRequestConfig != null ? defaultRequestConfig : RequestConfig.DEFAULT,
            closeables != null ? new ArrayList<Closeable>(closeables) : null);
}

HttpRoute在请求执行链入口处创建出来后,传递给请求执行链。顺着执行链走,在链条的最后一环MainClientExec里找到对HttpRoute对象的使用。

现在这段话,只是我的一点体悟,读者可以直接跳过,不影响阅读本文。其实在请求执行链里寻找使用HttpRoute的环节时,作者本人并没有从上至下看链条里各个环节的代码,也没有利用IDE里的Find all references,而是直接跳到了最后一环MainClientExec里。理由有两点:第一,“路由”的概念对应网络OSI模型的网络层。在请求执行链条里,除了最后一环,前面的各环,诸如重试、重导向、HTTP协议处理等,其功能对应OSI模型里较高的层次,只有最后一环的MainClientExec完成的是低层的处理,如建立连接、完成请求和响应的交换等;第二,前文提到,HttpRoute对象是immutable的,那么在链条传递过程中,HttpRoute对象里的字段不会发生变化。我的感悟是,一个合格的软件工程师,不仅要能掌握一两门编程语言,还应该具备一些必要的计算机知识。

MainClientExec使用HttpRoute对象的代码片断较复杂,我用抽取出来并作简化并加上注释,如下。

public CloseableHttpResponse execute(
        final HttpRoute route,
        final HttpRequestWrapper request,
        final HttpClientContext context,
        final HttpExecutionAware execAware) throws IOException, HttpException {

	...

	// 从connection manager里获取一个链接
    final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);
    final HttpClientConnection managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);

	...

    if (!managedConn.isOpen()) {
	    // 如果连接没有打开,根据HttpRoute来打开连接
        establishRoute(proxyAuthState, managedConn, route, request, context);
    }
	
	...
}

void establishRoute(
        final AuthState proxyAuthState,
        final HttpClientConnection managedConn,
        final HttpRoute route,
        final HttpRequest request,
        final HttpClientContext context) throws HttpException, IOException {
    <strong>final RouteTracker tracker = new RouteTracker(route);</strong>
    int step;
    do {
        <strong>final HttpRoute fact = tracker.toRoute();
        step = this.routeDirector.nextStep(route, fact);</strong>

        switch (step) {

        case HttpRouteDirector.CONNECT_TARGET:
		    // 打开到目标主机的连接
            this.connManager.connect(managedConn, route, timeout > 0 ? timeout : 0, context);
            <strong>tracker.connectTarget(route.isSecure())</strong>;
            break;
        case HttpRouteDirector.CONNECT_PROXY:
		    // 打开到代理机的连接
            this.connManager.connect(managedConn, route, timeout > 0 ? timeout : 0, context);
            final HttpHost proxy  = route.getProxyHost();
            <strong>tracker.connectProxy(proxy, false);</strong>
            break;
        case HttpRouteDirector.TUNNEL_TARGET: {
		    // 打开到目标主机的Tunnel连接
            final boolean secure = createTunnelToTarget(proxyAuthState, managedConn, route, request, context);
            this.log.debug("Tunnel to target created.");
            <strong>tracker.tunnelTarget(secure);</strong>
        }   break;

        case HttpRouteDirector.TUNNEL_PROXY: {
            // 在有多个代理的路由里,打开到下一条代理机的Tunnel连接
            final int hop = fact.getHopCount()-1; // the hop to establish
            final boolean secure = createTunnelToProxy(route, hop, context);
            this.log.debug("Tunnel to proxy created.");
            <strong>tracker.tunnelProxy(route.getHopTarget(hop), secure);</strong>
        }   break;

        case HttpRouteDirector.LAYER_PROTOCOL:
		    // 把连接重新绑定到layered socket
            this.connManager.upgrade(managedConn, route, context);
            <strong>tracker.layerProtocol(route.isSecure());</strong>
            break;

        case HttpRouteDirector.UNREACHABLE:
		    // 路由不可达,抛出异常
            throw new HttpException("Unable to establish route: " + "planned = " + route + "; current = " + fact);
        case HttpRouteDirector.COMPLETE:
		    // 路由完成
            this.connManager.routeComplete(managedConn, route, context);
            break;
        default:
		    // 非法
            throw new IllegalStateException("Unknown step indicator " + step + " from RouteDirector.");
        }

    } while (step > HttpRouteDirector.COMPLETE);
}

establishRoute方法的作用是打开连接,其主体逻辑是个循环。循环的终止条件由每次迭代调用的routeDirector.nextStep的返回结果step来控制:循环之前,先用HttpRoute来创建RouteTracker对象,此时RouteTracker只用HttpRoute的目标主机和本地地址来初始化。进入循环后,每次迭代向都调用RouteTracker的toRoute方法来获取RouteTracker当前跟踪的路由信息,然后routeDirector通过比较期望的路由和当前的路由,来计算得出下一步要进行的操作类型step,根据step,对连接采取相应操作,然后调用RouteTracker对应的方法来更改RouteTracker里的字段值。由此可以回答第一个问题:RouteTracker的命名表明了它的作用是作为一个跟踪器,在打开连接的过程中跟踪记录实际的路由。顺手贴一张RouteTracker的状态图。


routeDirector.nextStep是如何根据期望路由和实际路由计算出下一步的操作的?Apache hc提供了默认的BasicRouteDirector,其源代码比较简单,读者可以自行去看看,如果我有时间,会上一幅图描述。

现在值得关注的是上面代码的55 ~ 61行,routeDirector.nextStep会在下面所示的情况下会得出下一步操作的类型为TUNNEL_TARGET:

期望的路由:Source -> P1 -> P2 -> Target (3 hops),当前实际路由:Source -> P1 -> Target       (2 hops)

而前文提到,Apache hc默认的计算HttpRoute的逻辑所产生的HttpRoute对象,其proxyChain里最多只有一个代理,不会出现Source -> P1 -> P2 -> Target (3 hops)的情况,因此这部分代码是不会执行的。那我们手贱看看58行调用的createTunnelToProxy方法里在做什么:

private boolean createTunnelToProxy(
        final HttpRoute route,
        final int hop,
        final HttpClientContext context) throws HttpException {

    // Have a look at createTunnelToTarget and replicate the parts
    // you need in a custom derived class. If your proxies don't require
    // authentication, it is not too hard. But for the stock version of
    // HttpClient, we cannot make such simplifying assumptions and would
    // have to include proxy authentication code. The HttpComponents team
    // is currently not in a position to support rarely used code of this
    // complexity. Feel free to submit patches that refactor the code in
    // createTunnelToTarget to facilitate re-use for proxy tunnelling.

    throw new HttpException("Proxy chains are not supported.");
}

Apache hc明确告诉你不支持。问题2得到回答。

总结一下,Apache hc根据请求对象和用户配置来创建的HttpRoute,用于在请求执行链里传递的路由信息,并在MainClientExec这一环用路由信息来完成连接的打开。但是,目前Apache hc仅支持HttpRoute里最多一个代理,所以在Apache hc里,路由这个概念只关注是否是直接访问目标地址,还是通过设置代理来访问,其结构简化如下:

Source -> P(optional) -> Target

需要澄清的是,这里的目标主机和代理机的概念,与网络实际的物理拓扑是不同的:在网络里,客户端的请求会经过多跳才能到达目的主机,中间会经过各种网关、代理等。Apache hc里的代理更贴近于正常代理的概念,而且是请求传递过程中,HttpClient要明确感知到的第一个正向代理,并对随后是否有代理以及代理的拓扑结构,Apache hc不关心。举个简单的例子,通常在使用HttpClient向目标主机发送请求时,URI所代表的目标主机其实是负载均衡(反向代理),但是在HttpClient看来,这个负载均衡就是Target,而不是代理,此时的HttpClient构建的路由就是直连的。如果我们显示的通过HttpClientBuilder.setProxy来告诉HttpClient一个正向代理的存在,HttpClient所构建的路由就有一个跳转的代理。

留点思考题:routerPlanner不仅在InternalHttpClient里用来构建HttpRoute,还被RetryExec使用,为什么?

<think>好的,我现在需要帮用户解决一个关于Gradle构建时出现的多个META-INF/DEPENDERCIES文件冲突的问题。用户已经提供了错误信息,显示有五个来自不同Apache HttpClient相关依赖的jar包都包含META-INF/DEPENDENCIES路径的文件,导致构建失败。用户还提到Android文档建议添加packaging块来处理这个问题。 首先,我需要确认这个问题的根本原因。通常,当多个依赖项包含相同路径的文件时,Gradle在合并资源时会冲突,尤其是在Android项目中,因为Android插件默认会合并所有依赖项的资源。META-INF目录通常包含一些元数据,比如许可证信息或依赖列表,不同库可能会有自己的版本,导致冲突。 接下来,我需要回忆Android项目中处理这类问题的方法。根据用户提供的链接,解决方案是在Gradle的packagingOptions中排除这些重复的文件。用户可能需要在其模块的build.gradle文件中添加排除规则,告诉Gradle在合并时忽略这些特定的文件。 然后,我需要考虑如何正确配置packagingOptions。通常会使用exclude或pickFirst方法。exclude会完全排除匹配的文件,而pickFirst则是选择第一个遇到的并忽略后面的。在这个情况下,用户可能只需要排除META-INF/DEPENDENCIES文件,但需要确认是否还有其他冲突的文件,比如NOTICE或LICENSE,这些也可能导致问题,所以最好一并处理。 另外,需要注意各个依赖项的版本是否兼容。用户列出的依赖项中有httpmime、httpclient、httpcore-nio、httpasyncclient和httpcore,它们的版本有些是4.5.13,有些是4.4.13,还有httpasyncclient是4.1.5。版本不一致可能导致潜在的兼容性问题,但用户当前的问题主要是文件冲突,所以可能需要先解决冲突,再检查版本问题。 然后,我需要验证解决方案的正确性。添加packagingOptions块到android配置中,排除指定的文件。例如: android { packagingOptions { exclude 'META-INF/DEPENDENCIES' exclude 'META-INF/NOTICE' exclude 'META-INF/LICENSE' exclude 'META-INF/LICENSE.txt' exclude 'META-INF/NOTICE.txt' } } 这样配置后,Gradle在构建时会跳过这些文件,避免冲突。但需要注意,排除这些文件是否会影响库的功能,尤其是许可证文件,但通常这些文件只是元信息,不会影响运行时行为。 此外,用户可能需要同步Gradle项目,清理并重新构建,以确保更改生效。如果问题仍然存在,可能需要检查是否有其他依赖项也包含相同的文件,或者是否有其他配置冲突。 最后,还要提醒用户关于依赖版本的问题,建议统一使用相同版本的HttpComponents库,以避免潜在的兼容性问题。例如,将httpcore-nio和httpcore升级到与httpclient相同的4.5.13版本,或者确认各版本之间的兼容性。</think>--- ### **问题原因** 该警告提示在构建时检测到**5个依赖库**的JAR包中包含相同路径的文件`META-INF/DEPENDENCIES`,导致文件冲突。具体涉及的依赖库为: - `httpmime-4.5.13.jar` - `httpclient-4.5.13.jar` - `httpcore-nio-4.4.13.jar` - `httpasyncclient-4.1.5.jar` - `httpcore-4.4.13.jar` Android构建系统默认会合并所有依赖项的资源文件,但`META-INF/`目录下的文件(如许可证、依赖描述)通常具有唯一性要求,重复时会触发冲突。 --- ### **解决方案** 通过Gradle的`packagingOptions`配置,**手动排除冲突文件**或指定合并策略: #### 步骤1:在模块的`build.gradle`中添加配置 ```groovy android { packagingOptions { // 直接排除所有META-INF/DEPENDENCIES文件 exclude 'META-INF/DEPENDENCIES' // 可选:根据需要排除其他可能冲突的文件 exclude 'META-INF/NOTICE' exclude 'META-INF/LICENSE' exclude 'META-INF/LICENSE.txt' exclude 'META-INF/NOTICE.txt' } } ``` #### 步骤2:同步项目并重新构建 - 点击Android Studio的 **Sync Project with Gradle Files** 按钮 - 执行 **Clean Project** → **Rebuild Project** --- ### **原理说明** | 配置选项 | 作用 | |-------------------|----------------------------------------------------------------------| | `exclude` | 完全忽略匹配路径的文件,不参与构建 | | `merge` | 合并同名文件(默认行为) | | `pickFirst` | 选择第一个匹配的文件,忽略后续同名文件 | --- ### **扩展建议** 1. **依赖版本统一** 检查Apache HttpComponents相关依赖的版本是否一致: ```groovy // 示例:统一版本号 implementation 'org.apache.httpcomponents:httpclient:4.5.13' implementation 'org.apache.httpcomponents:httpmime:4.5.13' implementation 'org.apache.httpcomponents:httpasyncclient:4.1.5' // 注意:httpasyncclient 4.1.5对应httpcore-nio和httpcore的4.4.x版本 ``` 2. **验证排除影响** `META-INF/`中的文件通常为元数据,排除后一般不影响功能,但需测试核心逻辑是否正常。 3. **日志调试** 若问题未解决,启用Gradle调试日志定位具体冲突文件: ```bash ./gradlew assembleDebug --stacktrace --info ``` --- ### **参考文档** - [Android官方packagingOptions配置指南](https://developer.android.com/reference/tools/gradle-api/com/android/build/api/dsl/Packaging) - [Apache HttpComponents版本兼容性说明](https://hc.apache.org/httpcomponents-client-4.5.x/)
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值