Feign 的使用
服务拆分后,在一个服务中会经常需要调用到另外的服务。这种情况,除了使用 Dubbo 等 RPC 框架外,最简单的方法是通过 Spring Cloud Feign 来进行服务间的调用。
Feign 最终是通过代理使用 http 请求服务返回编码后的内容。使用 Feign 可以通过简单的申明去除手动发起 http 和编解码等复杂过程。
先看看 如何简单的使用 Feign。
- 首先引入
spring-cloud-dependencies
和spring-cloud-starter-openfeign
// 这里贴出了 feign 使用必须的包
org.springframework.cloud
spring-cloud-dependencies
Greenwich.SR4
pom
import
org.springframework.cloud
spring-cloud-starter-openfeign
2.2.3.RELEASE
compile
// ...
- 启动类上添加注解
@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 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 ,它们可以使用到连接池
如有疑问,欢迎留言交流