OpenFeign 源码阅读 —— 负载均衡与客户端集成
lb:// 地址如何变成真实实例地址?HTTP 请求由谁发出?本文拆解 OpenFeign 的负载均衡客户端与可插拔 HTTP 客户端体系。
Client 接口:HTTP 客户端抽象
接口定义
java
// feign.Client
public interface Client {
Response execute(Request request, Request.Options options) throws IOException;
}客户端实现层级
Client(接口)
├── Client.Default JDK HttpURLConnection(默认,无连接池)
├── OkHttpClient OkHttp 适配
├── ApacheHttpClient Apache HttpClient 适配(连接池)
├── Http2Client HTTP/2 适配
│
└── LoadBalancerFeignClient 负载均衡包装(选中实例后委托下层 Client)
└── 委托真正发送的 Client负载均衡客户端
Spring Cloud 中的 Client 链
java
// spring-cloud-openfeign-core 自动配置
// FeignBlockingLoadBalancerClient 包装(新版 LoadBalancer)Feign 的 Client = FeignBlockingLoadBalancerClient(负载均衡层)
│
└─ 委托 → 实际 HTTP 客户端(OkHttp/Apache/Default)调用流程
java
proxy.getOrder(id) // name = "order-service"
│
▼
FeignBlockingLoadBalancerClient.execute(request, options)
│
├─ 1. 从 URL 解析服务名(http://order-service/api/... → order-service)
│
├─ 2. loadBalancerClient.choose("order-service")
│ → 返回一个实例(ip:port)(轮询/随机/权重)
│
├─ 3. 替换 URI:http://order-service → http://10.0.0.5:8080
│
└─ 4. delegate.execute(替换后的请求, options) → 真实发送源码
java
// org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient
public class FeignBlockingLoadBalancerClient implements Client {
private final Client delegate; // 实际 HTTP 客户端
private final LoadBalancerClient loadBalancerClient; // 负载均衡器
private final LoadBalancerClientFactory loadBalancerClientFactory;
@Override
public Response execute(Request request, Request.Options options) throws IOException {
// 1. 取原始 URI
URI originalUri = URI.create(request.url());
String serviceId = originalUri.getHost(); // 服务名
// 2. 请求体复制(可重复读)
Request lbRequest = new Request(request.httpMethod(), originalUri, ...);
// 3. 负载均衡选择实例
ServiceInstance instance = loadBalancerClient.choose(serviceId);
// 4. 重建 URI:替换 host:port 为实例地址
URI uri = loadBalancerClient.reconstructURI(instance, originalUri);
Request newRequest = new Request(request.httpMethod(), uri, ...);
// 5. 委托下层客户端发送
return delegate.execute(newRequest, options);
}
}旧版 LoadBalancerFeignClient
java
// spring-cloud-netflix(Ribbon 时代,已被替换)
public class LoadBalancerFeignClient implements Client {
private final Client delegate;
private final CachingSpringLoadBalancerFactory lbClientFactory;
private final LoadBalancerClient lbClient;
@Override
public Response execute(Request request, Request.Options options) throws IOException {
URI originalUri = URI.create(request.url());
String serviceId = originalUri.getHost();
// 用 Ribbon 的 LBClient 选择实例
LoadBalancerFeignClient ...
// 重试逻辑(RetryableFeignLoadBalancer)
}
}新旧对比:
| 版本 | 负载均衡客户端 | 底层负载均衡 |
|---|---|---|
| 旧(Ribbon) | LoadBalancerFeignClient | Netflix Ribbon |
| 新(LoadBalancer) | FeignBlockingLoadBalancerClient | Spring Cloud LoadBalancer |
各 HTTP 客户端适配
OkHttp 适配
java
// feign-okhttp
public class OkHttpClient implements Client {
private final okhttp3.OkHttpClient delegate; // OkHttp 实例(连接池)
@Override
public Response execute(Request request, Request.Options options) throws IOException {
// 把 feign.Request 转换为 okhttp3.Request
okhttp3.Request okRequest = toOkHttpRequest(request);
// 执行同步调用
okhttp3.Response okResponse = delegate.newCall(okRequest).execute();
// 把 okhttp3.Response 转换回 feign.Response
return toFeignResponse(okResponse, request);
}
}Apache HttpClient 适配
java
// feign-httpclient
public class ApacheHttpClient implements Client {
private final CloseableHttpClient delegate; // Apache 连接池
@Override
public Response execute(Request request, Request.Options options) throws IOException {
// 转换请求
HttpUriRequest apacheRequest = toApacheHttpRequest(request);
// 执行
CloseableHttpResponse apacheResponse = delegate.execute(apacheRequest);
// 转换响应
return toFeignResponse(apacheResponse, request);
}
}HTTP/2 适配
java
// feign-http2(基于 Netty)
public class Http2Client implements Client {
@Override
public Response execute(Request request, Request.Options options) throws IOException {
// 用 Netty HttpClient 发起 HTTP/2 请求(多路复用)
}
}客户端选型与配置
启用 OkHttp
yaml
feign:
httpclient:
enabled: false
okhttp:
enabled: truejava
// OkHttp 连接池配置
@Bean
public okhttp3.OkHttpClient okHttpClient() {
return new okhttp3.OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES)) // 连接池
.build();
}启用 Apache HttpClient
yaml
feign:
httpclient:
enabled: trueyaml
# Apache 连接池配置
feign:
httpclient:
max-connections: 200 # 总连接数
max-connections-per-route: 50 # 单路由连接数
time-to-live: 60
connection-timeout: 5000客户端对比
| 客户端 | 连接池 | HTTP/2 | 适用场景 |
|---|---|---|---|
| Client.Default | 无(每次新建) | 否 | 简单测试 |
| OkHttp | 有 | 是 | 生产推荐(轻量高效) |
| Apache HttpClient | 有 | 否 | 需要 Apache 生态特性 |
| Http2Client | 有 | 是 | 追求 HTTP/2 多路复用 |
请求超时配置
全局配置
yaml
feign:
client:
config:
default:
connectTimeout: 5000 # 连接超时(ms)
readTimeout: 10000 # 读取超时(ms)按服务配置
yaml
feign:
client:
config:
order-service: # 服务名
connectTimeout: 3000
readTimeout: 5000源码中的超时传递
java
// Request.Options
public class Options {
private final int connectTimeoutMillis; // 连接超时
private final int readTimeoutMillis; // 读取超时
private final boolean followRedirects;
private final TimeUnit connectTimeoutUnit;
private final TimeUnit readTimeoutUnit;
}
// 透传给底层 Client(OkHttp/Apache 等)完整调用时序
proxy.getOrder(id)
│
▼
SynchronousMethodHandler.invoke()
│
▼
client.execute(request, options) ← Client 是 FeignBlockingLoadBalancerClient
│
├─ 1. 解析服务名 order-service
├─ 2. LoadBalancer.choose() → 实例 10.0.0.5:8080
├─ 3. 重建 URI http://10.0.0.5:8080/api/order/1
│
▼
delegate.execute()(OkHttp/Apache)
│
├─ 连接池取连接 → 发送请求 → 获取响应
│
▼
返回 feign.Response → 解码 → 方法返回值常见问题
- lb:// 解析失败(UnknownHostException)? 未引入负载均衡客户端,或服务不在注册中心;检查
spring-cloud-starter-loadbalancer。 - 连接池耗尽? 调大 max-connections / per-route,检查下游响应是否过慢。
- OkHttp 与 Apache 同时启用? 有优先级配置,确认只启用一个避免混乱。
- 超时配置不生效? 检查是按服务还是全局配置、yml 层级是否正确(feign.client.config.xxx)。