天天看點

SpringCloud.Honxton 版本 OpenFeign原理 下篇

SpringCloud.Honxton 版本 OpenFeign原理

    • 前置說明
    • 如何使用
    • springcloud 和 openfeign 整合原理
    • 總結

前置說明

上篇介紹了openfeign的使用和原理, 那麼這篇将介紹和springcloud 的整合部分. 有了上一篇的基礎, 那麼在分析原理的時候就簡單很多了, 更多的将關注于springcloud 的整合部分.

使用的依賴

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

如何使用

按照慣例還是先介紹如何使用feign

@FeignClient("client2") // service-id
public interface HelloOpenfeignSpringcloud {

   
    @RequestMapping(name=" /feign",method = RequestMethod.GET, produces="application/x-www-form-urlencoded")
    String feign(@RequestParam("feign") String feign);

    @RequestMapping(name="/feignPost",method = RequestMethod.POST, produces="application/x-www-form-urlencoded")
    String feignPost(@RequestParam("feign") String feign);

    @RequestMapping(name = "/feignJson",method = RequestMethod.POST , produces="application/json")
    String feignJson(@RequestBody String feign);

}

           

另外還需要加上

@EnableFeignClients

注解放到啟動類上

可以看到, 整合之後的用法的優點有以下幾點

  1. 不在需要手動調用代理的生成
  2. 可以使用我們熟悉的springmvc注解
  3. 結合注冊中心之後,不需要我們指定url, 且能支援負載均衡

    處此之外, springcloud 提供了熔斷器和feign的整合, 用法也很簡單, 就是給接口增加個實作類,作為降級處理的邏輯.

    當然缺點也有很多啦, 包括傳遞檔案時會有些問題, 然後傳遞對象必須要用@RequestBody, 但是也算是一個公認較好的用戶端遠端調用架構。

springcloud 和 openfeign 整合原理

了解完基本使用, 那麼就是揭開springcloud整合feign面紗的時候了

以下是所有的feign的自動配置類

org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,

org.springframework.cloud.openfeign.hateoas.FeignHalAutoConfiguration,

org.springframework.cloud.openfeign.FeignAutoConfiguration,

org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,

org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration,

org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration

其中最重要的就是

FeignAutoConfiguration

以及

FeignRibbonClientAutoConfiguration

或者

FeignLoadBalancerAutoConfiguration

那麼我們一個個分析

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Feign.class) // 條件裝配
@EnableConfigurationProperties({ FeignClientProperties.class,
		FeignHttpClientProperties.class })
@Import(DefaultGzipDecoderConfiguration.class)
public class FeignAutoConfiguration {

	@Autowired(required = false)  // 單獨的feignclient配置
	private List<FeignClientSpecification> configurations = new ArrayList<>();

	@Bean  // feignclient的  spring命名工廠, 主要根據不同的serviceId建立不同的applicationContext 然後擷取專屬的配置, 很友善的和其他的client進行配置的隔離
	public FeignContext feignContext() {
		FeignContext context = new FeignContext();
		context.setConfigurations(this.configurations);
		return context;
	}

	// 是否開啟了 feign和hystrix的整合
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(name = "feign.hystrix.HystrixFeign")
	protected static class HystrixFeignTargeterConfiguration {
		// 這裡的Targeter接口是, springcloud 的一個抽象, 預設的實作是DefaultTargeter ,會委托Feign.Builder的target方法, 也就是我上篇講到的
		@Bean
		@ConditionalOnMissingBean
		public Targeter feignTargeter() {
			return new HystrixTargeter();
		}

	}
	// 沒有開啟hystrix 的情況
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnMissingClass("feign.hystrix.HystrixFeign")
	protected static class DefaultFeignTargeterConfiguration {

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

	}


	// feign 的底層調用實作 , 這裡隻粘貼了和httpclient相關的bean 定義
	@Configuration(proxyBeanMethods = false)
	@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;
		}
		// 這個client接口就是上篇講到的用于替換底層實作的地方, 這裡用httpclient來實作了, 但是通常不是使用這裡的這個bean後面會有提到
		@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();
			}
		}

	}
...

}

           

下面來分别介紹一下feign底層負載均衡的實作, 這裡有2種, 一種是ribbon的底層整合,另一種是我之前的文章介紹過的spring自己研發的loadbalancer.

@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled",
		matchIfMissing = true)
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(FeignAutoConfiguration.class) //在上一個自動配置之前配置
@EnableConfigurationProperties({ FeignHttpClientProperties.class })
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
		OkHttpFeignLoadBalancedConfiguration.class,
		DefaultFeignLoadBalancedConfiguration.class }) // 這裡導入 相關底層client的是實作的client的bean配置
public class FeignRibbonClientAutoConfiguration {
	
	@Bean
	@Primary
	@ConditionalOnMissingBean
	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
	public CachingSpringLoadBalancerFactory cachingLBClientFactory(
			SpringClientFactory factory) {
		return new CachingSpringLoadBalancerFactory(factory);
	}
	// 用于建立 負載均衡器 給Client 進行execute的委派執行
	@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;
	}

}

//這裡介紹一個導入的底層client實作, 預設其實還是使用feign原生的DefaultClient
@Configuration(proxyBeanMethods = false)
class DefaultFeignLoadBalancedConfiguration {
	
	@Bean
	@ConditionalOnMissingBean
	public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
			SpringClientFactory clientFactory) {
		return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
				clientFactory);
	}

}


           

另外看下另一個負載均衡器和feign的整合

// 實作非常的簡單
@ConditionalOnClass(Feign.class)
@ConditionalOnBean(BlockingLoadBalancerClient.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
@AutoConfigureAfter(FeignRibbonClientAutoConfiguration.class)
@EnableConfigurationProperties(FeignHttpClientProperties.class)
@Configuration(proxyBeanMethods = false)
@Import({ HttpClientFeignLoadBalancerConfiguration.class,
		OkHttpFeignLoadBalancerConfiguration.class,
		DefaultFeignLoadBalancerConfiguration.class }) //僅僅是進行了配置導入, 和上面ribbon 一樣
class FeignLoadBalancerAutoConfiguration {

}

// 這個導入的配置類也是提供一個Client的實作
@Configuration(proxyBeanMethods = false)
class DefaultFeignLoadBalancedConfiguration {

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

}

           

看完上面的全部配置類,我相信可能大家都暈了, 其實不用擔心, 因為下面就會講FeignClient掃描建立FeignClientFactoryBean的時候, 到時候就會将上面配置bean都注入進來使用.

首先是

@EnableFeignClients

注解就是開啟掃描的功能

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class) //導入掃描器
public @interface EnableFeignClients {
	String[] value() default {};
	String[] basePackages() default {};//配置掃描的包
... 


}
//@FeignClient掃描器
class FeignClientsRegistrar
		implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

  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)));
		}
		// 循環掃描和注冊beandefinition
		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"));
						//注冊方法
					registerFeignClient(registry, annotationMetadata, attributes);
				}
			}
		}
	}

	private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
		validate(attributes);
		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);

		String alias = contextId + "FeignClient";
		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

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

		beanDefinition.setPrimary(primary);

		String qualifier = getQualifier(attributes);
		if (StringUtils.hasText(qualifier)) {
			alias = qualifier;
		}

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

以上是關于掃描相關的實作原理, 所有打着注解的都接口, 最後都變成了

FeignClientFactoryBean

形式注冊成bean了, 接下來就簡單了, 隻要我們盯着

FeignClientFactoryBean

的getObject方法就能揭曉最後的謎底了.

class FeignClientFactoryBean
		implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {


<T> T getTarget() {
		FeignContext context = this.applicationContext.getBean(FeignContext.class);
		// 擷取Builder, 之後就是和上篇講的源碼類似了
		Feign.Builder builder = feign(context);

		if (!StringUtils.hasText(this.url)) {
			if (!this.name.startsWith("http")) {
				this.url = "http://" + this.name;
			}
			else {
				this.url = this.name;
			}
			this.url += cleanPath();
			return (T) loadBalance(builder, context,
					new HardCodedTarget<>(this.type, this.name, this.url));
		}
		if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
			this.url = "http://" + this.url;
		}
		String url = this.url + cleanPath();
		Client client = getOptional(context, Client.class); // 擷取負載均衡用戶端
		if (client != null) {
			if (client instanceof LoadBalancerFeignClient) {
				client = ((LoadBalancerFeignClient) client).getDelegate();
			}
			builder.client(client);
		}
		// 這裡的 Targeter前面也有介紹, 這裡預設是 DefaultTargeter
		Targeter targeter = get(context, Targeter.class);
		return (T) targeter.target(this, builder, context, //這裡進去就是就會驚奇的發現就是上一篇的 入口方法
				new HardCodedTarget<>(this.type, this.name, url));
	}

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

		// @formatter:off   其實我看到這裡會有個疑問, 這個bean是怎麼從 對應的命名applicationContext擷取的, 那麼下面會給出答案
		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.Builder.class 這個bean是如何擷取的

// 這個是最早介紹的被注冊成bean 的, 其實還包含了一個配置類 FeignClientsConfiguration
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {

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

}

@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {
...
	@Bean
	@Scope("prototype")
	@ConditionalOnMissingBean
	public Feign.Builder feignBuilder(Retryer retryer) {
		return Feign.builder().retryer(retryer);
	}

...

}

           

總結

最後總結一下, springcloud為了整合feign做了很多的工作, 包括很多為了結合負載均衡器 以及 hystrix 做的很多擴充實作。 這也是導緻配置類急劇膨脹 ,讓我們看的眼花缭亂, 不知道看完這篇文章有沒有讓你撥開雲霧見青天的趕腳,但是我也是盡可能的去将我所了解的講出來了, 要是覺得我有講錯的可以在下方評論交流。