写点什么

Spring Boot Feign 使用与源码学习

用户头像
Yangjing
关注
发布于: 2021 年 03 月 28 日

Feign 的使用

单体服务拆分微服务后,在一个服务中会经常需要调用到另外的服务。这种情况,除了使用 Dubbo 等 RPC 框架外,最简单的方法是通过 Spring Cloud Feign 来进行服务间的调用。

Feign 是通过代理使用 Http 请求服务返回编码后的内容。使用 Feign 可以通过简单的申明去除手动发起 Http 和编解码等复杂过程。

先看看 如何简单的使用 Feign。

  • 首先引入 `spring-cloud-dependencies` 和 `spring-cloud-starter-openfeign`

// 这里贴出了 feign 使用必须的包
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.3.RELEASE</version>
<scope>compile</scope>
</dependency>
// ...
</dependencies>
复制代码


  • 启动类上添加注解 `@EnableFeignClients`

@EnableFeignClients
public class MsApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(MsApplication.class, args);
}
}
复制代码


  • 定义 Client 类

// 如果配置的 url 不为空 ,实际会用 url+value 作为实际请求的地址
// 如果 url 为空,实际会请求 http://{name}+value 作为实际请求地址。这里 name 一般是注册中心对应的服务名
@FeignClient(name = "MyClient", url = "http://api.xxxx.cn/")
public interface MyClient {
// url+value 是远程服务的调用路径
@RequestMapping(method = RequestMethod.GET, value = "/api/path")
String getInfo(@RequestParam Long id);
}
复制代码


  • 使用 Client 类调用远程服务,这样调用使逻辑看上去是在面向对象编程,而不用再去手动处理 Http 请求。

// 实际调用远程服务
private MyClient myClient;
String res = myClient.getInfo(1L);
复制代码


Feign 源码解读

从项目启动、获取 Client 实例、实际方法调用 3 个过程分析 Feign 相关源码。

启动

  • spring-cloud-openfeign-core 包中通过 SPI 机制,运行 FeignAutoConfiguration 文件。根据配置生成 `org.springframework.cloud.openfeign.FeignContext`、`feign.Client`、`org.springframework.cloud.openfeign.Targeter` 3 个主要的类

@Configuration
@ConditionalOnClass({Feign.class})
@EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.class})
public class FeignAutoConfiguration {
@Bean
public FeignContext feignContext() {
FeignContext context = new FeignContext();
context.setConfigurations(this.configurations);
return context;
}
@Configuration
// 当项目中存在 OkHttpClient.class 类(项目引入了 OkHttpClient 的包)的时候会初始化下面类
@ConditionalOnClass({OkHttpClient.class})
@ConditionalOnMissingBean({okhttp3.OkHttpClient.class})
@ConditionalOnProperty({"feign.okhttp.enabled"})
protected static class OkHttpFeignConfiguration {
// OkHttpClient 可以使用连接池,这样可以减少 tcp 多次连接的开销。默认的 Client 是没有的
@Bean
@ConditionalOnMissingBean({ConnectionPool.class})
public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) {
Integer maxTotalConnections = httpClientProperties.getMaxConnections();
Long timeToLive = httpClientProperties.getTimeToLive();
TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
}
// ....
// 如果没有自定义 Client 时,这里会生成 OkHttpClient 作为 feign.Client 进行 http 调用的客户端
@Bean
@ConditionalOnMissingBean({Client.class})
public Client feignClient(okhttp3.OkHttpClient client) {
return new OkHttpClient(client);
}
}
@Configuration
// 同理,这里使用 ApacheHttpClient 的包
@ConditionalOnClass({ApacheHttpClient.class})
@ConditionalOnMissingClass({"com.netflix.loadbalancer.ILoadBalancer"})
@ConditionalOnMissingBean({CloseableHttpClient.class})
@ConditionalOnProperty(
value = {"feign.httpclient.enabled"},
matchIfMissing = true
)
protected static class HttpClientFeignConfiguration {
// 使用连接池时定时的清除过期的连接
private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer", true);
// 连接池的配置
@Bean
@ConditionalOnMissingBean({HttpClientConnectionManager.class})
public HttpClientConnectionManager connectionManager(ApacheHttpClientConnectionManagerFactory connectionManagerFactory, FeignHttpClientProperties httpClientProperties) {
final HttpClientConnectionManager connectionManager = connectionManagerFactory.newConnectionManager(httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(), httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getTimeToLive(), httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
// 初始化连接池过期连接的清除任务
this.connectionManagerTimer.schedule(new TimerTask() {
public void run() {
connectionManager.closeExpiredConnections();
}
}, 30000L, (long)httpClientProperties.getConnectionTimerRepeat());
return connectionManager;
}
// 如果没有自定义 Client 时,这里会生成 ApacheHttpClient 作为 feign.Client 进行 http 调用的客户端
@Bean
@ConditionalOnMissingBean({Client.class})
public Client feignClient(HttpClient httpClient) {
return new ApacheHttpClient(httpClient);
}
}
@Configuration
@ConditionalOnMissingClass({"feign.hystrix.HystrixFeign"})
protected static class DefaultFeignTargeterConfiguration {
protected DefaultFeignTargeterConfiguration() {
}
// 默认使用的 Targeter 类
@Bean
@ConditionalOnMissingBean
public Targeter feignTargeter() {
return new DefaultTargeter();
}
}
@Configuration
@ConditionalOnClass(
name = {"feign.hystrix.HystrixFeign"}
)
protected static class HystrixFeignTargeterConfiguration {
protected HystrixFeignTargeterConfiguration() {
}
@Bean
@ConditionalOnMissingBean
public Targeter feignTargeter() {
return new HystrixTargeter();
}
}
}
复制代码


  • `@EnableFeignClients` 注解中通过 `@Import` 导入 FeignClientsRegistrar.class 类,在类中注册 FeigntClient 的默认配置和扫描并注册 Client 类到容器中。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
// ...
}
复制代码


FeignClientsRegistrar 类解读

//
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
// 注册配置和 client 的 beanDefinition
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 注册 Feign 的默认配置
this.registerDefaultConfiguration(metadata, registry);
// 通过扫描有 `FeignClient.class` 注解的类,并注入到容器中
this.registerFeignClients(metadata, registry);
}
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
// 每个 Client 都是 FeignClientFactoryBean.class 类,获取类实例的时候是通过调用 FeignClientFactoryBean 类的 getObject() 方法获得
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
}
复制代码


获取 Client 实例

根据启动时注入的内容可知, 从容器获得 client 的实例是通过 FeignClientFactoryBean 类的 getObject() 方法获取。

最终得到的是一个 Proxy 代理,实例化过程见下图:


  • FeignContext、HystrixTargeter 都是启动的时候生成到容器的 Bean

  • Feign.Builder 是 Feign Client 的构建者

  • SynchronousMethodHandler.Factory 是 Client 中定义的方法拦截器的创建工厂,Client 中每个方法对应一个 SynchronousMethodHandler 处理器

  • ReflectiveFeign.ParseHandlersByName 将 Client 中的方法名解析得到不同的 SynchronousMethodHandler

  • ReflectiveFeign 是 Feign 类的唯一实现

  • InvocationHandlerFactory.Default 方法处理创建工厂的默认实现,生成代理类方法的处理实现

  • ReflectiveFeign.FeignInvocationHandler 代理类方法处理的默认类,是代理类拦截后的处理类,代理的方法会在它的 invoke() 方法中实现,它又是将拦截的方法转发到不同的 SynchronousMethodHandler 中进行处理

Client 方法调用

从上面获取 Client 实例的过程可以知道,在调 client 的方法时,实例调用的是 Proxy 类的方法,会对应的 SynchronousMethodHandler 拦截执行实际的逻辑。SynchronousMethodHandler 执行的逻辑如下:

public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
// 重试器,默认是 Retryer.Default 会重试 5次
Retryer retryer = this.retryer.clone();
while (true) {
try {
// 由启动时注入的 httpclient 发起 http 请求,并编码返回的内容
return executeAndDecode(template, options);
} catch (RetryableException e) {
try {
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
} } }
复制代码


  • Client 默认是 Client.Default 使用的是 HttpURLConnection 发起 http 调用

  • 推荐在实际过程中换成 OkHttpClient 或者 ApacheHttpClient ,它们可以使用到连接池

*如有疑问,欢迎留言交流*

发布于: 2021 年 03 月 28 日阅读数: 26
用户头像

Yangjing

关注

还未添加个人签名 2017.11.09 加入

还未添加个人简介

评论

发布
暂无评论
Spring Boot Feign 使用与源码学习