feign源码

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
简介: feign源码

一. feign做了哪些事?


1187916-20200805050001318-1407536376.png


上面是一段feign的代码, 系统是如何通过feign, 将reduceStock方法转换成stock服务的接口调用的呢?


他做了两件事


1. 讲reduceStock方法中的入参拼接到请求地址

2. 讲请求的域名解析对应到指定的服务ip+端口号port----这一步使用到了ribbon进行服务器的选择

3. 然后调用http请求, 发送请求到stock服务----通过ribbon封装的restTemplate, 发送请求


二. feign的入口



通常我们使用feign会怎么使用呢?


第一步: 在启动类加上@EnableFeignClients注解


package com.lxl.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class OrderApplication {
    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}


第二步: 在对应的client类上加上@FeignClient注解


@FeignClient(name = "stock")
public interface StockClient {
    @PostMapping("stock/reduce")
    String reduceStock();
    /**
     * http://stock-service/stock/deduct/{productId}/{stockCount}
     * @param productId
     * @param stockCount
     * @return
     */
    @PostMapping("reduce/count/{productId}/{stockCount}")
    String reduceStock(@PathVariable String productId, @PathVariable Integer stockCount);
}

那么看源码, 我们就从这两个注解入手.


首先看第一个注解@EnableFeignClients


三. EnableFeignClients



通过@EnableFeignClients可以直接定位到feign的源码位置.


首先, 还是第一步: 看spring.factories


1187916-20200805052704924-5469768.png


我们看到有一个FeignAutoConfiguration, 那么很有可能值就是feign的最开始的配置文件了. 我们来看看这个配置文件


3.1 FeignAutoConfiguration


这是一个配置类, 通常spring都是会通过一个AutoConfiguration来自动引入一些配置, 但是在feign的AutoConfiguration中没有引入太多的内容


/*
 * Copyright 2013-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.cloud.openfeign;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import javax.annotation.PreDestroy;
import feign.Client;
import feign.Feign;
import feign.httpclient.ApacheHttpClient;
import feign.okhttp.OkHttpClient;
import okhttp3.ConnectionPool;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.actuator.HasFeatures;
import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory;
import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
import org.springframework.cloud.openfeign.support.DefaultGzipDecoderConfiguration;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
 * @author Spencer Gibb
 * @author Julien Roy
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({ FeignClientProperties.class,
        FeignHttpClientProperties.class })
@Import(DefaultGzipDecoderConfiguration.class)
public class FeignAutoConfiguration {
    @Autowired(required = false)
    private List<FeignClientSpecification> configurations = new ArrayList<>();
    @Bean
    public HasFeatures feignFeature() {
        return HasFeatures.namedFeature("Feign", Feign.class);
    }
    @Bean
    public FeignContext feignContext() {
        FeignContext context = new FeignContext();
        context.setConfigurations(this.configurations);
        return context;
    }
  // 这个类定义的是和Hystrix有关的内容
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(name = "feign.hystrix.HystrixFeign")
    protected static class HystrixFeignTargeterConfiguration {
        @Bean
        @ConditionalOnMissingBean
        public Targeter feignTargeter() {
            return new HystrixTargeter();
        }
    }
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingClass("feign.hystrix.HystrixFeign")
    protected static class DefaultFeignTargeterConfiguration {
        @Bean
        @ConditionalOnMissingBean
        public Targeter feignTargeter() {
            return new DefaultTargeter();
        }
    }
    // the following configuration is for alternate feign clients if
    // ribbon is not on the class path.
    // see corresponding configurations in FeignRibbonClientAutoConfiguration
    // for load balanced ribbon clients.   // 这个也是有引入条件的, 如果使用了ApacheHttpClient, 那么使用这个配置内容
    @Configuration(proxyBeanMethods = false)
    @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);
        @Autowired(required = false)
        private RegistryBuilder registryBuilder;
        private CloseableHttpClient httpClient;
        @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() {
                @Override
                public void run() {
                    connectionManager.closeExpiredConnections();
                }
            }, 30000, httpClientProperties.getConnectionTimerRepeat());
            return connectionManager;
        }
        @Bean
        public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
                HttpClientConnectionManager httpClientConnectionManager,
                FeignHttpClientProperties httpClientProperties) {
            RequestConfig defaultRequestConfig = RequestConfig.custom()
                    .setConnectTimeout(httpClientProperties.getConnectionTimeout())
                    .setRedirectsEnabled(httpClientProperties.isFollowRedirects())
                    .build();
            this.httpClient = httpClientFactory.createBuilder()
                    .setConnectionManager(httpClientConnectionManager)
                    .setDefaultRequestConfig(defaultRequestConfig).build();
            return this.httpClient;
        }
        @Bean
        @ConditionalOnMissingBean(Client.class)
        public Client feignClient(HttpClient httpClient) {
            return new ApacheHttpClient(httpClient);
        }
        @PreDestroy
        public void destroy() throws Exception {
            this.connectionManagerTimer.cancel();
            if (this.httpClient != null) {
                this.httpClient.close();
            }
        }
    }
    // 这个类的引入条件是在OkHttpClient上. 也就是如果你使用了OkHttpClient类,那么会执行这段内容
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(OkHttpClient.class)
    @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
    @ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
    @ConditionalOnProperty("feign.okhttp.enabled")
    protected static class OkHttpFeignConfiguration {
        private okhttp3.OkHttpClient okHttpClient;
        @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);
        }
        @Bean
        public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
                ConnectionPool connectionPool,
                FeignHttpClientProperties httpClientProperties) {
            Boolean followRedirects = httpClientProperties.isFollowRedirects();
            Integer connectTimeout = httpClientProperties.getConnectionTimeout();
            Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
            this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation)
                    .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
                    .followRedirects(followRedirects).connectionPool(connectionPool)
                    .build();
            return this.okHttpClient;
        }
        @PreDestroy
        public void destroy() {
            if (this.okHttpClient != null) {
                this.okHttpClient.dispatcher().executorService().shutdown();
                this.okHttpClient.connectionPool().evictAll();
            }
        }
        @Bean
        @ConditionalOnMissingBean(Client.class)
        public Client feignClient(okhttp3.OkHttpClient client) {
            return new OkHttpClient(client);
        }
    }
}

如上注解, 我们看到在这个FeignAutoConfiguration中没有引入太多的东西. 很多内容都是有条件使用的.


在feign中, 有一个最重要的注解, 就是下面这个注解

1187916-20200805055110954-1850275288.png


这是Spring 的注解了, 我们知道引入配置文件的方式有很多. 其中一个就是使用Import, 下面来具体看看FeignClientsRegistrar都做了那些事情


3.2 FeignClientsRegistrar

class FeignClientsRegistrar
        implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

这个类实现了ImportBeanDefinitionRegistrar, 那么就要重写他的一个方法registerBeanDefinitions


 @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        registerDefaultConfiguration(metadata, registry);
        registerFeignClients(metadata, registry);
    }

1. registerDefaultConfiguration 看名字应该就能看出来, 这是一个引入默认的注册配置, 比如:我自定义在application中的配置, 在这时候读去出来, 进行加载


2. registerFeignClients: 这是一个主要的方法, 看名字就能猜出来, 这是注册feignClients, 我们在客户端自定义了很多带有@FeignClient的类, 就是扫描这些类.


public void registerFeignClients(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {     // 这里得到了一个扫描器
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        Set<String> basePackages;
     //  扫描@EnableFeignClients注解,及其下面的属性,包和子包
        Map<String, Object> attrs = metadata
                .getAnnotationAttributes(EnableFeignClients.class.getName());     // 过滤带有@FeignClient注解的包
        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);
            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);
            for (BeanDefinition candidateComponent : candidateComponents) {
                if (candidateComponent instanceof AnnotatedBeanDefinition) {
                    // verify annotated class is an interface
                    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);
                }
            }
        }
    }

1. 扫描器scanner扫描带有@EnableFiegnClients注解的包及其子包, 扫描带有@FeignClient注解的类, 使用过滤器扫描获得.


2. 把扫描的类放到spring容器里面

1187916-20200805064719005-833701326.png



1. 使用spring的动态代理获取带有@FeignClient注解的类,然后解析方法,将参数和路径进行拼接获得完整的url.


2. 通过LoadBalancerFeignClient的execute方法解析上一步获得的url,将域名进行负载均衡后找到对应的ip:port. 然后进行http服务请求,后面就是ribbon的逻辑了,可以参考ribbon的实现

相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
负载均衡 前端开发 Java
Spring Cloud Feign(声明式服务调用)使用指南
Spring Cloud Feign(声明式服务调用)使用指南
3490 0
Spring Cloud Feign(声明式服务调用)使用指南
|
6月前
Feign使用原理
Feign使用原理
136 0
|
8月前
|
负载均衡 Java 应用服务中间件
Ribbon、Feign和OpenFeign的区别来了
Ribbon、Feign和OpenFeign的区别来了
332 2
|
8月前
|
Java Apache
Feign
Feign
49 0
|
8月前
|
监控 安全 Java
深入理解Feign
深入理解Feign
258 2
|
缓存 负载均衡 Java
深入剖析ribbon源码
深入剖析ribbon源码
175 0
深入剖析ribbon源码
|
负载均衡 Dubbo Java
简单理解Feign的原理与使用
简单理解Feign的原理与使用
264 0
|
负载均衡 Java
10SpringCloud - Feign简介
10SpringCloud - Feign简介
45 0
|
存储 Java
Feign接口踩坑
由于业务需要,需要在接口中传递参数,调用消息中心的短信接口,进行短信的发送。如果使用Feign接口,没有携带token时,调用Feign接口,可以正常调用,但是如果携带token,就会出现appId拼接参数的情况。appId出现拼接时什么原因导致的呢?
279 0
Feign接口踩坑
|
负载均衡 算法 Java
SpringCloud-04 Feign学习笔记
SpringCloud-04 Feign学习笔记
SpringCloud-04 Feign学习笔记