Feign 的使用
服務(wù)拆分后拄踪,在一個服務(wù)中會經(jīng)常需要調(diào)用到另外的服務(wù)蝇恶。這種情況,除了使用 Dubbo 等 RPC 框架外惶桐,最簡單的方法是通過 Spring Cloud Feign 來進(jìn)行服務(wù)間的調(diào)用撮弧。
Feign 最終是通過代理使用 http 請求服務(wù)返回編碼后的內(nèi)容。使用 Feign 可以通過簡單的申明去除手動發(fā)起 http 和編解碼等復(fù)雜過程姚糊。
先看看 如何簡單的使用 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 一般是注冊中心對應(yīng)的服務(wù)名
@FeignClient(name = "MyClient", url = "http://api.xxxx.cn/")
public interface MyClient {
// url+value 是遠(yuǎn)程服務(wù)的調(diào)用路徑
@RequestMapping(method = RequestMethod.GET, value = "/api/path")
String getInfo(@RequestParam Long id);
}
- 使用 Client 類調(diào)用遠(yuǎn)程服務(wù),這樣調(diào)用使邏輯看上去是在面向?qū)ο缶幊坛Σ郏挥迷偃ナ謩犹幚?Http 請求擎淤。
// 實際調(diào)用遠(yuǎn)程服務(wù)
private MyClient myClient;
String res = myClient.getInfo(1L);
Feign 源碼解讀
從項目啟動、獲取 Client 實例秸仙、實際方法調(diào)用 3 個過程分析 Feign 相關(guān)源碼嘴拢。
啟動
- spring-cloud-openfeign-core 包中通過 SPI 機(jī)制,運行 FeignAutoConfiguration 文件寂纪。根據(jù)配置生成
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
// 當(dāng)項目中存在 OkHttpClient.class 類(項目引入了 OkHttpClient 的包)的時候會初始化下面類
@ConditionalOnClass({OkHttpClient.class})
@ConditionalOnMissingBean({okhttp3.OkHttpClient.class})
@ConditionalOnProperty({"feign.okhttp.enabled"})
protected static class OkHttpFeignConfiguration {
// OkHttpClient 可以使用連接池,這樣可以減少 tcp 多次連接的開銷抢腐。默認(rèn)的 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 進(jìn)行 http 調(diào)用的客戶端
@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);
// 初始化連接池過期連接的清除任務(wù)
this.connectionManagerTimer.schedule(new TimerTask() {
public void run() {
connectionManager.closeExpiredConnections();
}
}, 30000L, (long)httpClientProperties.getConnectionTimerRepeat());
return connectionManager;
}
// 如果沒有自定義 Client 時迈倍,這里會生成 ApacheHttpClient 作為 feign.Client 進(jìn)行 http 調(diào)用的客戶端
@Bean
@ConditionalOnMissingBean({Client.class})
public Client feignClient(HttpClient httpClient) {
return new ApacheHttpClient(httpClient);
}
}
@Configuration
@ConditionalOnMissingClass({"feign.hystrix.HystrixFeign"})
protected static class DefaultFeignTargeterConfiguration {
protected DefaultFeignTargeterConfiguration() {
}
// 默認(rèn)使用的 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
導(dǎo)入 FeignClientsRegistrar.class 類佩研,在類中注冊 FeigntClient 的默認(rèn)配置和掃描并注冊 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 的默認(rèn)配置
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 類投剥,獲取類實例的時候是通過調(diào)用 FeignClientFactoryBean 類的 getObject() 方法獲得
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
}
獲取 Client 實例
根據(jù)啟動時注入的內(nèi)容可知, 從容器獲得 client 的實例是通過 FeignClientFactoryBean 類的 getObject() 方法獲取迹鹅。
最終得到的是一個 Proxy 代理卦洽,實例化過程見下圖:
- FeignContext、HystrixTargeter 都是啟動的時候生成到容器的 Bean
- Feign.Builder 是 Feign Client 的構(gòu)建者
- SynchronousMethodHandler.Factory 是 Client 中定義的方法攔截器的創(chuàng)建工廠斜棚,Client 中每個方法對應(yīng)一個 SynchronousMethodHandler 處理器
- ReflectiveFeign.ParseHandlersByName 將 Client 中的方法名解析得到不同的 SynchronousMethodHandler
- ReflectiveFeign 是 Feign 類的唯一實現(xiàn)
- InvocationHandlerFactory.Default 方法處理創(chuàng)建工廠的默認(rèn)實現(xiàn)阀蒂,生成代理類方法的處理實現(xiàn)
- ReflectiveFeign.FeignInvocationHandler 代理類方法處理的默認(rèn)類,是代理類攔截后的處理類弟蚀,代理的方法會在它的 invoke() 方法中實現(xiàn)蚤霞,它又是將攔截的方法轉(zhuǎn)發(fā)到不同的 SynchronousMethodHandler 中進(jìn)行處理
Client 方法調(diào)用
從上面獲取 Client 實例的過程可以知道,在調(diào) client 的方法時义钉,實例調(diào)用的是 Proxy 類的方法昧绣,會對應(yīng)的 SynchronousMethodHandler 攔截執(zhí)行實際的邏輯。SynchronousMethodHandler 執(zhí)行的邏輯如下:
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
// 重試器捶闸,默認(rèn)是 Retryer.Default 會重試 5次
Retryer retryer = this.retryer.clone();
while (true) {
try {
// 由啟動時注入的 httpclient 發(fā)起 http 請求夜畴,并編碼返回的內(nèi)容
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 默認(rèn)是 Client.Default 使用的是 HttpURLConnection 發(fā)起 http 調(diào)用
- 推薦在實際過程中換成 OkHttpClient 或者 ApacheHttpClient ,它們可以使用到連接池
如有疑問删壮,歡迎留言交流