1. 程式人生 > >SpringCloud | SpringCloud Feign原始碼深入分析

SpringCloud | SpringCloud Feign原始碼深入分析

概述

springCloud feign主要對netflix feign進行了增強和包裝,本篇從原始碼角度帶你過一遍裝配流程,揭開feign底層的神祕面紗。 主要包括feign整合ribbon,hystrix,sleuth,以及生成的代理類最終注入到spring容器的過程。篇幅略長,耐心讀完,相信你會有所收穫。

Feign架構圖

一些核心類及大致流程:

在這裡插入圖片描述

大體步驟: 一、註冊FeignClient配置類和FeignClient BeanDefinition 二、例項化Feign上下文物件FeignContext,以服務名為key,存入map 三、建立Feign.Builder 五、生成代理物件(Feign核心) 六、注入到spring容器

原始碼分析

主要圍繞上面6個步驟詳細分析。

一、註冊FeignClient配置類和FeignClient BeanDefinition

從啟動類註解開始,來看下@EnableFeignClients註解:

@EnableFeignClients
public class MyApplication {
}

這是在啟動類開啟feign裝配的註解,跟進該註解,看看做了什麼:

@Import(FeignClientsRegistrar.class)
public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
        ResourceLoaderAware, BeanClassLoaderAware {

    // patterned after Spring Integration IntegrationComponentScanRegistrar
    // and RibbonClientsConfigurationRegistgrar
    private final Logger logger = LoggerFactory.getLogger(FeignClientsRegistrar.class);
    private ResourceLoader resourceLoader;

    private ClassLoader classLoader;

    public FeignClientsRegistrar() {
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        //1、先註冊預設配置
        registerDefaultConfiguration(metadata, registry);
        //2、註冊所有的feignClient beanDefinition
        registerFeignClients(metadata, registry);
    }
    //...
}

我們分別來看一下上面registerBeanDefinitions中的兩個方法: 1) 註冊預設配置方法:registerDefaultConfiguration:

    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();
            }
            // name 預設以 default 開頭,後續會根據名稱選擇配置
            registerClientConfiguration(registry, name,
                    defaultAttrs.get("defaultConfiguration"));
        }
    }

上述方法為讀取啟動類上面@EnableFeignClients註解中宣告feign相關配置類,預設name為default,一般情況下無需配置。用預設的FeignAutoConfiguration即可。 上面有個比較重要的方法:註冊配置registerClientConfiguration,啟動流程一共有兩處讀取feign的配置類,這是第一處。根據該方法看一下:

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

上面將bean配置類包裝成FeignClientSpecification,注入到容器。該物件非常重要,包含FeignClient需要的重試策略,超時策略,日誌等配置,如果某個服務沒有設定,則讀取預設的配置。

2、掃描FeignClient

該方法主要是掃描類路徑,對所有的FeignClient生成對應的BeanDefinition:

public void registerFeignClients(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {

        //...
        //獲取掃描目錄下面所有的bean deanDefinition
        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");

                    Map<String, Object> attributes = annotationMetadata
                            .getAnnotationAttributes(
                                    FeignClient.class.getCanonicalName());

                    String name = getClientName(attributes);
                    //這裡是第二處
                    registerClientConfiguration(registry, name,
                            attributes.get("configuration"));

                    //註冊feignClient
                    registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }
    }

可以看到上面又呼叫了registerClientConfiguration註冊配置的方法,這裡是第二處呼叫。這裡主要是將掃描的目錄下,每個專案的配置類載入的容器當中。 註冊到容器中,什麼時候會用到呢?具體又如何使用呢?彆著急,後面會有介紹。

我們先會回到繼續主流程,繼續看註冊feignClient的方法,跟進registerFeignClient

private void registerFeignClient(BeanDefinitionRegistry registry,
            AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        //宣告代理類名稱
        BeanDefinitionBuilder definition = BeanDefinitionBuilder
                .genericBeanDefinition(FeignClientFactoryBean.class);
        //logger.info("TEX do some replacement");
            //attributes.put("value", ((String)attributes.get("value")).replace('_','-'));
        validate(attributes);
        definition.addPropertyValue("url", getUrl(attributes));
        definition.addPropertyValue("path", getPath(attributes));
        String name = getName(attributes);
        definition.addPropertyValue("name", name);
        definition.addPropertyValue("type", className);
        definition.addPropertyValue("decode404", attributes.get("decode404"));
        definition.addPropertyValue("fallback", attributes.get("fallback"));
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

        String alias = name + "FeignClient";
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        beanDefinition.setPrimary(true);
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
                new String[] { alias });
        //將bean definition加入到spring容器
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }

劃重點,上面出現了一行相當關鍵程式碼:

BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);

springCloud FeignClient其實是利用了spring的代理工廠來生成代理類,所以這裡將所有的feignClient的描述資訊BeanDefinition設定為FeignClientFactoryBean型別,該類又繼承FactoryBean,很明顯,這是一個代理類。 在spring中,FactoryBean是一個工廠bean,用作建立代理bean,所以得出結論,feign將所有的feignClient bean包裝成FeignClientFactoryBean。掃描方法到此結束。

代理類什麼時候會觸發生成呢? 在spring重新整理容器時,當例項化我們的業務service時,如果發現註冊了FeignClient,spring就會去例項化該FeignClient,同時會進行判斷是否是代理bean,如果為代理bean,則呼叫FeignClientFactoryBeanT getObject() throws Exception;方法生成代理bean。

先來隆重介紹一下FeignClientFactoryBean,後面四步都基於此類。

先看一下代理feignClient代理生成入口:getObject方法:

@Override
    public Object getObject() throws Exception {
        // 二、例項化Feign上下文物件FeignContext
        FeignContext context = applicationContext.getBean(FeignContext.class);
        // 三、生成builder物件,用來生成feign
        Feign.Builder builder = feign(context);

        // 判斷生成的代理物件型別,如果url為空,則走負載均衡,生成有負載均衡功能的代理類
        if (!StringUtils.hasText(this.url)) {
            String url;
            if (!this.name.startsWith("http")) {
                url = "http://" + this.name;
            }
            else {
                url = this.name;
            }
            url += cleanPath();
            // 四、生成負載均衡代理類
            return loadBalance(builder, context, new HardCodedTarget<>(this.type,
                    this.name, url));
        }
        //如果指定了url,則生成預設的代理類
        if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
            this.url = "http://" + this.url;
        }
        String url = this.url + cleanPath();
        // 五、生成預設代理類
        return targeter.target(this, builder, context, new HardCodedTarget<>(
                this.type, this.name, url));
    }

getObject()邏輯比較多,每一行都會做一些初始化配置,來逐步分析。

二、例項化Feign上下文物件FeignContext

上述方法中第一行便是例項化FeignContext

FeignContext context = applicationContext.getBean(FeignContext.class);

獲取FeignContext物件,如果沒有例項化,則主動例項化,如下:

@Configuration
@ConditionalOnClass(Feign.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();
        //將feign的配置類設定到feign的容器當中
        context.setConfigurations(this.configurations);
        return context;
    }
}

可以看到feign的配置類設定到feign的容器當中,而集合中的元素 正是上面我們提到的兩處呼叫registerClientConfiguration方法新增進去的,前後呼應。

然而,當我們引入了sleuth之後,獲取的feignContext確是TraceFeignClientAutoConfiguration中配置的例項sleuthFeignContext:

在這裡插入圖片描述

可以看到上面建立了一個TraceFeignContext例項,因為該物件繼承FeignContext,同時又加了@Primary註解,所以在上面第2步中通過型別獲取: applicationContext.getBean(FeignContext.class);,最終拿到的是TraceFeignContext

三、構造FeignBuilder

繼續跟進該方法:

Feign.Builder builder = feign(context);

protected Feign.Builder feign(FeignContext context) {
        Logger logger = getOptional(context, Logger.class);

        if (logger == null) {
            logger = new Slf4jLogger(this.type);
        }

        // 1、構造 Feign.Builder
        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));


		// 2、設定重試策略,log等元件
		
		 //設定log級別
        Logger.Level level = getOptional(context, Logger.Level.class);
        if (level != null) {
            builder.logLevel(level);
        }
        //設定重試策略
        Retryer retryer = getOptional(context, Retryer.class);
        if (retryer != null) {
            builder.retryer(retryer);
        }
        //feign的錯誤code解析介面
        ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class);
        if (errorDecoder != null) {
            builder.errorDecoder(errorDecoder);
        }
        //超時時間設定,連線超時時間:connectTimeout預設10s,請求請求超時時間:readTimeout預設60s
        Request.Options options = getOptional(context, Request.Options.class);
        if (options != null) {
            builder.options(options);
        }
        //攔截器設定,可以看出攔截器也是可以針對單獨的feignClient設定
        Map<String, RequestInterceptor> requestInterceptors = context.getInstances(
                this.name, RequestInterceptor.class);
        if (requestInterceptors != null) {
            builder.requestInterceptors(requestInterceptors.values());
        }

        if (decode404) {
            builder.decode404();
        }

        return builder;
    }

上述程式碼有兩處邏輯,分別來看:

1、Feign.Builder builder = get(context, Feign.Builder.class) ,又會有以下三種情況:

1)單獨使用Feign,沒有引入 sleuthhystrix: 通過載入FeignClientsConfiguration的配置建立Feign的靜態內部類:Feign.Builder

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

2)引入了hystrix,沒有引入sleuth: 通過載入FeignClientsConfiguration的配置建立HystrixFeign的靜態內部類:HystrixFeign.Builder

	@Configuration
	@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
	protected static class HystrixFeignConfiguration {
		@Bean
		@Scope("prototype")
		@ConditionalOnMissingBean
		@ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = false)
		public Feign.Builder feignHystrixBuilder() {
			return HystrixFeign.builder();
		}
	}

3)同時引入hystrixsleuth: 載入TraceFeignClientAutoConfiguration的配置建立:HystrixFeign.Builder在這裡插入圖片描述 注意:

  • TraceFeignClientAutoConfiguration的配置類載入一定是在FeignClientsConfiguration之前(先載入先生效),而FeignClientsConfiguration載入是通過FeignAutoConfiguration完成的,所以上圖中引入了條件註解:
@AutoConfigureBefore({FeignAutoConfiguration.class})
  • 建立建立的builder物件和第二種情況一下,只是做了一層包裝:
 final class SleuthFeignBuilder {

    private SleuthFeignBuilder() {}

    static Feign.Builder builder(Tracer tracer, HttpTraceKeysInjector keysInjector) {
        return HystrixFeign.builder()
                //各元件`client,retryer,decoder`進行增強,裝飾器模式。
                .client(new TraceFeignClient(tracer, keysInjector))
                .retryer(new TraceFeignRetryer(tracer))
                .decoder(new TraceFeignDecoder(tracer))
                .errorDecoder(new TraceFeignErrorDecoder(tracer));
    }
}

2、設定重試策略,log等元件 Feign.builder在獲取之後又分別指定了重試策略,日誌級別,錯誤程式碼code等,在上一步中呼叫SleuthFeignBuilder.build()時已經設定過預設值了,這裡為什麼要重複設定呢?

我們跟進去get()方法,一探究竟:

    protected <T> T get(FeignContext context, Class<T> type) {
        //根據name,也就是服務名稱來生成builder
        T instance = context.getInstance(this.name, type);
        if (instance == null) {
            throw new IllegalStateException("No bean found of type " + type + " for "
                    + this.name);
        }
        return instance;
    }
    public <T> T getInstance(String name, Class<T> type) {
        //這裡獲取AnnotationConfigApplicationContext容器
        AnnotationConfigApplicationContext context = getContext(name);
        if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
                type).length > 0) {
            return context.getBean(type);
        }
        return null;
    }

    private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();

    protected AnnotationConfigApplicationContext getContext(String name) {
        if (!this.contexts.containsKey(name)) {
            synchronized (this.contexts) {
                if (!this.contexts.containsKey(name)) {
                    //這裡建立容器createContext(name)
                    this.contexts.put(name, createContext(name));
                }
            }
        }
        return this.contexts.get(name);
    }

重點來了,上述程式碼將FeignContext做了快取,每個服務對應一個FeignContext,服務名作為key。 繼續跟進createContext(name)方法:

protected AnnotationConfigApplicationContext createContext(String name) {
        //注意:這裡的容器並不是spring的容器,而是每次都重新建立一個
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        //載入每個服務對應的配置類
        if (this.configurations.containsKey(name)) {
            for (Class<?> configuration : this.configurations.get(name)
                    .getConfiguration()) {
                context.register(configuration);
            }
        }
        //載入啟動類@EnableFeignClients註解指定的配置類
        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);
        }
        //重新整理容器
        context.refresh();
        return context;
    }

可以看到上述AnnotationConfigApplicationContext容器並非spring容器,只是利用了spring重新整理容器的方法來例項化配置類,以服務名作為key,配置隔離。

重點來了,上面載入配置的順序為:先載入每個服務的配置類,然後載入啟動類註解上的配置類,最後載入預設的配置類。這樣做有什麼好處? spring重新整理容器的方法也是對所有的bean進行了快取,如果已經建立,則不再例項化。所以優先選取每個FeignClient的配置類,最後預設的配置類兜底。

所以這也證明了sleuth的配置一定在feign的配置類之前載入。 至此,FeignBuilder構造流程結束。

四、生成負載均衡代理類

再貼一下生成代理類的入口:

		//判斷url是否為空	
        if (!StringUtils.hasText(this.url)) {
          //......
            return loadBalance(builder, context, new HardCodedTarget<>(this.type,
                    this.name, url));
        }
	    //......
        return targeter.target(this, builder, context, new HardCodedTarget<>(
                this.type, this.name, url));

這裡有個重要判斷:判斷FeignClient宣告的url是否為空,來判斷具體要生成的代理類。如下: 這麼做有什麼意義? 1)如果為空,則預設走Ribbon代理,也就是這個入口,會有載入ribbon的處理。 @FeignClient("MyFeignClient") 2)如果不為空,指定url,則走預設生成代理類的方式,也就是所謂的硬編碼。 @FeignClient(value = "MyFeignClient",url = "http://localhost:8081") 這樣處理方便開發人員進行測試,無需關注註冊中心,直接http呼叫,是個不錯的開發小技巧。

生產環境也可以用上述第二種方式,指定域名的方式。

我們跟進loadBalance方法:

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

Client client = getOptional(context, Client.class);這裡會從FeignContext上下文中獲取Client物件,該物件有三種例項,具體是哪個實現呢? 在這裡插入圖片描述

這裡又會有三種情況: 1)沒有整合ribbonsleuth: 獲取預設的ClientDefault例項。

2)整合了ribbon,沒有整合sleuth: 獲取LoadBalanceFeignClient例項。

在這裡插入圖片描述

3)整合了ribbonsleuth: 會獲取TraceFeignClient例項,該例項是對LoadBalanceFeignClient的一種包裝,實現方式通過BeanPostProcessor實現:FeignBeanPostProcessor中定義了包裝邏輯:

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName)
			throws BeansException {
		return this.traceFeignObjectWrapper.wrap(bean);
	}

通過wrap方法最終返回TraceFeignClient例項。

繼續回到主流程,先來看下Targeter介面:

interface Targeter {
        <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
                HardCodedTarget<T> target);
    }

該物件定義在FeignClientFactoryBean靜靜態程式碼塊中:

    private static final Targeter targeter;

    static {
        Targeter targeterToUse;
        //判斷類路徑是否引入了hystrixFeign
        if (ClassUtils.isPresent("feign.hystrix.HystrixFeign",
                FeignClientFactoryBean.class.getClassLoader())) {
            targeterToUse = new HystrixTargeter();
        }
        else {
            targeterToUse = new DefaultTargeter();
        }
        targeter = targeterToUse;
    }

這裡會初始化Targeter,該類是生成feign代理類的工具類,有兩種實現,正是上面的HystrixTargeter,DefaultTargeter。 因為我們引入了hystrix,所以Targeter實現類為HystrixTargeter