Feign集成原理

Feign是Netflix开发的声明式、模板化的HTTP客户端,可帮助我们更加便捷、优雅地调用HTTP API,Feign可以做到使用HTTP请求远程服务时就像调用本地方法一样的体验。

Spring Cloud整合Feign只需要引入以下依赖,且需要通过@EnableFeignClients注解开启Feign:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

日志配置

若需要需要对Feign的接口进行问题排查,或者想看看调用性能,就需要配置Feign的日志,可通过配置类的方式指定日志级别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 注意: 此处配置@Configuration注解就会全局生效,如果想指定对应微服务生效,就不能配置
public class FeignConfig {
/**
* 日志级别:
* NONE【性能最佳,适用于生产】:不记录任何日志(默认值)。
* BASIC【适用于生产环境追踪问题】:仅记录请求方法、URL、响应状态代码以及执行时间。
* HEADERS:记录BASIC级别的基础上,记录请求和响应的header。
* FULL【比较适用于开发及测试环境定位问题】:记录请求和响应的header、body和元数据。
*/
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}

局部配置,让调用的微服务生效,在@FeignClient注解中指定使用的配置类:

1
2
3
4
5
@FeignClient(value = "mall-order", path = "/order", configuration = FeignConfig.class, fallback = FallbackOrderFeignService.class)
public interface OrderFeignService {
@RequestMapping(value = "/findOrderByUserId/{userId}")
Object findOrderByUserId(@PathVariable(value = "userId") Integer userId);
}

局部配置还可以通过yml配置类完成,对应属性配置类为FeignClientProperties.FeignClientConfiguration,如拦截器、契约、超时时间等,Feign的底层用的是Ribbon,但超时时间以Feign配置为准

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
feign:
client:
config:
mall-order: #对应微服务
loggerLevel: FULL
contract: feign.Contract.Default #指定Feign原生注解契约配置
#配置拦截器
requestInterceptors[0]: com.eleven.***.interceptor.FeignAuthRequestInterceptor
# 连接超时时间,默认2s
connectTimeout: 5000
# 请求处理超时时间,默认5s
readTimeout: 10000
# 配置编解码器
encoder: feign.jackson.JacksonEncoder
decoder: feign.jackson.JacksonDecoder
httpclient:
enabled: true #feign 使用 Apache HttpClient 可以忽略,默认开启
okhttp:
enabled: true #feign 使用 okhttp
compression: # 配置 GZIP 来压缩数据
request:
enabled: true
# 配置压缩的类型
mime-types: text/xml,application/xml,application/json
# 最小压缩值
min-request-size: 2048
response:
enabled: true

Feign对SpringMvc的支持是通过配置契约来完成的,Spring Cloud中默认的是SpringMvcContract契约。若调用的接口有权限控制,可通过配置拦截器的方式实现,每次Feign发起HTTP调用之前,会去执行拦截器中的逻辑。

1
2
3
4
5
6
7
8
@Bean
public Contract feignContract() {
return new Contract.Default();
}
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("eleven", "123456");
}

集成原理

Feign的集成是通过@EnableFeignClients注解中导入了FeignClientsRegistrar类,该类实现了ImportBeanDefinitionRegistrar接口,故在Spring容器启动时会调用其registerBeanDefinitions完成将所有被@FeignClient注解标注的接口通过FeignClientFactoryBean注册到Spring容器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter); // 添加过滤器,过滤出所有被@FeignClient注解标注的类
basePackages = getBasePackages(metadata); // 获取扫描的包路径,若未配置则默认为当前类所在的包
} else {
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
}
for (String basePackage : basePackages) { // 遍历所有的包
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage); // 扫描出所有被@FeignClient注解标注的接口
for (BeanDefinition candidateComponent : candidateComponents) { // @FeignClient直接标注在接口上
if (candidateComponent instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
String name = getClientName(attributes); // 获取配置的客户端名称
registerClientConfiguration(registry, name, attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
private String getClientName(Map<String, Object> client) {
if (client == null) {
return null;
}
String value = (String) client.get("contextId");
if (!StringUtils.hasText(value)) { // 获取@FeignClient注解的value属性值
value = (String) client.get("value");
}
if (!StringUtils.hasText(value)) { // 获取@FeignClient注解的name属性值
value = (String) client.get("name");
}
if (!StringUtils.hasText(value)) { // 获取@FeignClient注解的serviceId属性值
value = (String) client.get("serviceId");
}
if (StringUtils.hasText(value)) {
return value;
}
throw new IllegalStateException("Either 'name' or 'value' must be provided in @" + FeignClient.class.getSimpleName());
}
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName(); // 获取@FeignClient注解标注的接口的全限定名
// 将BeanDefinition的beanClass设置为FeignClientFactoryBean.class
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes); // 校验@FeignClient注解fallback和fallbackFactory属性
definition.addPropertyValue("url", getUrl(attributes)); // 若@FeignClient注解中配置的是url对其进行处理
definition.addPropertyValue("path", getPath(attributes)); // 对@FeignClient注解中配置的是path属性进行处理
String name = getName(attributes); // 获取配置的客户端名称,即value或name或serviceId属性中配置的值,优先级从低到高
definition.addPropertyValue("name", name);
String contextId = getContextId(attributes); // 若默认未配置contextId注解,则默认为上面获取到的客户端名称name
definition.addPropertyValue("contextId", contextId);
definition.addPropertyValue("type", className); // 这里将原接口的Type设置到了FeignClientFactoryBean中,便于通过类型注入
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
public static BeanDefinitionBuilder genericBeanDefinition(Class<?> beanClass) {
BeanDefinitionBuilder builder = new BeanDefinitionBuilder(new GenericBeanDefinition());
builder.beanDefinition.setBeanClass(beanClass); // 将BeanDefinition的beanClass设置为FeignClientFactoryBean.class
return builder;
}
}

由于@FeignClient注解只能标注在接口上,而Spring容器中的Bean不能是接口,故这里通过FactoryBean的子类FeignClientFactoryBean来完成将被@FeignClient注解标注接口注册到Spring容器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
public Object getObject() throws Exception {
return getTarget();
}
<T> T getTarget() {
FeignContext context = applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context); // 对具体的Client接口进行配置,如encoder、decoder、retryer等
if (!StringUtils.hasText(url)) {
if (!name.startsWith("http")) {
url = "http://" + name;
} else {
url = name;
}
url += cleanPath(); // 对url进行拼接,例:http://mall-order/order
// 若FeignClient没有地址属性,用JDK动态代理生成FeignBlockingLoadBalancerClient代理
return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
}
if (StringUtils.hasText(url) && !url.startsWith("http")) {
url = "http://" + url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}
}

JDK动态代理生成FeignBlockingLoadBalancerClient代理,通过Feign接口时通过代理类最终会调用FeignBlockingLoadBalancerClient代理类的execute方法,最终会通过LoadBalancerClient去获取ServiceInstance让后调用具体的服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class FeignBlockingLoadBalancerClient implements Client {
private final Client delegate;
private final BlockingLoadBalancerClient loadBalancerClient;
public FeignBlockingLoadBalancerClient(Client delegate, BlockingLoadBalancerClient loadBalancerClient) {
this.delegate = delegate;
this.loadBalancerClient = loadBalancerClient;
}
public Response execute(Request request, Request.Options options) throws IOException {
final URI originalUri = URI.create(request.url());
String serviceId = originalUri.getHost();
Assert.state(serviceId != null, "Request URI does not contain a valid hostname: " + originalUri);
ServiceInstance instance = loadBalancerClient.choose(serviceId);
if (instance == null) {
return Response.builder().request(request)
.status(HttpStatus.SERVICE_UNAVAILABLE.value())
.body(message, StandardCharsets.UTF_8).build();
}
String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
Request newRequest = Request.create(request.httpMethod(), reconstructedUrl,
request.headers(), request.body(), request.charset(),
request.requestTemplate());
return delegate.execute(newRequest, options);
}
}

Feign中默认使用JDK原生的URLConnection发送HTTP请求,可集成别的组件来替换掉URLConnection,如Apache HttpClientOkHttp等。Feign发起调用真正执行逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public interface Client {
class Default implements Client {
public Response execute(Request request, Options options) throws IOException {
HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection, request);
}
}
}
@ConditionalOnClass(Feign.class)
@ConditionalOnBean(BlockingLoadBalancerClient.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
@AutoConfigureAfter(FeignRibbonClientAutoConfiguration.class)
@EnableConfigurationProperties(FeignHttpClientProperties.class)
@Configuration(proxyBeanMethods = false)
@Import({ HttpClientFeignLoadBalancerConfiguration.class,
OkHttpFeignLoadBalancerConfiguration.class,
DefaultFeignLoadBalancerConfiguration.class })
public class FeignLoadBalancerAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnBean(BlockingLoadBalancerClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
@Import(HttpClientFeignConfiguration.class)
class HttpClientFeignLoadBalancerConfiguration {
@Bean
@ConditionalOnMissingBean
public Client feignClient(BlockingLoadBalancerClient loadBalancerClient, HttpClient httpClient) {
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty("feign.okhttp.enabled")
@ConditionalOnBean(BlockingLoadBalancerClient.class)
@Import(OkHttpFeignConfiguration.class)
class OkHttpFeignLoadBalancerConfiguration {
@Bean
@ConditionalOnMissingBean
public Client feignClient(okhttp3.OkHttpClient okHttpClient, BlockingLoadBalancerClient loadBalancerClient) {
OkHttpClient delegate = new OkHttpClient(okHttpClient);
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient);
}
}
@Configuration(proxyBeanMethods = false)
class DefaultFeignLoadBalancerConfiguration {
@Bean
@ConditionalOnMissingBean
public Client feignClient(BlockingLoadBalancerClient loadBalancerClient) {
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient);
}
}