Spring Boot Feign 使用与源码学习

Feign 的使用

服务拆分后,在一个服务中会经常需要调用到另外的服务。这种情况,除了使用 Dubbo 等 RPC 框架外,最简单的方法是通过 Spring Cloud Feign 来进行服务间的调用。
Feign 最终是通过代理使用 http 请求服务返回编码后的内容。使用 Feign 可以通过简单的申明去除手动发起 http 和编解码等复杂过程。

先看看 如何简单的使用 Feign。

  • 首先引入 spring-cloud-dependenciesspring-cloud-starter-openfeign
// 这里贴出了 feign 使用必须的包


  
    org.springframework.cloud
    spring-cloud-dependencies
    Greenwich.SR4
    pom
    import
  



    
      org.springframework.cloud
      spring-cloud-starter-openfeign
      2.2.3.RELEASE
      compile
    
    // ...

  • 启动类上添加注解 @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 一般是注册中心对应的服务名
@FeignClient(name = "MyClient", url = "http://api.xxxx.cn/")
public interface MyClient {
    // url+value 是远程服务的调用路径
    @RequestMapping(method = RequestMethod.GET, value = "/api/path")
    String getInfo(@RequestParam Long id);
}
  • 使用 Client 类调用远程服务,这样调用使逻辑看上去是在面向对象编程,而不用再去手动处理 Http 请求。
// 实际调用远程服务
private MyClient myClient;
String res = myClient.getInfo(1L);

Feign 源码解读

从项目启动、获取 Client 实例、实际方法调用 3 个过程分析 Feign 相关源码。

启动

  • spring-cloud-openfeign-core 包中通过 SPI 机制,运行 FeignAutoConfiguration 文件。根据配置生成 org.springframework.cloud.openfeign.FeignContextfeign.Clientorg.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
    // 当项目中存在 OkHttpClient.class 类(项目引入了 OkHttpClient 的包)的时候会初始化下面类
    @ConditionalOnClass({OkHttpClient.class})
    @ConditionalOnMissingBean({okhttp3.OkHttpClient.class})
    @ConditionalOnProperty({"feign.okhttp.enabled"})
    protected static class OkHttpFeignConfiguration {
        // OkHttpClient 可以使用连接池,这样可以减少 tcp 多次连接的开销。默认的 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 进行 http 调用的客户端
        @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);
            // 初始化连接池过期连接的清除任务
            this.connectionManagerTimer.schedule(new TimerTask() {
                public void run() {
                    connectionManager.closeExpiredConnections();
                }
            }, 30000L, (long)httpClientProperties.getConnectionTimerRepeat());
            return connectionManager;
        }

        // 如果没有自定义 Client 时,这里会生成 ApacheHttpClient 作为 feign.Client 进行 http 调用的客户端
        @Bean
        @ConditionalOnMissingBean({Client.class})
        public Client feignClient(HttpClient httpClient) {
            return new ApacheHttpClient(httpClient);
        }
    }

    @Configuration
    @ConditionalOnMissingClass({"feign.hystrix.HystrixFeign"})
    protected static class DefaultFeignTargeterConfiguration {
        protected DefaultFeignTargeterConfiguration() {
        }

        // 默认使用的 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 导入 FeignClientsRegistrar.class 类,在类中注册 FeigntClient 的默认配置和扫描并注册 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 的默认配置
        this.registerDefaultConfiguration(metadata, registry);
        // 通过扫描有 `FeignClient.class` 注解的类,并注入到容器中
        this.registerFeignClients(metadata, registry);
    }

    private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map attributes) {
        String className = annotationMetadata.getClassName();
        // 每个 Client 都是 FeignClientFactoryBean.class 类,获取类实例的时候是通过调用 FeignClientFactoryBean 类的 getObject() 方法获得
        BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);

        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }
}

获取 Client 实例

根据启动时注入的内容可知, 从容器获得 client 的实例是通过 FeignClientFactoryBean 类的 getObject() 方法获取。
最终得到的是一个 Proxy 代理,实例化过程见下图:


Feign实例化过程.png
  • FeignContext、HystrixTargeter 都是启动的时候生成到容器的 Bean
  • Feign.Builder 是 Feign Client 的构建者
  • SynchronousMethodHandler.Factory 是 Client 中定义的方法拦截器的创建工厂,Client 中每个方法对应一个 SynchronousMethodHandler 处理器
  • ReflectiveFeign.ParseHandlersByName 将 Client 中的方法名解析得到不同的 SynchronousMethodHandler
  • ReflectiveFeign 是 Feign 类的唯一实现
  • InvocationHandlerFactory.Default 方法处理创建工厂的默认实现,生成代理类方法的处理实现
  • ReflectiveFeign.FeignInvocationHandler 代理类方法处理的默认类,是代理类拦截后的处理类,代理的方法会在它的 invoke() 方法中实现,它又是将拦截的方法转发到不同的 SynchronousMethodHandler 中进行处理

Client 方法调用

从上面获取 Client 实例的过程可以知道,在调 client 的方法时,实例调用的是 Proxy 类的方法,会对应的 SynchronousMethodHandler 拦截执行实际的逻辑。SynchronousMethodHandler 执行的逻辑如下:

public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    // 重试器,默认是 Retryer.Default 会重试 5次
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        // 由启动时注入的 httpclient 发起 http 请求,并编码返回的内容
        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 默认是 Client.Default 使用的是 HttpURLConnection 发起 http 调用
  • 推荐在实际过程中换成 OkHttpClient 或者 ApacheHttpClient ,它们可以使用到连接池

如有疑问,欢迎留言交流

你可能感兴趣的:(Spring Boot Feign 使用与源码学习)