OpenFeign學習(九):Spring Cloud OpenFeign的加載配置原理 II

說明

在上篇博文《OpenFeign學習(八):Spring Cloud OpenFeign的加載配置原理》中,我簡單介紹了Spring Cloud 是如何通過註解對Feign Client進行加載配置的。主要介紹了通過FeignClientsRegistrar類,對所有使用@FeignClient註解的類進行加載配置,實現Feign Client的配置類Bean的註冊和相對應Client的FeignClientFactoryBean的註冊。同時還提到在spring.factories配置文件中,配置了有關自動裝配的類FeignRibbonClientAutoConfiguration和FeignAutoConfiguration等類。在本篇博文中,我將繼續通過源碼學習介紹Spring Cloud OpenFeign的加載配置原理。

正文

通過上篇博文我們知道了對Feign Client進行掃描註冊的實際爲其對應的FeignClientFactoryBean,beanName爲@FeignClient作用接口類的ClassName。

接下來,我們瞭解下FeignRibbonClientAutoConfiguration類的作用:

FeignRibbonClientAutoConfiguration

該類從名稱可知屬於RibbonClient的支持類,

@ConditionalOnClass({ILoadBalancer.class, Feign.class})
@Configuration
@AutoConfigureBefore({FeignAutoConfiguration.class})
@EnableConfigurationProperties({FeignHttpClientProperties.class})
@Import({HttpClientFeignLoadBalancedConfiguration.class, OkHttpFeignLoadBalancedConfiguration.class, DefaultFeignLoadBalancedConfiguration.class})
public class FeignRibbonClientAutoConfiguration {
    
}

通過源碼可以看到,該類作用的前提是類路徑中必需包含了ILoadBalancer類和Feign類,而ILoadBalancer類屬於com.netflix.loadbalancer包,則證明類路徑必須存在Netflix Ribbon的相關依賴。通過該類將會在FeignAutoConfiguration類前加載。

同時通過@EnableConfigurationProperties註解將FeignHttpClientProperties配置類進行注入,該類爲FeignHttpClient的配置類。接着通過@Import註解分別引入了HttpClientFeignLoadBalancedConfiguration,OkHttpFeignLoadBalancedConfiguration,DefaultFeignLoadBalancedConfiguration類,顧名思義,這些類爲不同類型LoadBalancerClient的配置類。

可以看到,該類的作用主要是引入不同配置類,在創建cleint時,根據不同的配置創建不同的feignClient。

關於feignClient的創建,官方文檔對此有以下描述:

if Ribbon is in the classpath and is enabled it is a LoadBalancerFeignClient, otherwise if Spring Cloud LoadBalancer is in the classpath, FeignBlockingLoadBalancerClient is used. If none of them is in the classpath, the default feign client is used.

在創建feignClient時,不同的client優先級爲LoadBalancerFeignClient > FeignBlockingLoadBalancerClient > Default Feign Client 。

而且spring-cloud-starter-openfeign同時包含了spring-cloud-starter-netflix-ribbon和spring-cloud-starter-loadbalancer依賴。

由此可知,默認創建的feignClient都爲LoadBalancerFeignClient類型。

我們可以通過設置feign.okhttp.enabled或feign.httpclient.enabled爲ture來選擇不同的client,但前提是必須引入相關依賴

FeignAutoConfiguration

該類作用於FeignRibbonClientAutoConfiguration之後,與之類似,爲FeignClient引入註冊相關配置。

@Configuration
@ConditionalOnClass({Feign.class})
@EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.class})
public class FeignAutoConfiguration {

    @Autowired(
        required = false
    )
    private List<FeignClientSpecification> configurations = new ArrayList();
    
    ....
    
    @Bean
    public FeignContext feignContext() {
        FeignContext context = new FeignContext();
        context.setConfigurations(this.configurations);
        return context;
    }
    
    ....

}

通過源碼可以看到,該類通過@EnableConfigurationProperties註解注入了FeignClientProperties和FeignHttpClientProperties配置類。在其內部,注入了FeignClientSpecification集合,配置了FeignContext,還有對OkHttpClient, ApacheHttpClient,HystrixFeign的相關配置。這裏我們重點關注以上所顯示的代碼相關配置。

通過上篇博文我們可以知道,在註冊時對在@EnableFeignClients註解中配置的defaultConfiguration和在@FeignClient註解中配置的configuration都會註冊爲FeignClientSpecification類型的bean

在該類中,通過對FeignClientSpecificatio類型的List集合進行自動裝配,將之前註冊的配置類注入到該類中。同時,在實例化FeignContext類型的bean時,將其設置到上下文中。(有關集合的自動裝配請看:https://blog.csdn.net/tales522/article/details/89683282)

配置類FeignClientProperties爲獲取feign.client前綴的配置值,對不同的client進行配置則在前綴後加指定的clientName,對所有client的默認配置則加default(i.e. feign.client.default)

配置類FeignHttpClientProperties則爲獲取httpclient的配置值。配置前綴爲feign.httpclient。

FeignClientFactoryBean

在使用feignClient時,我們會使用@Autowired註解進行自動裝配依賴注入。此時,會通過factoryBean的getObject()方法獲取對應類的實例對象。

接下來,通過源碼來了解如何創建Feign Client:

public Object getObject() throws Exception {
        return this.getTarget();
    }

    <T> T getTarget() {
        // 獲取Feign上下文
        FeignContext context = (FeignContext)this.applicationContext.getBean(FeignContext.class);
        // 根據上下文創建FeignClient的構造器
        Builder builder = this.feign(context);
        // 創建對應的client
        if (!StringUtils.hasText(this.url)) {
            if (!this.name.startsWith("http")) {
                this.url = "http://" + this.name;
            } else {
                this.url = this.name;
            }

            this.url = this.url + this.cleanPath();
            return this.loadBalance(builder, context, new HardCodedTarget(this.type, this.name, this.url));
        } else {
            if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
                this.url = "http://" + this.url;
            }

            String url = this.url + this.cleanPath();
            Client client = (Client)this.getOptional(context, Client.class);
            if (client != null) {
                if (client instanceof LoadBalancerFeignClient) {
                    client = ((LoadBalancerFeignClient)client).getDelegate();
                }

                builder.client(client);
            }

            Targeter targeter = (Targeter)this.get(context, Targeter.class);
            return targeter.target(this, builder, context, new HardCodedTarget(this.type, this.name, url));
        }
    }

通過源碼可以看出,在getTarget()方法中主要分爲三步來創建feignClient:

  1. 獲取Feign的上下文對象 FeignContext
  2. 根據上下文創建FeignClient的構造器
  3. 根據是否配置url來創建對應的client

FeignContext

通過上面的介紹我們已經知道在FeignAutoConfiguration類中已經聲明配置了對應的Bean。在實例化時,創建對象後爲其設置了cleint的配置類集合。

@Bean
public FeignContext feignContext() {
    FeignContext context = new FeignContext();
    context.setConfigurations(this.configurations);
    return context;
}
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
    public FeignContext() {
        super(FeignClientsConfiguration.class, "feign", "feign.client.name");
    }
}

FeignContext繼承自NamedContextFactory類,默認構造函數調用了父類的構造函數,其中參數FeignClientsConfiguration類爲Spring Cloud Netflix爲Client提供的默認配置類

FeignClientsConfiguration.class is the default configuration provided by Spring Cloud Netflix.

FeignClientsConfiguration提供了一些Feign Client的默認配置,如Decoder,Encoder,Contract,Retryer等。

我們已經知道Spring Cloud對OpenFeign的集成使其支持SpringMVC註解及Spring Web的HttpMessageConverters。而這些配置,正是由FeignClientsConfiguration配置類進行聲明配置的。

@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
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
    return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}

可以看到SpringCloud用ResponseEntityDecoder作爲默認Decoder, SpringEncoder作爲默認Encoder,SpringMvcContract代替了OpenFeign默認的Contract,使其能支持SpringMVC的註解。

在SpringEncoder源碼中,可以看到其已經配置生成了SpringFormEncoder對象,所以在表單提交或者文件傳輸時,不用再手動配置SpringFormEncoder。

public class SpringEncoder implements Encoder {
    private static final Log log = LogFactory.getLog(SpringEncoder.class);
    private final SpringFormEncoder springFormEncoder = new SpringFormEncoder();
    private ObjectFactory<HttpMessageConverters> messageConverters;

    public SpringEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
        this.messageConverters = messageConverters;
    }
    
    .....
}

Builder

接下來通過Feign上下文創建Client構造器對象。

Builder builder = this.feign(context);
protected Builder feign(FeignContext context) {
    FeignLoggerFactory loggerFactory = (FeignLoggerFactory)this.get(context, FeignLoggerFactory.class);
    // 生成logger 默認仍爲Slf4jLogger
    Logger logger = loggerFactory.create(this.type);
    
    // 從AnnotationConfigApplicationContext獲取對應配置
    Builder builder = ((Builder)this.get(context, Builder.class)).logger(logger).encoder((Encoder)this.get(context, Encoder.class)).decoder((Decoder)this.get(context, Decoder.class)).contract((Contract)this.get(context, Contract.class));
    // 配置其他參數
    this.configureFeign(context, builder);
    return builder;
}

首先獲取FeignLoggerFactory對象,但是在獲取時,先生成了對應client的AnnotationConfigApplicationContext,並根據之前註解中的配置類進行配置。

protected <T> T get(FeignContext context, Class<T> type) {
    T instance = context.getInstance(this.contextId, type);
    if (instance == null) {
        throw new IllegalStateException("No bean found of type " + type + " for " + this.contextId);
    } else {
        return instance;
    }
}
public <T> T getInstance(String name, Class<T> type) {
        // 根據contextId配置生成對應的AnnotationConfigApplicationContext
        AnnotationConfigApplicationContext context = this.getContext(name);
        return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0 ? context.getBean(type) : null;
    }
protected AnnotationConfigApplicationContext getContext(String name) {
    // 每個client對應context都是單例,這裏使用了雙重檢查加鎖
    if (!this.contexts.containsKey(name)) {
        synchronized(this.contexts) {
            if (!this.contexts.containsKey(name)) {
                this.contexts.put(name, this.createContext(name));
            }
        }
    }

    return (AnnotationConfigApplicationContext)this.contexts.get(name);
}

創建AnnotationConfigApplicationContext時,首先獲取註冊client對應配置的configuration,然後再註冊client的默認配置,最後註冊defaultConfigType,也就是Spring Cloud Netflix提供的默認配置類FeignClientsConfiguration。

protected AnnotationConfigApplicationContext createContext(String name) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    
    // 獲取client對應的配置類進行註冊
    if (this.configurations.containsKey(name)) {
        Class[] var3 = ((NamedContextFactory.Specification)this.configurations.get(name)).getConfiguration();
        int var4 = var3.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            Class<?> configuration = var3[var5];
            context.register(new Class[]{configuration});
        }
    }

    Iterator var9 = this.configurations.entrySet().iterator();

    // 循環註冊所有默認的以default開頭的配置
    while(true) {
        Entry entry;
        do {
        
            // 最後註冊配置類FeignClientsConfiguration
            if (!var9.hasNext()) {
                context.register(new Class[]{PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType});
                context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(this.propertySourceName, Collections.singletonMap(this.propertyName, name)));
                if (this.parent != null) {
                    context.setParent(this.parent);
                    context.setClassLoader(this.parent.getClassLoader());
                }

                context.setDisplayName(this.generateDisplayName(name));
                context.refresh();
                return context;
            }

            entry = (Entry)var9.next();
        } while(!((String)entry.getKey()).startsWith("default."));

        Class[] var11 = ((NamedContextFactory.Specification)entry.getValue()).getConfiguration();
        int var12 = var11.length;

        for(int var7 = 0; var7 < var12; ++var7) {
            Class<?> configuration = var11[var7];
            context.register(new Class[]{configuration});
        }
    }
}

創建完成client對應的AnnotationConfigApplicationContext後,根據配置類創建生成構建起Builder。通過以上代碼可以看到,從自定義配置類或者默認配置類中,可以獲取先進行配置的有Logger,Encoder,Decoder, Contract。

Builder builder = ((Builder)this.get(context, Builder.class)).logger(logger).encoder((Encoder)this.get(context, Encoder.class)).decoder((Decoder)this.get(context, Decoder.class)).contract((Contract)this.get(context, Contract.class));

之後,在通過configureFeign方法配置client其他參數:

this.configureFeign(context, builder);
protected void configureFeign(FeignContext context, Builder builder) {
    // 獲取配置文件中關於client配置的參數
    FeignClientProperties properties = (FeignClientProperties)this.applicationContext.getBean(FeignClientProperties.class);
    // 按優先級順序進行配置
    if (properties != null) {
        if (properties.isDefaultToProperties()) {
            this.configureUsingConfiguration(context, builder);
            this.configureUsingProperties((FeignClientConfiguration)properties.getConfig().get(properties.getDefaultConfig()), builder);
            this.configureUsingProperties((FeignClientConfiguration)properties.getConfig().get(this.contextId), builder);
        } else {
            this.configureUsingProperties((FeignClientConfiguration)properties.getConfig().get(properties.getDefaultConfig()), builder);
            this.configureUsingProperties((FeignClientConfiguration)properties.getConfig().get(this.contextId), builder);
            this.configureUsingConfiguration(context, builder);
        }
    } else {
        this.configureUsingConfiguration(context, builder);
    }

}

通過以上代碼我們可以看到,先獲取了FeignClientProperties示例,該類用來讀取配置文件中有關feign client的配置參數。根據defaultToProperties參數來決定不同的配置順序,對此,官方文檔中也進行了說明:

If we create both @Configuration bean and configuration properties, configuration properties will win. It will override @Configuration values. But if you want to change the priority to @Configuration, you can change feign.client.default-to-properties to false.

可以看到如果同時使用了@Configuration bean 也就是註解中設置了配置類和配置文件配置了相關屬性時,默認情況下,配置文件會覆蓋配置類的值。但是可以通過設置feign.client.default-to-properties 值爲false改變優先級,此參數值對應爲FeignClientProperties的defaultToProperties參數值。

若默認情況,會先根據默認配置類和自定配置類的值進行配置:

this.configureUsingConfiguration(context, builder);

接下來根據配置文件中配置的對所有client的默認值進行配置:

this.configureUsingProperties((FeignClientConfiguration)properties.getConfig().get(properties.getDefaultConfig()), builder);

最後,根據配置文件的自定義參數值進行配置:

this.configureUsingProperties((FeignClientConfiguration)properties.getConfig().get(this.contextId), builder);

當設置爲false時,調換配置順序,將配置類的配置放到最後。
主要配置了Logger.Level,Retryer,ErrorDecoder,RequestInterceptor,decode404及httpclient相關配置。

在FeignClientsConfiguration類,配置了Retryer的bean,默認情況下,Spring Cloud OpenFeign的feignClient不會進行重試,這與原生的OpenFeign有所不同。

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

Client

在完成Buidler的創建配置後,根據@FeignClient註解中是否配置了url來完成最終的FeignClient創建。

在未配置時,進行了以下配置:

if (!StringUtils.hasText(this.url)) {
    if (!this.name.startsWith("http")) {
        this.url = "http://" + this.name;
    } else {
        this.url = this.name;
    }
    // url 值類似爲 http://clientName
    this.url = this.url + this.cleanPath();
    return this.loadBalance(builder, context, new HardCodedTarget(this.type, this.name, this.url));
} 

可以看到調用loadBalance方法進行創建

protected <T> T loadBalance(Builder builder, FeignContext context, HardCodedTarget<T> target) {
    Client client = (Client)this.getOptional(context, Client.class);
    if (client != null) {
        builder.client(client);
        Targeter targeter = (Targeter)this.get(context, Targeter.class);
        return targeter.target(this, builder, context, target);
    } else {
        throw new IllegalStateException("No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
    }
}

在該方法中,獲取了配置的Client對象,上面提到在FeignRibbonClientAutoConfiguration配置類中引入了三個配置類,分別爲HttpClientFeignLoadBalancedConfiguration,OkHttpFeignLoadBalancedConfiguration和DefaultFeignLoadBalancedConfiguration。

要使用 ApacheHttpClient 或 OkHttpClient 時,必須引入對應的依賴,同時配置文件中feign.okhttp.enabled或feign.httpclient.enabled對應值true,否則就使用默認的client。

這裏在DefaultFeignLoadBalancedConfiguration配置類中配置創建了默認的LoadBalancerFeignClient。

@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) {
    return new LoadBalancerFeignClient(new Default((SSLSocketFactory)null, (HostnameVerifier)null), cachingFactory, clientFactory);
}

該默認LoadBalancerFeignClient中delegate即爲feign默認的client,使用的是HttpURLConnection:

public static class Default implements Client {
    private final SSLSocketFactory sslContextFactory;
    private final HostnameVerifier hostnameVerifier;

    public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
        this.sslContextFactory = sslContextFactory;
        this.hostnameVerifier = hostnameVerifier;
    }
    ....
}

再獲取對應的Targeter,該接口共有兩個實現類,分別爲DefaultTargeter和HystrixTargeter,這裏獲取默認的DefaultTargeter。

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

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

最後調用feign.target(target)創建對應的代理target。

若配置有url,則進行以下配置:

else {
    if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
        this.url = "http://" + this.url;
    }

    String url = this.url + this.cleanPath();
    Client client = (Client)this.getOptional(context, Client.class);
    if (client != null) {
        if (client instanceof LoadBalancerFeignClient) {
            client = ((LoadBalancerFeignClient)client).getDelegate();
        }
        // 獲取最終代理client
        builder.client(client);
    }

    Targeter targeter = (Targeter)this.get(context, Targeter.class);
    return targeter.target(this, builder, context, new HardCodedTarget(this.type, this.name, url));
}

由源碼可知,若配置了url,在創建代理時使用的client不是LoadBalancerFeignClient,也就無法實現負載均衡

至此,FeignClient配置加載創建原理介紹完畢。

總結

通過源碼我們瞭解了Spring Cloud OpenFeign的加載配置創建流程。通過註解@FeignClient和@EnableFeignClients註解實現了client的配置聲明註冊,再通過FeignRibbonClientAutoConfiguration和FeignAutoConfiguration類進行自動裝配。

在通過FeignClientFactoryBean創建client對象時,瞭解了Spring Cloud是如何進行配置,不同配置的優先級順序的選擇,及根據配置生成不同配置類的流程,及Spring Cloud對OpenFeign的集成支持如何實現,及與原生OpenFeign的區別之處。

更多的使用方法及配置請閱讀官方文檔

接下來,我將繼續學習閱讀源碼,瞭解Ribbon是如何在Spring Cloud OpenFeign中進行負載均衡的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章