我是靠谱客的博主 优美钻石,最近开发中收集的这篇文章主要介绍SpringCloud整合Feign配置类之间的关系以及feign配置隔离的实现,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

SpringCloud整合Feign有多个配置类,因为其中SpringCloud和整合Feign的同时还需要把Ribbon整合进来,包括Feign的一些自身组件的配置更换等,下面我们先来把这些配置类梳理一下

 

关于springcloud整合feign相关的配置类如上图所示,这些类都是为springcloud整合feign的时候所准备的,我们去springcloud-feign包下面的spring.factories文件看一下

其中前面两个配置类是我们常用的,首先先来看一下FeignAutoConfiguration

FeignAutoConfiguration

@Configuration
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({ FeignClientProperties.class,
      FeignHttpClientProperties.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;
   }

   // 注入带有Hystrix熔断降级的Targeter
   // 默认当我们加上依赖SpringCloud-Feign依赖的时候就有HystrixFeign这个类
   // 与下面的DefaultTargeter互斥
   @Configuration
   @ConditionalOnClass(name = "feign.hystrix.HystrixFeign")
   protected static class HystrixFeignTargeterConfiguration {

      @Bean
      @ConditionalOnMissingBean
      public Targeter feignTargeter() {
         return new HystrixTargeter();
      }

   }

   // 注入默认的Targeter
   // 与上面的HystrixTargeter互斥
   @Configuration
   @ConditionalOnMissingClass("feign.hystrix.HystrixFeign")
   protected static class DefaultFeignTargeterConfiguration {

      @Bean
      @ConditionalOnMissingBean
      public Targeter feignTargeter() {
         return new DefaultTargeter();
      }

   }

   // 当类路径上不存在ribbon的LoadBalancer的才会注入到IOC容器
   // HttpClientFeignConfiguration与下面的OkHttpFeignConfiguration这两个配置
   // 作用就是用来配置Feign的Client组件的,而springcloud在整合feign的时候提供了
   // 对Client组件的两种替换实现,分别是HttpClient,Okhttp作为请求客户端,默认feign是用jdk自带的HttpURLConnection
   // 基本上HttpClientFeignConfiguration和下面的OkHttpFeignConfiguration都不会被注入到IOC容器
   // 因为只要我们把springcloud-feign包依赖进来,这个包里面也依赖了springcloud-ribbon包
   // 所有类路径上面肯定会有ribbon的LoadBalancer
   // 那么是在哪里替换client组件的呢?答案就是在FeignRibbonClientAutoConfiguration
   @Configuration
   @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);

      @Autowired(required = false)
      private RegistryBuilder registryBuilder;

      private CloseableHttpClient httpClient;

      @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() {
            @Override
            public void run() {
               connectionManager.closeExpiredConnections();
            }
         }, 30000, httpClientProperties.getConnectionTimerRepeat());
         return connectionManager;
      }

      @Bean
      public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
            HttpClientConnectionManager httpClientConnectionManager,
            FeignHttpClientProperties httpClientProperties) {
         RequestConfig defaultRequestConfig = RequestConfig.custom()
               .setConnectTimeout(httpClientProperties.getConnectionTimeout())
               .setRedirectsEnabled(httpClientProperties.isFollowRedirects())
               .build();
         this.httpClient = httpClientFactory.createBuilder()
               .setConnectionManager(httpClientConnectionManager)
               .setDefaultRequestConfig(defaultRequestConfig).build();
         return this.httpClient;
      }

      @Bean
      @ConditionalOnMissingBean(Client.class)
      public Client feignClient(HttpClient httpClient) {
         return new ApacheHttpClient(httpClient);
      }

      @PreDestroy
      public void destroy() throws Exception {
         this.connectionManagerTimer.cancel();
         if (this.httpClient != null) {
            this.httpClient.close();
         }
      }

   }

   @Configuration
   @ConditionalOnClass(OkHttpClient.class)
   @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
   @ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
   @ConditionalOnProperty("feign.okhttp.enabled")
   protected static class OkHttpFeignConfiguration {

      private okhttp3.OkHttpClient okHttpClient;

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

      @Bean
      public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
            ConnectionPool connectionPool,
            FeignHttpClientProperties httpClientProperties) {
         Boolean followRedirects = httpClientProperties.isFollowRedirects();
         Integer connectTimeout = httpClientProperties.getConnectionTimeout();
         Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
         this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation)
               .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
               .followRedirects(followRedirects).connectionPool(connectionPool)
               .build();
         return this.okHttpClient;
      }

      @PreDestroy
      public void destroy() {
         if (this.okHttpClient != null) {
            this.okHttpClient.dispatcher().executorService().shutdown();
            this.okHttpClient.connectionPool().evictAll();
         }
      }

      @Bean
      @ConditionalOnMissingBean(Client.class)
      public Client feignClient(okhttp3.OkHttpClient client) {
         return new OkHttpClient(client);
      }

   }

}

这个配置类看上去往容器中注入了很多bean,但是其实做的事情并不多,主要是因为后面注入的那两个client组件并不满足条件,所以这两个client并不会注入到容器中,还有一个比较重要的就是注入了一个FeignContext,这个组件说来话长,简单来说主要就是用来做对于每一个服务的容器配置隔离的,还有注入了一个HystrixTargeter,这个targeter并不是feign原生的组件,HystrixTargeter中主要就是加上了Hystrix的熔断降级功能,经过这个类feign生成的接口代理对象的InvocationHandler就不再是feign自带的FeignInvocation了,而是HystrixInvocationHandler,这个InvocationHandler里面的invoke方法加上了Hystrix的api熔断降级的支持

FeignRibbonAutoConfiguration

@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@Configuration
@AutoConfigureBefore(FeignAutoConfiguration.class)
@EnableConfigurationProperties({ FeignHttpClientProperties.class })
// Order is important here, last should be the default, first should be optional
// see
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
      OkHttpFeignLoadBalancedConfiguration.class,
      DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {

   @Bean
   @Primary
   @ConditionalOnMissingBean
   @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
   public CachingSpringLoadBalancerFactory cachingLBClientFactory(
         SpringClientFactory factory) {
      return new CachingSpringLoadBalancerFactory(factory);
   }

   @Bean
   @Primary
   @ConditionalOnMissingBean
   @ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
   public CachingSpringLoadBalancerFactory retryabeCachingLBClientFactory(
         SpringClientFactory factory, LoadBalancedRetryFactory retryFactory) {
      return new CachingSpringLoadBalancerFactory(factory, retryFactory);
   }

   @Bean
   @ConditionalOnMissingBean
   public Request.Options feignRequestOptions() {
      return LoadBalancerFeignClient.DEFAULT_OPTIONS;
   }

}

重点看

@Import({ HttpClientFeignLoadBalancedConfiguration.class,
      OkHttpFeignLoadBalancedConfiguration.class,
      DefaultFeignLoadBalancedConfiguration.class })

这里会往容器中导入三个配置类,从名字上来看这三个配置类所做的事情应该都是一样的,都和负载均衡有关,所以应该有对应的condition符合才能注入到容器中

这三个配置类分别就是往容器中注入HttpClient请求客户端,Okhttp请求客户端,jdk自带的HttpURLConnection请求客户端(默认使用),并且HttpClient和Okhttp需要加上对应的依赖包才能够符合condition条件往容器中注入

还有一点就是可以发现这三个配置类都是注入的一个LoadBalancerFeignClient,这个client有什么用?

public class LoadBalancerFeignClient implements Client {

   static final Request.Options DEFAULT_OPTIONS = new Request.Options();

   // 真正的请求客户端
   private final Client delegate;

   private CachingSpringLoadBalancerFactory lbClientFactory;

   private SpringClientFactory clientFactory;

   public LoadBalancerFeignClient(Client delegate,
         CachingSpringLoadBalancerFactory lbClientFactory,
         SpringClientFactory clientFactory) {
      this.delegate = delegate;
      this.lbClientFactory = lbClientFactory;
      this.clientFactory = clientFactory;
   }

   static URI cleanUrl(String originalUrl, String host) {
      String newUrl = originalUrl;
      if (originalUrl.startsWith("https://")) {
         newUrl = originalUrl.substring(0, 8)
               + originalUrl.substring(8 + host.length());
      }
      else if (originalUrl.startsWith("http")) {
         newUrl = originalUrl.substring(0, 7)
               + originalUrl.substring(7 + host.length());
      }
      StringBuffer buffer = new StringBuffer(newUrl);
      if ((newUrl.startsWith("https://") && newUrl.length() == 8)
            || (newUrl.startsWith("http://") && newUrl.length() == 7)) {
         buffer.append("/");
      }
      return URI.create(buffer.toString());
   }

   @Override
   public Response execute(Request request, Request.Options options) throws IOException {
      try {
         URI asUri = URI.create(request.url());
         String clientName = asUri.getHost();
         URI uriWithoutHost = cleanUrl(request.url(), clientName);
         FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
               this.delegate, request, uriWithoutHost);

         IClientConfig requestConfig = getClientConfig(options, clientName);
         return lbClient(clientName)
               .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
      }
      catch (ClientException e) {
         IOException io = findIOException(e);
         if (io != null) {
            throw io;
         }
         throw new RuntimeException(e);
      }
   }

   IClientConfig getClientConfig(Request.Options options, String clientName) {
      IClientConfig requestConfig;
      if (options == DEFAULT_OPTIONS) {
         requestConfig = this.clientFactory.getClientConfig(clientName);
      }
      else {
         requestConfig = new FeignOptionsClientConfig(options);
      }
      return requestConfig;
   }

   protected IOException findIOException(Throwable t) {
      if (t == null) {
         return null;
      }
      if (t instanceof IOException) {
         return (IOException) t;
      }
      return findIOException(t.getCause());
   }

   public Client getDelegate() {
      return this.delegate;
   }

   private FeignLoadBalancer lbClient(String clientName) {
      return this.lbClientFactory.create(clientName);
   }

   static class FeignOptionsClientConfig extends DefaultClientConfigImpl {

      FeignOptionsClientConfig(Request.Options options) {
         setProperty(CommonClientConfigKey.ConnectTimeout,
               options.connectTimeoutMillis());
         setProperty(CommonClientConfigKey.ReadTimeout, options.readTimeoutMillis());
      }

      @Override
      public void loadProperties(String clientName) {

      }

      @Override
      public void loadDefaultValues() {

      }

   }

}

 可以看到LoadBalancerFeignClient也实现了feign的client接口,所以说他自身也是一个client组件,并且LoadBalancerFeignClient中的delegate属性也是一个client对象,而在LoadBalancerFeignClient执行execute方法发起请求的时候,execute方法会再去调用ribbon的api去进行负载均衡得到目标服务实例,根据这个目标服务实例去重写url,然后使用delegate这个client对象真正地发起请求,所以说LoadBalancerFeignCleint只是一个包装类,包装了真正发起请求的请求客户端,在其上面包装了ribbon的负载均衡功能,这个类是springcloud-feign包里面的,所以说经过springcloud整合的feign才会有ribbon负载均衡的功能

FeignClientsConfiguration

@Configuration
public class FeignClientsConfiguration {

   @Autowired
   private ObjectFactory<HttpMessageConverters> messageConverters;

   @Autowired(required = false)
   private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();

   @Autowired(required = false)
   private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();

   @Autowired(required = false)
   private Logger logger;

   @Bean
   @ConditionalOnMissingBean
   public Decoder feignDecoder() {
      return new OptionalDecoder(
            new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
   }

   @Bean
   @ConditionalOnMissingBean
   @ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
   public Encoder feignEncoder() {
      return new SpringEncoder(this.messageConverters);
   }

   @Bean
   @ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
   @ConditionalOnMissingBean
   public Encoder feignEncoderPageable() {
      return new PageableSpringEncoder(new SpringEncoder(this.messageConverters));
   }

   @Bean
   @ConditionalOnMissingBean
   public Contract feignContract(ConversionService feignConversionService) {
      return new SpringMvcContract(this.parameterProcessors, feignConversionService);
   }

   @Bean
   public FormattingConversionService feignConversionService() {
      FormattingConversionService conversionService = new DefaultFormattingConversionService();
      for (FeignFormatterRegistrar feignFormatterRegistrar : this.feignFormatterRegistrars) {
         feignFormatterRegistrar.registerFormatters(conversionService);
      }
      return conversionService;
   }

   @Bean
   @ConditionalOnMissingBean
   public Retryer feignRetryer() {
      return Retryer.NEVER_RETRY;
   }

   @Bean
   @Scope("prototype")
   @ConditionalOnMissingBean
   public Feign.Builder feignBuilder(Retryer retryer) {
      return Feign.builder().retryer(retryer);
   }

   @Bean
   @ConditionalOnMissingBean(FeignLoggerFactory.class)
   public FeignLoggerFactory feignLoggerFactory() {
      return new DefaultFeignLoggerFactory(this.logger);
   }

   @Bean
   @ConditionalOnClass(name = "org.springframework.data.domain.Page")
   public Module pageJacksonModule() {
      return new PageJacksonModule();
   }

   @Configuration
   @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
   protected static class HystrixFeignConfiguration {

      @Bean
      @Scope("prototype")
      @ConditionalOnMissingBean
      @ConditionalOnProperty(name = "feign.hystrix.enabled")
      public Feign.Builder feignHystrixBuilder() {
         return HystrixFeign.builder();
      }

   }

}

可以看到这个配置类里面有很多都是feign的原生组件,所以这个配置类应该是专门用来配置feign的组件属性的,但是这个配置类并没有在spring.factories,那么这个配置是怎么放到容器里面的的呢?这里就要涉及到springcloud的一个规范包的东西了。springcloud-context规范包中提供了一个扩展类,使用者能够通过该扩展类去根据某个规则去创建不同的子容器,其中这个扩展类就是NamedContextFactory,它是一个抽象类,专门用来做配置隔离的。

SpringCloud整合Feign实现Feign的配置隔离

先来看下NamedContextFactory这个抽象类,它里面只有一个带三个参数的构造方法

public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
      implements DisposableBean, ApplicationContextAware {
private final String propertySourceName;

private final String propertyName;

// key是自定义的任意字符串,value是spring容器对象
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();

private Map<String, C> configurations = new ConcurrentHashMap<>();

private ApplicationContext parent;

private Class<?> defaultConfigType;

public NamedContextFactory(Class<?> defaultConfigType, String propertySourceName,
      String propertyName) {
   this.defaultConfigType = defaultConfigType;
   this.propertySourceName = propertySourceName;
   this.propertyName = propertyName;
}

// 省略
......
}

实现这个抽象类的有两个子类,分别是FeignContext,SpringClientFactory,其中FeignContext对应是为Feign服务的,SpringClientFactory是为Ribbon服务的,我们这里看FeignContext(SpringClientFactory与其大同小异)

public class FeignContext extends NamedContextFactory<FeignClientSpecification> {

   public FeignContext() {
      super(FeignClientsConfiguration.class, "feign", "feign.client.name");
   }

}

可以看到FeignContext只是在它的构造方法中调用了父类的构造方法,并且还传了三个参数,其中第一个参数有点熟悉,这不就是我们feign的组件配置类FeignClientsConfiguration吗?那这里把这个类传进去有什么用呢?接着继续看它的getContext和createContext方法

protected AnnotationConfigApplicationContext getContext(String name) {
    // 判断contexts里面是否有该name的容器
   if (!this.contexts.containsKey(name)) {
      synchronized (this.contexts) {
         if (!this.contexts.containsKey(name)) {
             // 创建该name对应的容器对象,并且放到contexts中
            this.contexts.put(name, createContext(name));
         }
      }
   }
   return this.contexts.get(name);
}

protected AnnotationConfigApplicationContext createContext(String name) {
   AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
   if (this.configurations.containsKey(name)) {
      for (Class<?> configuration : this.configurations.get(name)
            .getConfiguration()) {
         context.register(configuration);
      }
   }
   for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
      if (entry.getKey().startsWith("default.")) {
         for (Class<?> configuration : entry.getValue().getConfiguration()) {
            context.register(configuration);
         }
      }
   }
   // 把FeignClientsConfiguration放到容器中
   context.register(PropertyPlaceholderAutoConfiguration.class,
         this.defaultConfigType);
   context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
         this.propertySourceName,
         Collections.<String, Object>singletonMap(this.propertyName, name)));
   if (this.parent != null) {
      // Uses Environment from parent as well as beans
      // 当前的应用容器设置为创建子容器的父容器
      context.setParent(this.parent);
      // jdk11 issue
      // https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
      context.setClassLoader(this.parent.getClassLoader());
   }
   context.setDisplayName(generateDisplayName(name));
   // 刷新新容器
   context.refresh();
   return context;
}

可以看到createContext方法会创建一个新的spring容器,并且会把FeignClientsConfiguration配置类注册进这个新容器中,当新容器refresh之后,新容器里面就会存在feign组件的这些bean了

在createContext方法一开始还有两个for循环,并且这两个for循坏都是遍历configurations这个list,那么这个list是什么?是从哪里来的?

public void setConfigurations(List<C> configurations) {
   for (C client : configurations) {
      this.configurations.put(client.getName(), client);
   }
}

可以发现这个list有setConfigurations这个入口能够进来,但是这个list里面的元素是一个泛型,是由子类FeignContext去指定这个泛型的,而FeignContext指定的泛型是FeignClientSpecification这个类,我们看下这个类

class FeignClientSpecification implements NamedContextFactory.Specification {

   private String name;

   private Class<?>[] configuration;

   FeignClientSpecification() {
   }

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

   public String getName() {
      return this.name;
   }

   public void setName(String name) {
      this.name = name;
   }

   public Class<?>[] getConfiguration() {
      return this.configuration;
   }

   public void setConfiguration(Class<?>[] configuration) {
      this.configuration = configuration;
   }

   @Override
   public boolean equals(Object o) {
      if (this == o) {
         return true;
      }
      if (o == null || getClass() != o.getClass()) {
         return false;
      }
      FeignClientSpecification that = (FeignClientSpecification) o;
      return Objects.equals(this.name, that.name)
            && Arrays.equals(this.configuration, that.configuration);
   }

   @Override
   public int hashCode() {
      return Objects.hash(this.name, this.configuration);
   }

   @Override
   public String toString() {
      return new StringBuilder("FeignClientSpecification{").append("name='")
            .append(this.name).append("', ").append("configuration=")
            .append(Arrays.toString(this.configuration)).append("}").toString();
   }

}

这个类实现了NamedContextFactory里面的Specification接口,它有两个熟悉,一个是name,一个configuration的class数组,那么这个name应该就是我们上面说的可以是任意字符串,configuration又是什么?带着这个问题,我们需要去看它是怎么被创建并且注入进来的

我们上面知道有一个setConfiguration能够把一个FeignClientSpecification集合set进来,所以我们去到FeignContext创建的时候看看是否在创建的时候调用了这个方法,FeignContext的创建是在FeignAutoConfiguration中创建的,我们回到FeignAutoConfiguration中

@Autowired(required = false)
private List<FeignClientSpecification> configurations = new ArrayList<>();

@Bean
public FeignContext feignContext() {
   FeignContext context = new FeignContext();
   context.setConfigurations(this.configurations);
   return context;
}

可以看到确实是在FeignContext的时候把FeignClientSpecification放进来的,但是这些FeignClientSpecification是通过@Autowired依赖注入进FeignAutoConfiguration的,也就是说这些FeignClientSpecification都是在当前的应用容器中的,那又是什么时候把这些FeignClientSpecification放到容器中的呢?

答案就是feign的接口代理对象放到spring容器的时候,这个时候每个feign接口会去根据@FeignClient注解去生成对应的FeignClientSpecification然后放到spring容器中,具体我们看feign接口往spring容器放入的过程

FeignClientsRegistrar

我们直接来到FeignClientRegistrar这个类,为什么来到这个类?因为这个类是@EnableFeignClients注解里面通过@Import注解导入到spring容器的类,而这个类又实现了spring的ImportBeanDefinitionRegistrar接口,所以我们可以在这个里面直接往容器中去放bd,重点看ImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法

public void registerBeanDefinitions(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   registerDefaultConfiguration(metadata, registry);
   registerFeignClients(metadata, registry);
}

可以看到这个方法中调用了两个方法,这两个方法都是往spring容器中去放bd的,里面就有我们所关注的FeignClientSpecification对象的创建

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"));
   }
}
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
      Object configuration) {
   BeanDefinitionBuilder builder = BeanDefinitionBuilder
         .genericBeanDefinition(FeignClientSpecification.class);
   builder.addConstructorArgValue(name);
   builder.addConstructorArgValue(configuration);
   registry.registerBeanDefinition(
         name + "." + FeignClientSpecification.class.getSimpleName(),
         builder.getBeanDefinition());
}

我们可以看到上面的代码中主要就是创建FeignClientSpecification的bd,当spring通过构造方法反射得到FeignClientSpecification对象时,FeignClientSpecification对象里面的name就是default +"." +@EnableFeignClients注解标注的类的全类名,configuration就是@EnableFeignClients注解的defaultConfiguration属性的class数组,也就是说如果我们在@EnableFeignClients注解上面声明了defaultConfiguration属性,那么就会往spring容器中注册一个FeignClientSpecification对象,并且这个对象的name就是default +"." +@EnableFeignClients注解标注的类的全类名,configuration就是@EnableFeignClients注解的defaultConfiguration属性的class数组

那就spring容器中就只有这一个FeignClientSpecification对象了吗?当然不是,除了这里有往容器中注册FeignClientSpecification对象之外,registerFeignClients方法也会往容器中注册FeignClientSpecification对象

public void registerFeignClients(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   ClassPathScanningCandidateComponentProvider scanner = getScanner();
   scanner.setResourceLoader(this.resourceLoader);

   Set<String> basePackages;

   Map<String, Object> attrs = metadata
         .getAnnotationAttributes(EnableFeignClients.class.getName());
   AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
         FeignClient.class);
   final Class<?>[] clients = attrs == null ? null
         : (Class<?>[]) attrs.get("clients");
   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) {
      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();
            Assert.isTrue(annotationMetadata.isInterface(),
                  "@FeignClient can only be specified on an interface");

            // 拿出接口上@FeignClient注解的所有属性
            Map<String, Object> attributes = annotationMetadata
                  .getAnnotationAttributes(
                        FeignClient.class.getCanonicalName());
            // 获取name,这个name就是我们上面说的那个任意字符串
            String name = getClientName(attributes);
            registerClientConfiguration(registry, name,
                  attributes.get("configuration"));
            // 往容器中注册该接口的工厂bd
            registerFeignClient(registry, annotationMetadata, attributes);
         }
      }
   }
}

上面就是通过spring的扫描器去指定的包路径下扫描带有@FeignClient的接口类并把这些接口类包装为一个个的工厂bd让spring通过FactoryBean去创建出接口的代理对象并放到spring容器中,在这个过程中我们可以看到再次调用了registerClientConfiguration方法去给容器中注册FeignClientSpecification对象,但是这次是把@FeignClient注解中的configuration属性的值拿出来作为FeignClientSpecification的configuration,而name是什么?name是从getClientName方法中返回出来的

private String getClientName(Map<String, Object> client) {
   if (client == null) {
      return null;
   }
   String value = (String) client.get("contextId");
   if (!StringUtils.hasText(value)) {
      value = (String) client.get("value");
   }
   if (!StringUtils.hasText(value)) {
      value = (String) client.get("name");
   }
   if (!StringUtils.hasText(value)) {
      value = (String) client.get("serviceId");
   }
   if (StringUtils.hasText(value)) {
      return value;
   }

   throw new IllegalStateException("Either 'name' or 'value' must be provided in @"
         + FeignClient.class.getSimpleName());
}

可以看到先从@FeignClient注解的contextId属性取值,如果取出来的值是空,那么就取value的值,再没有的取name的值,再没有就取serviceId的值,也就是contextId>value>name>servieId,而我们通常都是直接在@FeignClient注解的name这个属性声明该接口所属的服务的服务名,所以通常来说FeignClientSpecification的name就是该api接口所属的服务名(如果有两个api接口所声明的name是相同的,也就是说都属于同一个服务的接口,这种情况是很常见的,而这种情况在稍微新一点的版本中应用在启动的时候就会报错,原因就是往spring容器中注册了相同名称的FeignClientSpecification对象,这在spring中默认不允许注册相同名称的bd的,当然也可以设置,但是并不推荐,而是我们都会显式地在这个api接口的@FeignClient注解上面声明contextId属性为接口的全类名,这样启动就不会报错了,但这仅仅是为了解决应用能够正常启动,我们要知道contextId属性主要是给我们用来区分容器配置的)

为每一个feign接口类的@FeignClient注解声明contextId的效果?

根据上面我们跟着源码可以知道,如果为每一个feign接口类的@FeignClient注解声明contextId的话,此时容器中的FeignClientSpecification的个数就和feign接口的个数一样,并且它们的name都分别是对应feign接口的@FeignClient注解的contextId属性值,configuration为@FeignClient注解的configuration属性值,那么我们又回到NamedContextFactory的createContext方法的那两个for循坏

// 此时configurations这个map里面就会有容器中所有的FeignClientSpecification了
// 当feign创建其代理对象的时候,需要各种组件构造其Feign.Build
// 需要一个组件的时候就会传在@FeignClient注解声明的contextId过来
// 先会去判断下有没有该contextId所属的spring容器对象
// 如果没有的话就需要创建spring容器对象,那么就会来到createContext方法
// 所以这里的name就是contextId
if (this.configurations.containsKey(name)) {
   for (Class<?> configuration : this.configurations.get(name)
         .getConfiguration()) {
      context.register(configuration);
   }
}
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
   if (entry.getKey().startsWith("default.")) {
      for (Class<?> configuration : entry.getValue().getConfiguration()) {
         context.register(configuration);
      }
   }
}

当feign创建其代理对象的时候,需要各种组件构造其Feign.Build,需要一个组件的时候就会传在@FeignClient注解声明的contextId过来,先会去判断下有没有该contextId所属的spring容器对象,如果没有的话就需要创建spring容器对象,那么就会来到createContext方法,所以这里的name就是contextId。可以看到在第一个for循环中会去根据contextId从configurations中找到对应的配置类数组然后进行遍历往新的spring容器中去注册,而第二个for循环我们可以看到它是取name以default.开头的配置类数组,我们上面也看到了name以defalut.开头的FeignClientSpecification是怎么来的了,就是在@EnableFeignClient注解的defaultConfiguration属性上声明了配置类,然后这个配置类就会往所有新创建的spring容器去注册了,也就是说在@EnableFeignClient注解的defaultConfiguration属性上声明的配置类会作为全局配置去使用,而在某一个feign接口的@FeignClient注解上声明contextId+configuration这个配置类只会在此feign接口上有效,而且我们可以发现是先注册feign接口自己的配置类,然后再注册全局的配置类,最后才是注册默认的配置类,所以说配置类的优先级是feign接口自己的配置类>全局的配置类>默认的配置类

最后

以上就是优美钻石为你收集整理的SpringCloud整合Feign配置类之间的关系以及feign配置隔离的实现的全部内容,希望文章能够帮你解决SpringCloud整合Feign配置类之间的关系以及feign配置隔离的实现所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(109)

评论列表共有 0 条评论

立即
投稿
返回
顶部