Appearance
OpenFeign生成代理类
一、前置知识:OpenFeign的核心设计目标
OpenFeign的核心是用“声明式接口”替代“手动HTTP请求”,因此需要为@FeignClient标记的接口生成代理对象——代理对象会拦截接口方法的调用,自动完成“HTTP请求构建→发送→响应解析”的全流程。
二、代理对象生成的完整流程
OpenFeign生成代理对象的过程,本质是Spring容器结合Feign框架,通过“工厂Bean+JDK动态代理”实现接口实例化。流程可分为以下5步:
1. 启动时扫描@FeignClient接口
- 当Spring Boot应用启动时,
@EnableFeignClients注解会触发Feign客户端扫描(类似@ComponentScan)。 - 扫描逻辑由
FeignClientScannerRegistrar实现:它会扫描basePackages(默认是启动类所在包)下所有被@FeignClient标记的接口。 - 对于每个扫描到的
@FeignClient接口,Spring会**注册一个FeignClientFactoryBean**到容器中(而非直接创建接口实例)。
2. FeignClientFactoryBean:代理对象的“工厂”
FeignClientFactoryBean是OpenFeign的核心工厂Bean(实现FactoryBean接口),负责为@FeignClient接口生成代理对象。其核心逻辑在getObject()方法中:
java
// FeignClientFactoryBean的核心方法
@Override
public Object getObject() throws Exception {
return getTarget(); // 关键:获取代理对象
}
<T> T getTarget() {
// 1. 获取Feign上下文(包含当前FeignClient的配置)
FeignContext context = applicationContext.getBean(FeignContext.class);
// 2. 构建Feign.Builder(Feign的核心构建器)
Feign.Builder builder = feign(context);
// 3. 处理服务地址(服务发现/直接URL)
if (!StringUtils.hasText(url)) {
// 若未指定url,通过服务发现获取实例(整合Eureka/Nacos)
url = "http://" + name; // name是@FeignClient的服务名
// 结合负载均衡器(Ribbon/Spring Cloud LoadBalancer)
builder.client(loadBalancerClientFactory.create(name));
}
// 4. 生成代理对象(关键步骤)
Targeter targeter = getTargeter();
return targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}关键细节:
FeignContext:每个@FeignClient都有独立的上下文,用于加载该客户端的自定义配置(如configuration属性指定的配置类)。Feign.Builder:Feign的核心构建器,用于配置编码器、解码器、日志、拦截器、错误处理器等组件(这些组件由FeignContext从Spring容器中获取)。Targeter:默认实现是DefaultTargeter,负责调用Feign.Builder生成代理对象。
3. Feign.Builder:组装代理对象的“零件”
Feign.Builder是Feign框架的核心构建类,它会整合以下关键组件(这些组件决定了代理对象的行为):
| 组件 | 作用 |
|---|---|
Encoder | 将方法参数序列化为HTTP请求体(默认JacksonEncoder,支持JSON) |
Decoder | 将HTTP响应体反序列化为接口返回类型(默认JacksonDecoder) |
Logger | 记录Feign请求日志(默认Slf4jLogger,配合Logger.Level控制日志级别) |
RequestInterceptor | 请求拦截器(统一添加Header、修改参数,如AuthInterceptor) |
ErrorDecoder | 自定义异常解码(将HTTP状态码转换为业务异常,如404→ResourceNotFoundException) |
Client | HTTP客户端(默认URLConnection,可替换为Apache HttpClient/OKHttp) |
示例:Builder的配置逻辑(来自FeignClientFactoryBean):
java
protected Feign.Builder feign(FeignContext context) {
Feign.Builder builder = Feign.builder()
.logger(logger)
.encoder(context.getBean(Encoder.class)) // 从上下文获取编码器
.decoder(context.getBean(Decoder.class)) // 从上下文获取解码器
.contract(context.getBean(Contract.class)) // 注解解析契约(默认SpringMvcContract)
.requestInterceptor(context.getBean(RequestInterceptor.class)); // 拦截器
// 其他配置(如日志级别、错误处理器)
return builder;
}4. 生成JDK动态代理对象
当Feign.Builder配置完成后,会调用target()方法生成JDK动态代理对象(因为@FeignClient标记的是接口,JDK代理天生支持接口代理)。
(1)关键类:HardCodedTarget
HardCodedTarget是Feign的Target接口实现类,用于封装接口类型、服务名、服务URL(如type=UserFeignClient.class,name=user-service,url=http://user-service)。
(2)生成代理的核心代码
Feign.Builder的target()方法最终会调用ReflectiveFeign.newInstance(),生成代理对象:
java
// ReflectiveFeign的核心方法(简化版)
public <T> T newInstance(Target<T> target) {
// 1. 解析接口方法元数据(关键!)
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<>();
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue; // 跳过Object类的方法(如toString())
}
// 解析方法上的注解(@GetMapping、@PathVariable等),生成MethodMetadata
MethodMetadata metadata = contract.parseAndValidateMetadata(target.type(), method);
// 创建MethodHandler(方法调用的处理器)
MethodHandler handler = factory.create(target, metadata);
methodToHandler.put(method, handler);
}
// 2. 创建InvocationHandler(代理对象的方法拦截器)
InvocationHandler invocationHandler = new FeignInvocationHandler(target, methodToHandler);
// 3. 生成JDK动态代理对象
return (T) Proxy.newProxyInstance(
target.type().getClassLoader(),
new Class<?>[]{target.type()},
invocationHandler
);
}关键细节:
- 方法元数据解析:
Contract接口(默认SpringMvcContract)负责解析接口方法上的注解(如@GetMapping、@RequestParam),生成MethodMetadata(包含请求方法、路径、参数映射、Header等信息)。 - MethodHandler:每个接口方法对应一个
MethodHandler,负责将方法调用转换为HTTP请求(如SynchronousMethodHandler是默认实现,同步发送请求)。 - FeignInvocationHandler:JDK代理的方法拦截器(实现
InvocationHandler接口),是代理对象的“大脑”——拦截所有接口方法的调用,转发给对应的MethodHandler处理。
5. FeignInvocationHandler:代理对象的“执行引擎”
当通过代理对象调用接口方法时,FeignInvocationHandler的invoke()方法会被触发,执行以下步骤:
java
// FeignInvocationHandler的核心方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 处理Object类的方法(如toString()、hashCode())
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
// 2. 获取当前方法对应的MethodHandler
MethodHandler handler = methodToHandler.get(method);
// 3. 调用MethodHandler处理请求(转换为HTTP请求并发送)
return handler.invoke(args);
}深入SynchronousMethodHandler.invoke()(默认MethodHandler的执行逻辑):
- 构建RequestTemplate:根据
MethodMetadata和方法参数,生成HTTP请求模板(包含URL、Header、参数、请求体)。 - 发送请求:通过
Client(如Apache HttpClient)发送HTTP请求,获取响应。 - 解析响应:通过
Decoder将响应体反序列化为接口方法的返回类型(如User对象)。 - 处理异常:若请求失败(如超时、服务不可达),通过
ErrorDecoder转换为业务异常,或触发熔断降级(Hystrix/Sentinel)。
三、关键组件总结
| 组件 | 作用 |
|---|---|
@EnableFeignClients | 开启Feign客户端扫描,触发FeignClientScannerRegistrar |
@FeignClient | 标记Feign接口,指定服务名、配置类、降级策略 |
FeignClientFactoryBean | 工厂Bean,负责创建Feign代理对象,整合Spring配置与Feign框架 |
Feign.Builder | Feign的核心构建器,配置编码器、解码器、拦截器等组件 |
ReflectiveFeign | 生成JDK动态代理对象,解析接口方法元数据,创建FeignInvocationHandler |
FeignInvocationHandler | JDK代理的方法拦截器,转发方法调用给MethodHandler |
SynchronousMethodHandler | 默认MethodHandler,将方法调用转换为HTTP请求并执行 |
四、为什么用JDK动态代理?
OpenFeign选择JDK动态代理而非CGLIB的原因:
@FeignClient标记的是接口,JDK代理天生支持接口代理(无需生成子类);- 接口是“声明式契约”的最佳载体,符合OpenFeign的设计理念(接口定义即服务契约);
- JDK代理更轻量,无需额外依赖(CGLIB需要引入第三方库)。
五、示例验证:代理对象的类型
可以通过以下代码验证Feign代理对象的类型:
java
@Autowired
private UserFeignClient userFeignClient;
@Test
public void testProxyType() {
// 输出:com.sun.proxy.$ProxyXX(JDK动态代理的类名)
System.out.println(userFeignClient.getClass().getName());
// 输出:true(代理对象实现了UserFeignClient接口)
System.out.println(userFeignClient instanceof UserFeignClient);
}总结:代理对象生成的核心逻辑链
@EnableFeignClients扫描→注册FeignClientFactoryBean→getObject()调用Feign.Builder→ReflectiveFeign生成JDK代理→FeignInvocationHandler拦截方法调用→SynchronousMethodHandler执行HTTP请求。
本质上,OpenFeign是通过Spring的工厂Bean机制,将Feign框架的动态代理能力整合到Spring容器中,让开发者可以像注入普通Bean一样使用Feign接口,而无需关心底层HTTP请求的细节。
