今天咱们来聊一个很多人面试经常被问,但答得稀里糊涂的问题——**Feign 第一次调用为什么会很慢?**你可能也经历过:接口调试好好的,第一次请求突然卡住,等了好几秒,第二次、第三次秒回,咋回事?是不是Feign有bug?其实不是,它背后有一些机制,咱们今天就给它扒一扒。
1 开门见山:第一次真的慢!
先说结论:Feign 第一次调用慢,通常是因为动态代理、懒加载、类初始化以及网络连接预热等多个因素叠加造成的。
我自己也在几个微服务项目里踩过这个坑,尤其是使用 Spring Cloud 的时候,服务刚启动完马上测试接口,第一次请求那是真的能等好几秒,特别是用 RestTemplate 调用不会慢,Feign 调用就卡一下。
那到底卡哪了?
2 你以为的Feign,其实是Spring的“把戏”
咱们先来捋清楚Feign的核心逻辑,它本质是用 Java 的动态代理,帮你在接口上织入逻辑,然后配合 Spring Boot 的注入机制,把它变成一个可调用的 bean。
简单点说,你写了个接口:
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User getUserById(@PathVariable("id") Long id);
}
你以为这个接口马上就能调用?其实不是,Spring Boot 启动的时候并没有马上实例化这个接口,而是等你第一次用到它的时候才开始执行一系列操作:找注册中心、解析配置、生成代理、连接目标服务……这几个步骤加起来,一慢就慢在第一发请求。
3 关键点:JDK动态代理+懒加载
这事其实跟“懒人策略”有关。Feign 默认的行为是懒加载(懒得加载),它会在真正调用的时候才去生成实际的代理对象。而代理对象是怎么来的呢?Feign 使用了 JDK 的动态代理机制,这玩意儿在第一次生成的时候会触发一系列反射操作,比如生成代理类、构建 MethodHandler、包装 HTTP 客户端调用等等。
这部分初始化逻辑,在你第一次 userClient.getUserById(1L)
的时候才开始跑。
那这一步到底干了些啥?看看简化版源码:
Feign.Builder builder = Feign.builder()
.decoder(new JacksonDecoder())
.encoder(new JacksonEncoder())
.target(UserClient.class, "http://user-service");
这一串代码其实就是在帮你动态生成 UserClient
的代理类,它要做的事情挺多:
-
编码请求参数
-
组装URL
-
调用底层HTTP客户端(默认是OkHttp或HttpURLConnection)
-
处理响应,反序列化为User对象
第一次这些逻辑都要初始化、加载class、创建对象,必然慢。
4 再慢一点:底层HTTP连接也要“热身”
如果你用了OkHttp或者Apache HttpClient,那第一次请求很可能还会卡在连接池建立和DNS解析阶段。
这两个动作都是懒执行的,比如第一次访问 http://user-service
,DNS 要解析、TCP 要连接、SSL(如果是https)还得握个手,这个成本都在第一次请求里了。
OkHttp 为例:
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("http://user-service/user/1")
.build();
Response response = client.newCall(request).execute();
这段代码你多跑几次就知道,第一次最慢,后面就快了,原因同上。
如果你在 Spring Cloud 里配了 ribbon 或 loadbalancer,其实还多了一层负载均衡客户端初始化的逻辑,也得考虑进去。
5 真正解决之道:提前初始化
如果你真想解决这个“首发慢”的问题,可以从两个角度下手:
1)预热机制我测试项目里最常用的做法,就是在服务启动后自动发一次请求,比如用 CommandLineRunner
写个请求逻辑,调用一次常用的Feign接口,让它“醒醒”。
@Component
public class FeignPreloader implements CommandLineRunner {
@Autowired
private UserClient userClient;
@Override
public void run(String... args) {
try {
userClient.getUserById(1L);
} catch (Exception ignored) {
}
}
}
别小看这一发请求,它能触发代理初始化、类加载、连接建立,真正的“热身动作”。
2)禁用懒加载(不推荐)有些人喜欢暴力解决,直接关掉懒加载,比如在配置里加上:
feign:
lazy-initialization: false
但这个方案我个人不太建议,理由很简单:如果你有几十个Feign Client,一次全初始化,会大大拉长服务启动时间,启动时间动不动多几秒,对 CI/CD 或线上故障恢复都不友好。
6 说点细节
对了,还有些朋友喜欢用 OpenFeign + Spring Boot 最新版,那注意了,从某些版本开始(Spring Cloud 2021以后)Feign 默认开启懒加载,这个跟你用的Spring版本有关系。如果你遇到特别明显的第一次卡顿,不妨看看是不是升级版本引起的。
另外,如果你底层 HTTP 客户端换成了 WebClient、或者用了某些支持连接复用的增强库,也可能能减轻这个问题,但本质原理还是一样:初始化很贵,预热能救命。
好了,今天这个问题咱们就聊到这。如果你之前也被这个“第一次调用慢”搞懵了,那现在应该清楚了:不是Feign本身慢,而是它在第一次调用之前“事太多”——生成代理、建立连接、初始化配置,一起扎堆上。
想优化?预热就是最好用的方法之一。