feign的远端调用

三丰 soft张三丰

Feign能干什么?

Feign旨在是编写Java Http客户端变得更加容易。

之前使用Ribbon+RestTemplate时,利用RESTTemplate请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,有偶遇对于服务依赖的调用可能不止一处,往往一个借口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务借口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon,自动封装服务调用客户端的开发量。

Feign和OpenFeign有什么区别?

feign超时配置:默认1s,在application.yml里面配置这个


ribbon:
    #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
    ReadTimeout: 5000
    #指的是建立连接后服务器读取到可用资源所用的时间
    ConectTimeout:5000

调用流程

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

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

动态代理

  JDK的Proxy动态代理会涉及到以下几个类:   1、java.lang.reflect.InvocationHandler   2、java.lang.reflect.Method   3、java.lang.reflect.Proxy

创建代理对象的过程很简单:   1、使用java.lang.reflect.Proxy类的newProxyInstance方法;   2、输入三个参数:     (1) 被代理对象的类加载器;     (2) 被代理对象的接口加载器;     (3) java.lang.reflect.InvocationHandler实现类;   我们需要做的就是在java.lang.reflect.InvocationHandler的invoke方法中把增强逻辑写好。   调用代理对象的方法时,其实就进入InvocationHandler的invoke方法中。

代理模式-JDK Proxy

场景如下: 本文例子代理了ArrayList, 在ArrayList每次操作时, 在操作之前和之后都进行一些额外的操作.

ArrayListProxy类

这里是代理的实现.


import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class ArrayListProxy implements InvocationHandler {
    private Object proxy;

    public ArrayListProxy(Object obj) {
        this.proxy = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
        System.out.println("before calling " + method);
        if (args != null) {
            for (Object arg : args) {
                System.out.println(arg);
            }
        }
        Object o = method.invoke(this.proxy, args);
        System.out.println("after calling " + method);
        return o;
    }
}

MyUtils类

封装了Proxy.newProxyInstance()方法, 方便取得代理对象.


import java.lang.reflect.Proxy;

public class MyUtils {
    public static Object getProxy(Object obj) {
        Class cls = obj.getClass();
        return Proxy.newProxyInstance(
                cls.getClassLoader(),
                cls.getInterfaces(),
                new ArrayListProxy(obj)
        );
    }
}  

Main

在这里进行运行测试


import java.util.ArrayList;
import java.util.List;

@SuppressWarnings("unchecked")
public class Main {
    public static void main(String[] args) {
        List realList = new ArrayList<String>(10);

        List proxyList = (List) MyUtils.getProxy(realList);

        proxyList.add("New");

        System.out.print("\n\n");

        proxyList.add("York");
    }
}

远程接口的本地JDK Proxy代理实例,有以下特点:

(1)Proxy代理实例,实现了一个加 @FeignClient 注解的远程调用接口;

(2)Proxy代理实例,能在内部进行HTTP请求的封装,以及发送HTTP 请求;

(3)Proxy代理实例,能处理远程HTTP请求的响应,并且完成结果的解码,然后返回给调用者。

调用处理器 InvocationHandler

通过 JDK Proxy 生成动态代理类,核心步骤就是需要定制一个调用处理器,具体来说,就是实现JDK中位于java.lang.reflect 包中的 InvocationHandler 调用处理器接口,并且实现该接口的 invoke(…) 抽象方法。

为了创建Feign的远程接口的代理实现类,Feign提供了自己的一个默认的调用处理器,叫做 FeignInvocationHandler 类,该类处于 feign-core 核心jar包中。当然,调用处理器可以进行替换,如果Feign与Hystrix结合使用,则会替换成 HystrixInvocationHandler 调用处理器类,类处于 feign-hystrix 的jar包中。

默认的调用处理器 FeignInvocationHandler

默认的调用处理器 FeignInvocationHandler 是一个相对简单的类,有一个非常重要Map类型成员 dispatch 映射,保存着远程接口方法到MethodHandler方法处理器的映射。

客户端组件 feign.Client

客户端组件是Feign中一个非常重要的组件,负责端到端的执行URL请求。其核心的逻辑:发送request请求到服务器,并接收response响应后进行解码。

feign.Client 类,是代表客户端的顶层接口,只有一个抽象方法,源码如下:


package feign;

/**客户端接口
 * Submits HTTP {@link Request requests}. 
Implementations are expected to be thread-safe.
 */
public interface Client {
  //提交HTTP请求,并且接收response响应后进行解码
  Response execute(Request request, Options options) throws IOException;

}

由于不同的feign.Client 实现类,内部完成HTTP请求的组件和技术不同,故,feign.Client 有多个不同的实现。这里举出几个例子:

(1)Client.Default类:默认的feign.Client 客户端实现类,内部使用HttpURLConnnection 完成URL请求处理;

(2)ApacheHttpClient 类:内部使用 Apache httpclient 开源组件完成URL请求处理的feign.Client 客户端实现类;

(3)OkHttpClient类:内部使用 OkHttp3 开源组件完成URL请求处理的feign.Client 客户端实现类。

(4)LoadBalancerFeignClient 类:内部使用 Ribben 负载均衡技术完成URL请求处理的feign.Client 客户端实现类。

Client.Default类:

作为默认的Client 接口的实现类,在Client.Default内部使用JDK自带的HttpURLConnnection类实现URL网络请求。

在JKD1.8中,虽然在HttpURLConnnection 底层,使用了非常简单的HTTP连接池技术,但是,其HTTP连接的复用能力,实际是非常弱的,性能当然也很低。具体的原因,参见后面的“SpringCloud与长连接的深入剖析”专题内容。

二:ApacheHttpClient类

ApacheHttpClient 客户端类的内部,使用 Apache HttpClient开源组件完成URL请求的处理。

从代码开发的角度而言,Apache HttpClient相比传统JDK自带的URLConnection,增加了易用性和灵活性,它不仅使客户端发送Http请求变得容易,而且也方便开发人员测试接口。既提高了开发的效率,也方便提高代码的健壮性。

从性能的角度而言,Apache HttpClient带有连接池的功能,具备优秀的HTTP连接的复用能力。关于带有连接池Apache HttpClient的性能提升倍数,具体可以参见后面的对比试验。

ApacheHttpClient 类处于 feign-httpclient 的专门jar包中,如果使用,还需要通过Maven依赖或者其他的方式,倒入配套版本的专门jar包。

三:OkHttpClient类

OkHttpClient 客户端类的内部,使用OkHttp3 开源组件完成URL请求处理。OkHttp3 开源组件由Square公司开发,用于替代HttpUrlConnection和Apache HttpClient。由于OkHttp3较好的支持 SPDY协议(SPDY是Google开发的基于TCP的传输层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。),从Android4.4开始,google已经开始将Android源码中的 HttpURLConnection 请求类使用OkHttp进行了替换。也就是说,对于Android 移动端APP开发来说,OkHttp3 组件,是基础的开发组件之一。

四:LoadBalancerFeignClient 类

LoadBalancerFeignClient 内部使用了 Ribben 客户端负载均衡技术完成URL请求处理。在原理上,简单的使用了delegate包装代理模式:Ribben负载均衡组件计算出合适的服务端server之后,由内部包装 delegate 代理客户端完成到服务端server的HTTP请求;所封装的 delegate 客户端代理实例的类型,可以是 Client.Default 默认客户端,也可以是 ApacheHttpClient 客户端类或OkHttpClient 高性能客户端类,还可以其他的定制的feign.Client 客户端实现类型。

FeignClient注解

FeignClient注解被@Target(ElementType.TYPE)修饰,表示FeignClient注解的作用目标在接口上


@FeignClient(name = "github-client", url = "https://api.github.com", configuration = GitHubExampleConfig.class)
public interface GitHubClient {
    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    String searchRepo(@RequestParam("q") String queryStr);
}

声明接口之后,在代码中通过@Resource注入之后即可使用。@FeignClient标签的常用属性如下:

  • name:指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现
  • url: url一般用于调试,可以手动指定@FeignClient调用的地址
  • decode404:当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
  • configuration: Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract
  • fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
  • fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
  • path: 定义当前FeignClient的统一前缀

@FeignClient(name = "github-client",
        url = "https://api.github.com",
        configuration = GitHubExampleConfig.class,
        fallback = GitHubClient.DefaultFallback.class)
public interface GitHubClient {
    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    String searchRepo(@RequestParam("q") String queryStr);
 
    /**
     * 容错处理类,当调用失败时,简单返回空字符串
     */
    @Component
    public class DefaultFallback implements GitHubClient {
        @Override
        public String searchRepo(@RequestParam("q") String queryStr) {
            return "";
        }
    }
}

在使用fallback属性时,需要使用@Component注解,保证fallback类被Spring容器扫描到,GitHubExampleConfig内容如下:


@Configuration
public class GitHubExampleConfig {
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

在使用FeignClient时,Spring会按name创建不同的ApplicationContext,通过不同的Context来隔离FeignClient的配置信息,在使用配置类时,不能把配置类放到Spring App Component scan的路径下,否则,配置类会对所有FeignClient生效.

Feign请求超时问题

Hystrix默认的超时时间是1秒,如果超过这个时间尚未响应,将会进入fallback代码。而首次请求往往会比较慢(因为Spring的懒加载机制,要实例化一些类),这个响应时间可能就大于1秒了 解决方案有三种,以feign为例。 方法一 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000 该配置是让Hystrix的超时时间改为5秒 方法二 hystrix.command.default.execution.timeout.enabled: false 该配置,用于禁用Hystrix的超时时间 方法三 feign.hystrix.enabled: false 该配置,用于索性禁用feign的hystrix。该做法除非一些特殊场景,不推荐使用。