Feign的使用以及调用过程分析

Feign的使用以及调用过程分析

在进行微服务之间的调用的时候,我们本质上都是通过http请求来进行的(参数处理,返回结果处理),在使用Feign之前我们都是使用的RestTemplate来完成这些工作的,类似于下面的这种方式:

UserInfo userInfo = this.restTemplate.getForObject("http://user-client/userInfo/getUser/{name}", UserInfo.class, name); 

这仅仅只是一次接口的调用,往往微服务之间接口的调用存在于很多地方,如果每次接口调用都使用这种硬编码的方式看起来并不友好。那么是否存在一种SpringMVC的那种方式,通过类似于方法的调用来处理微服务之间接口的调用,这就是Feign。

Feign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。而Feign则会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。

1.入门使用

  1. 首先引入依赖

    <dependency>
          <groupId>org.springframework.cloudgroupId>
          <artifactId>spring-cloud-starter-openfeignartifactId>
    dependency>
    
  2. 在主类上通过@EnableFeignClients注解开启Spring Cloud Feign的支持功能

  3. 定义FeignClient接口,@FeignClient注解指定服务名来绑定服务,然后再使用Spring MVC的注解来绑定具体该服务提供的REST接口。这里为了方便,创建了一个公共的Api,通过继承的方式来处理

    //公共api
    public interface CommonApi {
        /**
         * 查找用户
         * @param name
         * @return
         */
        @GetMapping("/userInfo/getUserByName/{name}")
        Object findByName(@PathVariable("name") String name);
    
        /**
         * 增加用户
         * @param userRequest
         * @return
         */
        @PostMapping("/userInfo/save")
        Object saveUserInfo(@RequestBody UserRequest userRequest);
    }
    
    
    
    //定义FeignClient接口,并绑定服务提供者。fallbackFactory属性可以在服务不可用时自动调用fallback制定的处理方法
    @FeignClient(name = "user-provider",fallbackFactory = UserFeignClientFallback.class)
    public interface MovieService extends CommonApi {
    }
    
    
    
    //服务不可用进入回退逻辑
    @Component
    @Slf4j
    public class UserFeignClientFallback implements FallbackFactory<MovieService> {
    
        @Override
        public MovieService create(Throwable throwable) {
          return  new MovieService() {
              @Override
              public Object findByName(String name) {
                  log.error("进入回退逻辑", throwable);
                  return new UserInfo(0L,"默认用户",0);
              }
              @Override
              public Object saveUserInfo(UserRequest userRequest) {
                  log.error("进入回退逻辑", throwable);
                  return new UserInfo(0L,"默认用户",0);
              }
          };
        }
    }
    
  4. 属性说明

value、name:value和name的作用一样,如果没有配置url那么配置的值将作为服务名称,用于服务发现。反之只是一个名称。

contextId:如果有多个name值一样的服务,可以通过contextId来区分服务。也可以通过设置属性

spring.main.allow-bean-definition-overriding=true

url:用于配置指定服务的地址,相当于直接请求这个服务,不经过Ribbon的服务选择。

path:指定FeignClient访问接口的统一前缀,和在类上使用@RequestMapping一样。

  1. 创建controller实现对Feign客户端的调用

    @RestController
    public class MovieController2 {
    
        @Autowired
        private MovieService movieService;
    
        @GetMapping("/userInfo/getUserByName/{name}")
        public Object findByName(@PathVariable("name") String name){
            return movieService.findByName(name);
        }
    
        @PostMapping("/userInfo/save")
        public Object findByNameAndAge(@RequestBody UserRequest userRequest){
           return movieService.saveUserInfo(userRequest);
        }
    }
    
  2. 创建provider实现CommonApi

    @RestController
    public class UserService implements CommonApi {
    
        @Autowired
        private UserInfoRepository userInfoRepository;
    
        @Override
        public Object findByName(String name) {
            UserInfo userInfo=userInfoRepository.findByName(name);
            return userInfo;
        }
    
        @Override
        public Object saveUserInfo(UserRequest userRequest) {
            UserInfo userInfo=new UserInfo();
            userInfo.setId(userRequest.getId());
            userInfo.setAge(userRequest.getAge());
            userInfo.setName(userRequest.getName());
            return userInfoRepository.save(userInfo);
        }
    
    }
    

2. 自定义配置

feign的默认配置类是:org.springframework.cloud.openfeign.FeignClientsConfiguration。默认定义了feign使用的编码器,解码器等。
允许使用@FeignClient的configuration的属性自定义Feign配置。自定义的配置优先级高于上面的FeignClientsConfiguration。
**如果@configuration注解那么所有的FeignClient都会使用这个配置类,Configuration属性是指定单独某个Feign。**例如有关于负载均衡规则、权限认证的一些配置

2.1 负载均衡

//首先通过configuration设置feign的配置类
@FeignClient(configuration = FeignAuthConfiguration.class)

public class FeignAuthConfiguration {
    //可以在类中配置,也可以在配置文件中设置
    /*
      user-provider:
          ribbon:
          NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
    */
   @Bean
   IRule getIRule(){
       return new RoundRobinRule();
   }
}

2.权限认证

如果服务端开启类security权限认证,那么FeignClient也需要做相应的配置

首先服务端开启security权限认证

spring:
  security:
    user:
      name: root
      password: root

消费端设置FeignAuthConfiguration


public class FeignAuthConfiguration {
    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("root", "root");
    }
}

在feign上加配置
@FeignClient(name = "user-provider",configuration = FeignAuthConfiguration.class)

3.Feign的Http Client

Feign在默认情况下使用的是JDK原生URLConnection发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用HTTP的persistence connection。我们可以用Apache的HTTP Client替换Feign原始的http client,从而获取连接池、超时时间等与性能息息相关的控制能力。Spring Cloud从Brixtion.SR5版本开始支持这种替换,首先在项目中声明ApcaheHTTP Client和feign-httpclient依赖,然后在application.properties中添加:

feign:
  httpclient:
    enabled: true

4.RequestInterceptor拦截器

feign组件提供了请求操作接口RequestInterceptor,实现之后对apply函数进行重写就能对request进行修改,包括header和body操作。也可实现权限认证的操作

@Configuration
public class MyBasicAuthRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        //权限认证    cm9vdDpyb290 base64加密  原文:root:root
        template.header("Authorization", "Basic cm9vdDpyb290");
        //方法名
        String method = template.method();
        String url = template.url();
    }
}
//如果为指定某个服务的拦截器在配置文件中配置
      user-provider:  服务名
        request-interceptors:
        - com.example.userconsumer.config.MyBasicAuthRequestInterceptor 

3.请求压缩

Spring Cloud Feign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。我们只需通过下面两个参数设置,就能开启请求与响应的压缩功能:

请求压缩一般用不上

feign:
  compression:
    request:
      enabled: true
    response: #设置返回值后,接受参数要改一下。
      enabled: true 
          
          
#也可以进行某种格式的压缩
feign:
  compression:
    request:
      enabled: true
      mime-types:
      - text/xml
      min-request-size: 2048

4.日志配置

feign:
  client: 
    config:  
      user-provider: 
        logger-level: basic
        
//上面有4种日志类型
none:不记录任何日志,默认值
basic:仅记录请求方法,url,响应状态码,执行时间。
headers:在basic基础上,记录header信息
full:记录请求和响应的header,body,元数据。
//上面的logger-level只对下面的 debug级别日志做出响应。
logging:
  level:
    com.example.consumermovie.service.MovieService: debug

5.源码分析

5.1 @EnableFeignClients

先从启动类的@EnableFeignClients开始说:首先里面的属性默认全是空的

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
    String[] value() default {};
    ...
}

这里使用了@Import注解来导入类注册成bean,通过import注解来注册bean有几种方式:

  1. 实现ImportSelector接口,spring容器就会实例化类,并且调用其selectImports方法;

  2. 实现ImportBeanDefinitionRegistrar接口,spring容器就会调用其registerBeanDefinitions方法;

  3. 带有Configuration注解的配置类。

这里是第二种方式

class FeignClientsRegistrar
    implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

  @Override
  public void registerBeanDefinitions(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
        /**
        注册@EnableFeignClients提供的自定义配置类中的相关bean。
        此配置类是被@Configuration注解修饰的配置类,
        它会提供一系列组装FeignClient的各类组件实例,比如Decoder、Encoder等。
        */
    registerDefaultConfiguration(metadata, registry);
        //注册@FeignClient注解对应的属性,并注册相应的bean
    registerFeignClients(metadata, registry);
  }
}

5.1.1 registerDefaultConfiguration()

private void registerDefaultConfiguration(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
    //首先获取到@EnableFeignClients的属性键值对Map集合(value、basePackages、defaultConfiguration...)
    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();
      }
            //注册自定义配置类,如果在@EnableFeignClients没有指定的话就是默认的
      registerClientConfiguration(registry, name,
          defaultAttrs.get("defaultConfiguration"));
    }
  }
//注册
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
      Object configuration) {
      //通过BeanDefinitionBuilder建造BeanDefinition对象
    BeanDefinitionBuilder builder = BeanDefinitionBuilder
        .genericBeanDefinition(FeignClientSpecification.class);
    builder.addConstructorArgValue(name);
    builder.addConstructorArgValue(configuration);
      //注册bean
    registry.registerBeanDefinition(
        name + "." + FeignClientSpecification.class.getSimpleName(),
        builder.getBeanDefinition());
}

这里注册bean的时候名称是主类的“全限定名+FeignClientSpecification”,这里的FeignClientSpecification其实就是Feign组件环境,保存了名称和配置类的信息。

class FeignClientSpecification implements NamedContextFactory.Specification {
    private String name;

  private Class<?>[] configuration;

  FeignClientSpecification() {
  }

  FeignClientSpecification(String name, Class<?>[] configuration) {
    this.name = name;
    this.configuration = configuration;
  }
}

5.1.2 registerFeignClients()

public void registerFeignClients(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
       // 获取ClassPath扫描器
    ClassPathScanningCandidateComponentProvider scanner = getScanner();
     // 获取ClassPath扫描器
      scanner.setResourceLoader(this.resourceLoader);

    Set<String> basePackages;
    //获取EnableFeignClients中所有的属性信息
    Map<String, Object> attrs = metadata
        .getAnnotationAttributes(EnableFeignClients.class.getName());
    //注解类型过滤器, 只过滤FeignClient
      AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
        FeignClient.class);
      //获取所有的clients注解的值,并强转成Class数组
    final Class<?>[] clients = attrs == null ? null
        : (Class<?>[]) attrs.get("clients");
      //程序刚启动的时候attrs.get("clients")默认是空的,因此clients.length==0
    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) {
            //前面scanner设置了注解类型过滤器,因此这个candidateComponents的set集合是所有@FeignClient注解类的集合
      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();
          //验证BeanDefinition是否是接口
                    Assert.isTrue(annotationMetadata.isInterface(),
              "@FeignClient can only be specified on an interface");
          //获取FeignClient注解的属性
          Map<String, Object> attributes = annotationMetadata
              .getAnnotationAttributes(
                  FeignClient.class.getCanonicalName());
          //获取FeignClient的服务名称(根据contextId、name、value、serviceId属性获取)
          String name = getClientName(attributes);
           //注册FeignClient配置类的Bean
          registerClientConfiguration(registry, name,
              attributes.get("configuration"));
          //注册FeignClient的Bean
          registerFeignClient(registry, annotationMetadata, attributes);
        }
      }
    }
  }

5.1.3 registerFeignClient 注册Bean

private void registerFeignClient(BeanDefinitionRegistry registry,
      AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
    //获取BeanDefinition的全限定名
      String className = annotationMetadata.getClassName();
      // 2.BeanDefinitionBuilder的主要作用就是构建一个AbstractBeanDefinition
        // AbstractBeanDefinition类最终被构建成一个BeanDefinitionHolder
        // 然后注册到Spring中
        // 注意:beanDefinition类为FeignClientFactoryBean,故在Spring获取类的时候实际返回的是
        // FeignClientFactoryBean类
    BeanDefinitionBuilder definition = BeanDefinitionBuilder
        .genericBeanDefinition(FeignClientFactoryBean.class);
    validate(attributes);
      // 3.添加FeignClientFactoryBean的属性,
        // 这些属性也都是我们在@FeignClient中定义的属性
    definition.addPropertyValue("url", getUrl(attributes));
    definition.addPropertyValue("path", getPath(attributes));
    String name = getName(attributes);
    definition.addPropertyValue("name", name);
    String contextId = getContextId(attributes);
    definition.addPropertyValue("contextId", contextId);
    definition.addPropertyValue("type", className);
    definition.addPropertyValue("decode404", attributes.get("decode404"));
    definition.addPropertyValue("fallback", attributes.get("fallback"));
    definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
    definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    //设置别名 可以看到是通过contextId来设置的
    String alias = contextId + "FeignClient";
    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
    beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);

    // has a default, won't be null
    boolean primary = (Boolean) attributes.get("primary");

    beanDefinition.setPrimary(primary);

    String qualifier = getQualifier(attributes);
    if (StringUtils.hasText(qualifier)) {
      alias = qualifier;
    }
    // 5.定义BeanDefinitionHolder
    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
        new String[] { alias });
      //这里BeanDefinition的name为FeignClient注解类的全限定名,类为FeignClientFactoryBean
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
  }

在这里做了几件事情:

  1. 将EnableFeignClients注解对应的配置属性注入;

  2. 将FeignClient注解对应的属性注入。

  3. 生成FeignClient对应的bean,注入到Spring 的IOC容器。

registerFeignClient方法中构造了一个BeanDefinitionBuilder对象,BeanDefinitionBuilder的主要作用就是构建一个AbstractBeanDefinition,AbstractBeanDefinition类最终被构建成一个BeanDefinitionHolder 然后注册到Spring中。

beanDefinition类为FeignClientFactoryBean,故在Spring获取类的时候实际返回的是FeignClientFactoryBean类。

5.2 FeignClientFactoryBean

通过上面的分析得出所有FeignClient注解修饰的Bean实际类型是FeignClientFactoryBean。

FeignClientFactoryBean作为一个实现了FactoryBean的工厂类,那么每次在Spring Context 创建实体类的时候会调用它的getObject()方法。

@Override
  public Object getObject() throws Exception {
    return getTarget();
  }

  /**
   * @param  the target type of the Feign client
   * @return a {@link Feign} client created with the specified data and the context
   * information
   */
  <T> T getTarget() {
    FeignContext context = applicationContext.getBean(FeignContext.class);
    Feign.Builder builder = feign(context);

    if (!StringUtils.hasText(url)) {
      if (!name.startsWith("http")) {
        url = "http://" + name;
      }
      else {
        url = name;
      }
      url += cleanPath();
      return (T) loadBalance(builder, context,
          new HardCodedTarget<>(type, name, url));
    }
    if (StringUtils.hasText(url) && !url.startsWith("http")) {
      url = "http://" + url;
    }
    String url = this.url + cleanPath();
    Client client = getOptional(context, Client.class);
    if (client != null) {
      if (client instanceof LoadBalancerFeignClient) {
        // not load balancing because we have a url,
        // but ribbon is on the classpath, so unwrap
        client = ((LoadBalancerFeignClient) client).getDelegate();
      }
      if (client instanceof FeignBlockingLoadBalancerClient) {
        // not load balancing because we have a url,
        // but Spring Cloud LoadBalancer is on the classpath, so unwrap
        client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
      }
      builder.client(client);
    }
    Targeter targeter = get(context, Targeter.class);
    return (T) targeter.target(this, builder, context,
        new HardCodedTarget<>(type, name, url));
  }

这里首先需要从applicationContext中获取FeignContext对象,这个对象什么时候被注入到Bean中的呢?点击这个类查看被引用的地方,发现在FeignAutoConfiguration声明了这个Bean

@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;
  }
  ...
} 

这里会把Listset进去,还记得在5.1.2中registerClientConfiguration方法

private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
      Object configuration) {
      //通过BeanDefinitionBuilder建造BeanDefinition对象
    BeanDefinitionBuilder builder = BeanDefinitionBuilder
        .genericBeanDefinition(FeignClientSpecification.class);
    builder.addConstructorArgValue(name);
    builder.addConstructorArgValue(configuration);
      //注册bean
    registry.registerBeanDefinition(
        name + "." + FeignClientSpecification.class.getSimpleName(),
        builder.getBeanDefinition());
}

在注册BeanDefinition的时候, configuration 其实也被作为参数,传给了 FeignClientSpecification。 所以这时候在FeignContext中是带着configuration配置信息的。

接着通过context对象获取builder对象

Feign.Builder builder = feign(context);

protected Feign.Builder feign(FeignContext context) {
    FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
    Logger logger = loggerFactory.create(type);

    // @formatter:off
    Feign.Builder builder = get(context, Feign.Builder.class)
        // required values
        .logger(logger)
        .encoder(get(context, Encoder.class))
        .decoder(get(context, Decoder.class))
        .contract(get(context, Contract.class));
    // @formatter:on

    configureFeign(context, builder);

    return builder;
  } 

至此我们已经完成了配置属性的装配工作。具体后续请求的处理是通过动态代理实现的。

详细的源码分析可以看这篇文章:Feign的调用分析

你可能感兴趣的:(SpringCloud,spring,cloud,java,Feign)