Spring Cloud——Feign设计原理

什么是Feign?

Feign 的英文表意为“假装,伪装,变形”, 是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。

Feign被广泛应用在Spring Cloud 的解决方案中,是学习基于Spring Cloud 微服务架构不可或缺的重要组件。

开源项目地址:https://github.com/OpenFeign/feign

Feign解决了什么问题?

封装了Http调用流程,更适合面向接口化的编程习惯
在服务调用的场景中,我们经常调用基于Http协议的服务,而我们经常使用到的框架可能有HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty等等,这些框架在基于自身的专注点提供了自身特性。而从角色划分上来看,他们的职能是一致的提供Http调用服务。具体流程如下:


Feign是如何设计的?

工作原理

  • 主程序入口添加了@EnableFeignClients注解开启对FeignClient扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClientd注解。

  • 当程序启动时,会进行包扫描,扫描所有@FeignClients的注解的类,并且将这些信息注入Spring IOC容器中,当定义的的Feign接口中的方法被调用时,通过JDK动态代理方式,来生成具体的RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装可HTTP请求需要的全部信息,如请求参数名,请求方法等信息都是在这个过程中确定的。

  • 然后RequestTemplate生成Request,然后把Request交给Client去处理,这里指的Client可以是JDK原生的URLConnection、Apache的HttpClient、也可以是OKhttp,最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。

1、基于面向接口的JDK动态代理方式生成实现类

在使用feign 时,会定义对应的接口类,在接口类上使用Http相关的注解,标识HTTP请求参数信息,如下所示:

interface GitHub {
    @RequestLine("GET /repos/{owner}/{repo}/contributors")
    List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
}

public static class Contributor {
    String login;
    int contributions;
}

public class MyApp {
    public static void main(String... args) {
        GitHub github = Feign.builder()
                         .decoder(new GsonDecoder())
                         .target(GitHub.class, "https://api.github.com");
  
        // Fetch and print a list of the contributors to this library.
        List<Contributor> contributors = github.contributors("OpenFeign", "feign");
        for (Contributor contributor : contributors) {
            System.out.println(contributor.login + " (" + contributor.contributions + ")");
        }
    }
}

在Feign 底层,通过基于面向接口的动态代理方式生成实现类,将请求调用委托到动态代理实现类,基本原理如下所示:


public class ReflectiveFeign extends Feign {

    @Override
    public <T> T newInstance(Target<T> target) {
        //根据接口类和Contract协议解析方式,解析接口类上的方法和注解,转换成内部的MethodHandler处理方式
        Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
        Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
        List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

        for (Method method : target.type().getMethods()) {
            if (method.getDeclaringClass() == Object.class) {
                continue;
            } else if (Util.isDefault(method)) {
                DefaultMethodHandler handler = new DefaultMethodHandler(method);
                defaultMethodHandlers.add(handler);
                methodToHandler.put(method, handler);
            } else {
                methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
            }
        }
        InvocationHandler handler = factory.create(target, methodToHandler);
        // 基于Proxy.newProxyInstance 为接口类创建动态实现,将所有的请求转换给InvocationHandler 处理。
        T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),new Class<?>[] {target.type()}, handler);

        for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
            defaultMethodHandler.bindTo(proxy);
        }
        return proxy;
    }
}

2、根据Contract协议规则,解析接口类的注解信息,解析成内部表现

Feign 定义了转换协议,定义如下:

public interface Contract {

    //传入接口定义,解析成相应的方法内部元数据表示
    List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType);
}
默认Contract 实现

Feign 默认有一套自己的协议规范,规定了一些注解,可以映射成对应的Http请求,如官方的一个例子:

public interface GitHub {
  
    @RequestLine("GET /repos/{owner}/{repo}/contributors")
    List<Contributor> getContributors(@Param("owner") String owner, @Param("repo") String repository);
  
    class Contributor {
        String login;
        int contributions;
    }
}

上述的例子中,尝试调用GitHub.getContributors(“foo”,“myrepo”)的的时候,会转换成如下的HTTP请求:

GET /repos/foo/myrepo/contributors
HOST XXXX.XXX.XXX
Feign 默认的协议规范
注解 接口Target 使用说明
@RequestLine 方法上 定义HttpMethod 和 UriTemplate. UriTemplate 中使用{} 包裹的表达式,可以通过在方法参数上使用@Param 自动注入
@Param 方法参数 定义模板变量,模板变量的值可以使用名称的方式使用模板注入解析
@Headers 类上或者方法上 定义头部模板变量,使用@Param 注解提供参数值的注入。如果该注解添加在接口类上,则所有的请求都会携带对应的Header信息;如果在方法上,则只会添加到对应的方法请求上
@QueryMap 方法上 定义一个键值对或者 pojo,参数值将会被转换成URL上的 query 字符串上
@HeaderMap 方法上 定义一个HeaderMap, 与 UrlTemplate 和HeaderTemplate 类型,可以使用@Param 注解提供参数值

具体FeignContract 是如何解析的,详情请参考代码:
https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Contract.java

2.1、基于Spring MVC的协议规范SpringMvcContract

OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解
当前Spring Cloud 微服务解决方案中spring-cloud-starter-openfeign,在Feign的基础上支持了Spring MVC的注解,OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,也就是说 ,写客户端请求接口和像写服务端代码一样:客户端和服务端可以通过SDK的方式进行约定,客户端只需要引入服务端发布的SDK API,就可以使用面向接口的编码方式对接服务:

3、基于RequestBean动态生成Request

根据传入的Bean对象和注解信息,从中提取出相应的值,来构造Http Request 对象:


4、使用Encoder 将Bean转换成 Http报文正文(消息解析和转码逻辑)

Feign 最终会将请求转换成Http 消息发送出去,传入的请求对象最终会解析成消息体,如下所示:


在接口定义上Feign做的比较简单,抽象出了Encoder 和decoder 接口:

public interface Encoder {
    Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;

    //将实体对象转换成Http请求的消息正文中
    void encode(Object var1, Type var2, RequestTemplate var3) throws EncodeException;

    public static class Default implements Encoder {
        public Default() {
        }

        public void encode(Object object, Type bodyType, RequestTemplate template) {
            if (bodyType == String.class) {
                template.body(object.toString());
            } else if (bodyType == byte[].class) {
                template.body((byte[])((byte[])object), (Charset)null);
            } else if (object != null) {
                throw new EncodeException(String.format("%s is not a type supported by this encoder.", object.getClass()));
            }

        }
    }
}
public interface Decoder {

    //从Response 中提取Http消息正文,通过接口类声明的返回类型,消息自动装配
    Object decode(Response response, Type type) throws IOException, DecodeException, FeignException;
    
    public class Default extends StringDecoder {

        @Override
        public Object decode(Response response, Type type) throws IOException {
            if (response.status() == 404 || response.status() == 204)
                return Util.emptyValueOf(type);
            if (response.body() == null)
                return null;
            if (byte[].class.equals(type)) {
                return Util.toByteArray(response.body().asInputStream());
            }
            return super.decode(response, type);
        }
    }
}   

目前Feign 有以下实现:

Encoder/ Decoder 实现 说明
JacksonEncoder,JacksonDecoder 基于 Jackson 格式的持久化转换协议
GsonEncoder,GsonDecoder 基于Google GSON 格式的持久化转换协议
SaxEncoder,SaxDecoder 基于XML 格式的Sax 库持久化转换协议
JAXBEncoder,JAXBDecoder 基于XML 格式的JAXB 库持久化转换协议
ResponseEntityEncoder,ResponseEntityDecoder Spring MVC 基于ResponseEntity< T > 返回格式的转换协议
SpringEncoder,SpringDecoder 基于Spring MVC HttpMessageConverters 一套机制实现的转换协议 ,应用于Spring Cloud 体系中

5、拦截器负责对请求和返回进行装饰处理

在请求转换的过程中,Feign 抽象出来了拦截器接口,用于用户自定义对请求的操作:

public interface RequestInterceptor {

    /**
     * 可以在构造RequestTemplate 请求时,增加或者修改Header, Method, Body 等信息
     */
    void apply(RequestTemplate template);
}

比如,如果希望Http消息传递过程中被压缩,可以定义一个请求拦截器:

public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {

    /**
     * Creates new instance of {@link FeignAcceptGzipEncodingInterceptor}.
     * @param properties the encoding properties
     */
    protected FeignAcceptGzipEncodingInterceptor(
            FeignClientEncodingProperties properties) {
        super(properties);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void apply(RequestTemplate template) {
        //  在Header 头部添加相应的数据信息
        addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER,
                HttpEncoding.GZIP_ENCODING, HttpEncoding.DEFLATE_ENCODING);
    }

}

6、日志记录

在发送和接收请求的时候,Feign定义了统一的日志门面来输出日志信息 , 并且将日志的输出定义了四个等级:

级别 说明
NONE 不做任何记录
BASIC 只记录输出Http 方法名称、请求URL、返回状态码和执行时间
HEADERS 记录输出Http 方法名称、请求URL、返回状态码和执行时间 和 Header 信息
FULL 记录Request 和Response的Header,Body和一些请求元数据
public abstract class Logger {

    /**
     * Controls the level of logging.
     */
    public enum Level {
        /**
         * No logging.
         */
        NONE,
        /**
         * Log only the request method and URL and the response status code and execution time.
         */
        BASIC,
        /**
         * Log the basic information along with request and response headers.
         */
        HEADERS,
        /**
         * Log the headers, body, and metadata for both requests and responses.
         */
        FULL
    }
}

7、基于重试器发送HTTP请求

Feign 内置了一个重试器,当HTTP请求出现IO异常时,Feign会有一个最大尝试次数发送请求,以下是Feign核心
代码逻辑:

final class SynchronousMethodHandler implements MethodHandler {

    @Override
    public Object invoke(Object[] argv) throws Throwable {
        //根据输入参数,构造Http 请求。
        RequestTemplate template = buildTemplateFromArgs.create(argv);
        Options options = findOptions(argv);
        // 克隆出一份重试器
        Retryer retryer = this.retryer.clone();
        // 尝试最大次数,如果中间有结果,直接返回
        while (true) {
            try {
                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;
            }
        }
    }
}

重试器有如下几个控制参数:

重试参数 说明 默认值
period 初始重试时间间隔,当请求失败后,重试器将会暂停 初始时间间隔(线程 sleep 的方式)后再开始,避免强刷请求,浪费性能 100ms
maxPeriod 当请求连续失败时,重试的时间间隔将按照:long interval = (long) (period * Math.pow(1.5, attempt - 1)); 计算,按照等比例方式延长,但是最大间隔时间为 maxPeriod, 设置此值能够避免 重试次数过多的情况下执行周期太长 1000ms
maxAttempts 最大重试次数 5

具体的代码实现可参考:
https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Retryer.java

8、发送Http请求

Feign 真正发送HTTP请求是委托给 feign.Client 来做的:

public interface Client {
    
    //执行Http请求,并返回Response
    Response execute(Request request, Options options) throws IOException;
    
}

Feign 默认底层通过JDK 的 java.net.HttpURLConnection 实现了feign.Client接口类,在每次发送请求的时候,都会创建新的HttpURLConnection 链接,这也就是为什么默认情况下Feign的性能很差的原因。可以通过拓展该接口,使用Apache HttpClient 或者OkHttp3等基于连接池的高性能Http客户端,我们项目内部使用的就是OkHttp3作为Http 客户端。

三、Feign性能优化

Feign 整体框架非常小巧,在处理请求转换和消息解析的过程中,基本上没什么时间消耗。真正影响性能的,是处理Http请求的环节。由于默认情况下,Feign采用的是JDK的HttpURLConnection,所以整体性能并不高。需要进行性能优化,通常采用ApacheHttpClient或者OKHttp,加入连接池技术。

3.1、使用ApacheHttpClient

相关类:

org.springframework.cloud.openfeign.ribbon.HttpClientFeignLoadBalancedConfiguration
org.springframework.cloud.openfeign.support.FeignHttpClientProperties

引入依赖:

<!-- Http Client 支持 -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>

<!-- Apache Http Client 对 Feign 支持 -->
<dependency>
    <groupId>com.netflix.feign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>${feign-httpclient.version}</version>
</dependency>

配置文件:

### Feign 配置
feign:
  httpclient:
    # 开启 Http Client
    enabled: true
    # 最大连接数,默认:200
    max-connections: 200
    # 最大路由,默认:50
    max-connections-per-route: 50
    # 连接超时,默认:2000/毫秒
    connection-timeout: 2000
    # 生存时间,默认:900L
    time-to-live: 900
    # 响应超时的时间单位,默认:TimeUnit.SECONDS
    #timeToLiveUnit: SECONDS

注意:
ApacheHttpClient的请求链接数是由最大连接数和最大路由数共同决定,最大路由数默认是2,这里一定要进行设置。不然会出现大量线程阻塞,等待获取http链接,直到超时的异常(wait timeout)。

3.2、使用OKHttp

OKHttp 是现在比较常用的一个 HTTP 客户端访问工具,具有以下特点:

  • 支持 SPDY,可以合并多个到同一个主机的请求。
  • 使用连接池技术减少请求的延迟(如果SPDY是可用的话)。
  • 使用 GZIP 压缩减少传输的数据量。
  • 缓存响应避免重复的网络请求。

相关类:

org.springframework.cloud.openfeign.FeignAutoConfiguration.OkHttpFeignConfiguration

引入依赖:

<!-- OKHttp 对 Feign 支持 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

配置文件:

### Feign 配置
feign:
  httpclient:
    # 是否开启 Http Client
    enabled: false
#    # 最大连接数,默认:200
#    max-connections: 200
#    # 最大路由,默认:50
#    max-connections-per-route: 50
#    # 连接超时,默认:2000/毫秒
#    connection-timeout: 2000
#    # 生存时间,默认:900L
#    time-to-live: 900
#    # 响应超时的时间单位,默认:TimeUnit.SECONDS
##    timeToLiveUnit: SECONDS
  okhttp:
    enabled: true

配置类:

/**
 * @Description:Feign 底层使用 OKHttp 访问配置
 */
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignClientOkHttpConfiguration {

    @Bean
    public OkHttpClient okHttpClient() {
        return new OkHttpClient.Builder()
                // 连接超时
                .connectTimeout(20, TimeUnit.SECONDS)
                // 响应超时
                .readTimeout(20, TimeUnit.SECONDS)
                // 写超时
                .writeTimeout(20, TimeUnit.SECONDS)
                // 是否自动重连
                .retryOnConnectionFailure(true)
                // 连接池
                .connectionPool(new ConnectionPool())
                .build();
    }

}

注意:
如果发现配置的超时时间无效,可以添加以下配置,因为读取超时配置的时候没有读取上面的okhttp的配置参数,而是从Request中读取。具体配置如下所示:

@Bean
public Request.Options options(){
    return new Request.Options(60000,60000);
}

3.3、undertow替换tomcat

如果将tomcat 换成 undertow,这个性能在 Jmeter 的压测下,undertow 比 tomcat 高一倍。

pom文件:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

配置项:

server: 
  undertow: 
    max-http-post-size: 0 
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程,数量和CPU 内核数目一样即可
    io-threads: 4
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载  io-threads*8
    worker-threads: 32
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分
    buffer-size: 1024
# 每个区分配的buffer数量 , 所以pool的大小是buffer-size * buffers-per-region
#   buffers-per-region: 1024 # 这个参数不需要写了
# 是否分配的直接内存
    direct-buffers: true

参考:
https://www.icode9.com/content-4-628558.html

https://blog.csdn.net/luanlouis/article/details/82821294

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 一、服务发现(Eureka/Consul/ZooKeeper) 1、Eureka介绍 Netflix Eureka...
    c_gentle阅读 1,009评论 0 2
  • 本文作者:sytyale,另外一个聪明好学的同事 一、原理 Feign 是一个 Java 到 HTTP 的客户端绑...
    hackingForest阅读 3,089评论 0 3
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 一、@EnableFeignClients详解 1、作用 扫描和注册所有使用注解@FeignClient定义的fe...
    杨健kimyeung阅读 2,816评论 1 0
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,030评论 0 4