暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Spring Cloud之Feign源码解析

北银金科逐浪青春 2021-09-02
939


最近项目中用到了 Spring Cloud OpenFeign,OpenFeign 在服务远程调用的时候非常方便,在这里分享给大家。


01Feign介绍


Feign 是 Spring Cloud Netiflix 组件中的一个轻量级 Restful 的 HTTP 服务客户端,实现了 webservice 面向接口编程,进一步降低了项目的耦合度。Feign 可以与 Eureka、Nacos 和Ribbon 组合使用以支持负载均衡。

 

Feign 是声明式服务调用组件,它的强大之处在于,实现了像调用本地方法一样调用远程方法,无感知远程 HTTP 请求。类似于 Dubbo 的思想,Consumer 直接调用 Provider 的接口方法,而不需要通过常规的 Http Client 构造请求,再解析返回数据。



02使用方法


spring cloud 工程中添加依赖

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>


定义远程接口

创建 RemoteService 接口,来定义 OpenFeign 要调用的远程服务接口。


同时通过 @FeginClient 注解指定被调用方的服务名,通过 fallback 属性指定 FeignHystrix 类,来进行远程调用的熔断和降级处理。


@FeginClient 中 name 是要调用的服务的名字。

@FeignClient(name = "CF-Service", fallback = FeignHystrix.class)
public interface IRemoteService {
   @RequestLine("POST /CFS/001/{id}")
   @Headers({"TraceId: {traceId}",
             "SpanId: {spanId}",
             "AreaCode: 010"})
   @Body("{body}")
   public Object send(@Param("id") String id, @Param("traceId") String traceId,
               @Param("spanId") String spanId,@Param("body") String requestBody);
}



FeignHystrix.java 代码如下:

@Component
public class FeignHystrix implements RemoteService {
   @Override
   public Object send() {
       return "请求超时了";
   }
}


在启动类 Application.java 中添加注解 @EnableDiscoveryClient 开启服务注册、添加注解 @EnableFeignClients 开启 OpenFeign,启动类通过 OpenFeign 调用服务代码如下:

@SpringBootApplication
@RestController
@EnableDiscoveryClient
@EnableFeignClients
public class Application {
   public static void main(String[] args) {
       SpringApplication.run(Application.class, args);
   }


   @Autowired
   private IRemoteService remoteService;
   @GetMapping("/feign")
   public String test() {
       return remoteService.send();
   }
}


FeignClient 注解的属性定义 …


Feign 的注解的定义



03源码分析


源码入口

在使用 Feign时,通过 @EnableFeignClients 来启用。它以 @Import 的方式将FeignClientsRegistrar实例注入到Spring Ioc 容器中。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
   ...
   Class<?>[] defaultConfiguration() default {};
}


FeignClientsRegistrar 用于处理 FeignClient 的全局配置和被 @FeignClient 标记的接口,为接口动态创建实现类并添加到 IOC 容器。

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,ResourceLoaderAware, EnvironmentAware {
  @Override
  public void registerBeanDefinitions(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
      // 处理默认配置类
      registerDefaultConfiguration(metadata, registry);
      // 注册被 @FeignClient 标记的接口
      registerFeignClients(metadata, registry);
  }
}


@EnableFeignClients 中有个属性 defaultConfiguration,用来配置 Feign 的属性。

public @interface EnableFeignClients {
   Class<?>[] defaultConfiguration() default {};


registerDefaultConfiguration() 方法就是获取 defaultConfiguration 属性值,如果有则将配置类注入到 IOC 容器。

Private void registerDefaultConfiguration(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
   Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
   if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
       String name;
       if (metadata.hasEnclosingClass()) {
           name = "default." + metadata.getEnclosingClassName();
       } else {
           name = "default." + metadata.getClassName();
       }
  registerClientConfiguration(registry,name,defaultAttrs.get("defaultConfiguration"));
   }
}


registerFeignClients() 用来处理 @FeignClient 标记的接口。首先扫描了 classpath 中 @FeignClient 标记的接口,然后注册。


由于 @FeignClient 标记的是接口,不是普通对象,因此 Feign 利用了 FeignClientFactoryBean 来特殊处理。


public void registerFeignClients(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {


   // classpath scan 工具
   ClassPathScanningCandidateComponentProvider scanner = getScanner();
   scanner.setResourceLoader(this.resourceLoader);
   ...
   // 利用 FeignClient 作为过滤条件
   AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);


   for (String basePackage : basePackages) {
       Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
       for (BeanDefinition candidateComponent : candidateComponents) {
           if (candidateComponent instanceof AnnotatedBeanDefinition) {
               ...
               // 注册
               registerFeignClient(registry, annotationMetadata, attributes);
           }
       }
   }
}


FeignClient 标记的接口实例会由 FeignClientFactoryBean.getObject() 来搞定。

private void registerFeignClient(BeanDefinitionRegistry registry,AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
   String className = annotationMetadata.getClassName();
   // 拿到 FeignClientFactoryBean 的 BeanDefinitionBuilder
   BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
   validate(attributes);
   definition.addPropertyValue("url", getUrl(attributes));
   ...


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


getObject() 会根据 @FeignClient 注解的一些属性信息来创建 bean。

@Override
public Object getObject() throws Exception {
   FeignContext context = applicationContext.getBean(FeignContext.class);
   Feign.Builder builder = feign(context);
   // 如果 FeignClient 没有指定 URL (配置的是 service )
   if (!StringUtils.hasText(this.url)) {
       String url;
       if (!this.name.startsWith("http")) {
           url = "http://" + this.name;
       }
       else {
           url = this.name;
       }
       url += cleanPath();
       // 结合 ribbon 使得客户端具备负载均衡的能力
       return loadBalance(builder, context, new HardCodedTarget<>(this.type,this.name, url));
   }
   ...
}


loadBalance() 方法:

protected <T> T loadBalance(Feign.Builder builder, FeignContext context,HardCodedTarget<T> target) {
   // 得到的是 LoadBalancerFeignClient
   Client client = getOptional(context, Client.class);
   if (client != null) {
       builder.client(client);
       // HystrixTargeter
       Targeter targeter = get(context, Targeter.class);
       return targeter.target(this, builder, context, target);
   }
   ...
}


调用了 SynchronousMethodHandler.create() 方法。

public MethodHandler create(Target<?> target, MethodMetadata md,RequestTemplate.Factory buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) {
 return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,
                                     logLevel, md, buildTemplateFromArgs, options, decoder,
                                     errorDecoder, decode404);
}


SynchronousMethodHandler 是核心类,负责根据参数创建 RequestTemplate,然后使用具体的 http client 执行请求。

@Override
public Object invoke(Object[] argv) throws Throwable {
 // 利用参数构建请求模板, argv 就是被 MVC 注解描述的各种参数
 RequestTemplate template = buildTemplateFromArgs.create(argv);
 Retryer retryer = this.retryer.clone();
 while (true) {
   try {
     // 执行请求  
     return executeAndDecode(template);
   } catch (RetryableException e) {
     retryer.continueOrPropagate(e);
     if (logLevel != Logger.Level.NONE) {
       logger.logRetry(metadata.configKey(), logLevel);
     }
     continue;
   }
 }
}



具体发起请求由 executeAndDecode() 来做,targetRequest() 就是应用 Feign 的拦截器,decode() 用于处理 response,可以自定义 Decoder.

Object executeAndDecode(RequestTemplate template) throws Throwable {
 // 应用 Feign 的拦截器
 Request request = targetRequest(template);
 Response response;
 long start = System.nanoTime();
 try {
   // 真正发起请求  
   response = client.execute(request, options);
   // ensure the request is set. TODO: remove in Feign 10
   response.toBuilder().request(request).build();
 } catch (IOException e) {
   ...
 }
 try {
   // response 处理机制,可以自定义 Decoder 来处理 response
   if (response.status() >= 200 && response.status() < 300) {
     if (void.class == metadata.returnType()) {
       return null;
     } else {
       return decode(response);
     }
   } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
     return decode(response);
   } else {
     throw errorDecoder.decode(metadata.configKey(), response);
   }
 } ...
}



04Feign的配置


项目中用 Feign 就是为了可以方便做负载均衡,可以只根据服务名进行负载均衡。


Ribbon 配置

Feign 使用 Ribbon 配置非常简单:

ribbon.ConnectTimeout = 500
ribbon.ReadTimeout = 500


由于 Spring Cloud Feign 会根据注解的 name 和 value 属性,自动创建一个同名的 Ribbon 客户端。这样我们就可以使用 @FeignClient 注解中的服务名属性来设置 Ribbon 参数,这样就可以只针对这一个服务配置相应的负载均衡参数。

CF-Service.ribbon.ConnectTimeout=500
CF-Service.ribbon.ReadTimeout=500
// 还可配置一些重试机制
CF-Service.ribbon.OkToRetryOnAllOperations=true
CF-Service.ribbon.MaxAutoRetriesNextServer=2
CF-Service.ribbon.MaxAutoRetries=1


Hystrix 配置

Hystrix 配置同 Ribbon 配置一样,直接使用前缀 hystrix.command.default 就可以进行配置,比如全局超时时间:

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000


禁用单个FeginClient的Hystrix的支持,只针对此服务进行 Hystrix的禁用配置。

// Configuration表示feign的自定义配置类
@FeignClient(name = "CF-Service", configuration = Configuration.class)


@Configuration
public class Configuration {
 //禁用当前配置的 hystrix,局部禁用
 @Bean
 @Scope("prototype")
 public Feign.Builder feignBuilder() {
   return Feign.builder();
 }
}



参考文献


  • https://blog.csdn.net/qq_33619378/article/details/95353326

  • https://blog.csdn.net/Anonymous_L/article/details/103991903

  • https://mp.weixin.qq.com/s/CQgZ-TyhE50cFrEwAHFQWA


(责任编辑:张子鑫) 


作者/  彭博

彭博,银行转型业务开发部,专注于分布式、后端开发、系统优化等领域,目前主要负责银行核心系统架构、代码设计和开发工作。






招聘启事


北银金融科技有限责任公司根植于北京银行,是一家致力于大数据、人工智能、云计算、区块链、物联网等新技术创新与金融科技应用的科技企业,公司充分发挥北京银行企业文化和技术积淀先天优势,通过对技术、场景、生态的完美融合,输出科技创新产品和技术服务。


现诚邀优秀人才加盟

共享金融科技时代硕果


扫描此二维码

期待您的加入





文章转载自北银金科逐浪青春,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论