在Spring cloud應用中,當我們要使用feign客戶端時,一般要做以下三件事情 :
使用注解@EnableFeignClients啟用feign客戶端;
示例 :
@SpringBootApplication @EnableFeignClients public class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } }
使用注解@FeignClient 定義feign客戶端 ;
示例 : 該例子定義了一個feign客戶端,將遠程服務http://test-service/test/echo映射為一個本地Java方法調用。
@FeignClient(name = "test-service", path = "/test") public interface TestService { @RequestMapping(value = "/echo", method = RequestMethod.GET) TestModel echo(@RequestParam("parameter") String parameter); }
使用注解@Autowired使用上面所定義feign的客戶端 ;
@Autowired TestService testService; public void run() { // 這里的使用本地Java API的方式調用遠程的Restful接口 TestModel dto = testService.echo("Hello,你好!"); log.info("echo : {}", dto); }
上面的三個步驟,前兩個步驟可以理解為定義feign客戶端,第三步是使用所定義的feign客戶端。通過調試發現,上面第三步所注入的testService是一個代理對象,如下所示 :
testService = {$Proxy66@5502} "HardCodedTarget(type=TestService, name=test-service, url=http://test-service/test)" h = {ReflectiveFeign$FeignInvocationHandler@6924} target = {Target$HardCodedTarget@6930} dispatch = {LinkedHashMap@6931} size = 1 0 = {LinkedHashMap$Entry@6948} "public abstract xxx.model.TestModel xxx.service.TestService.echo(java.lang.String)"
該對象會代理客戶端完成遠程服務方法的調用,那么,該代理對象是如何生成的 ?這篇文章,我們通過源代碼解析來回答這兩個問題。
源代碼解析
源代碼版本 : spring-cloud-openfeign-core-2.1.0.RELEASE , Spring Cloud Greenwich.RELEASE
注解@EnableFeignClients:掃描和注冊feign客戶端bean定義
注解@EnableFeignClients用於告訴框架掃描所有通過注解@FeignClient定義的feign客戶端。它又通過注解@Import導入了類FeignClientsRegistrar( feign客戶端注冊器),如下所示:
@EnableFeignClients => @Import(FeignClientsRegistrar.class)
FeignClientsRegistrar : feign客戶端注冊器
FeignClientsRegistrar實現了接口 ImportBeanDefinitionRegistrar。而ImportBeanDefinitionRegistrar的設計目的,就是被某個實現類實現,配合@Configuration注解的配置類,在配置類被處理時,用於額外注冊一部分bean定義:
對於上面的例子,使用者配置類就是 TestApplication
public interface ImportBeanDefinitionRegistrar { /** * Register bean definitions as necessary based on the given annotation metadata of * the importing @Configuration class. * 根據使用者配置類的注解元數據注冊bean定義 * @param importingClassMetadata 使用者配置類的注解元數據 * @param registry 當前bean定義注冊表,一般指當前Spring應用上下文對象,當前Spring容器 */ public void registerBeanDefinitions( AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry); }
#registerBeanDefinitions – 注冊feign客戶端配置和feign客戶端
方法FeignClientsRegistrar#registerBeanDefinitions實現如下:
@Override public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) { // 注冊缺省配置到容器 registry registerDefaultConfiguration(metadata, registry); // 注冊所發現的各個 feign 客戶端到到容器 registry registerFeignClients(metadata, registry); }
#registerDefaultConfiguration– 注冊feign客戶端缺省配置
// 注冊feign客戶端的缺省配置,缺省配置信息來自注解元數據的屬性 defaultConfiguration private void registerDefaultConfiguration(AnnotationMetadata metadata,BeanDefinitionRegistry registry) { // 獲取注解@EnableFeignClients的注解屬性 Map<String, Object> defaultAttrs = metadata .getAnnotationAttributes(EnableFeignClients.class.getName(), true); if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) { String name; // 下面是對所注冊的缺省配置的的命名,格式如下 : // default.xxx.TestApplication if (metadata.hasEnclosingClass()) { // 針對注解元數據metadata對應一個內部類或者方法返回的方法本地類的情形 name = "default." + metadata.getEnclosingClassName(); } else { // name 舉例 : default.xxx.TestApplication // 這里 xxx.TestApplication 是注解@EnableFeignClients所在配置類的長名稱 name = "default." + metadata.getClassName(); } registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration")); } }
#registerDefaultConfiguration方法最終注冊客戶端缺省配置的動作交給方法#registerDefaultConfiguration執行。
#registerClientConfiguration – 注冊feign客戶端配置
// 將指定feign客戶端配置configuration作為一個bean定義注冊到容器: // bean 定義對象類型 : GenericBeanDefinition // bean class : FeignClientSpecification // bean name : default.xxx.TestApplication.FeignClientSpecification (缺省配置) // bean name : test-service.FeignClientSpecification (針對某個feign client 的配置) 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()); }
#registerDefaultConfiguration方法用於注冊一個feign客戶端配置bean,可以用於注冊針對所有feign客戶端的缺省配置的注冊,也可以用於針對每個feign客戶端的專有配置的注冊。
針對所有feign客戶端的缺省配置的bean名稱類似於 : default.xxx.TestApplication.FeignClientSpecification,
針對某個名稱為test-service的feign客戶端的配置的bean名稱類似於:test-service.FeignClientSpecification。
#registerFeignClients – 注冊各個feign客戶端及其配置
// 參數 metadata : 注解@EnableFeignClients所在配置類的注解元數據 public void registerFeignClients(AnnotationMetadata metadata,BeanDefinitionRegistry registry) { // 定義一個基於classpath的組件掃描器,它會根據指定的掃描位置和@EnableFeignClients注解屬性 // 找出開發人員定義的所有feign客戶端,也就是那些使用了注解@FeignClient的所有接口定義 ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(this.resourceLoader); Set<String> basePackages; // attrs 用於表示注解@EnableFeignClients所在配置類的注解元數據中注解@EnableFeignClients // 的部分 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) { // @EnableFeignClients 中沒有指定 clients 屬性的情況 scanner.addIncludeFilter(annotationTypeFilter); basePackages = getBasePackages(metadata); } else { // @EnableFeignClients 中指定了 clients 屬性的情況 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))); } // 使用 scanner 掃描每一個 basePackage, 獲取其中的 feign 客戶端定義, // 也就是 @FeignClient 定義的那些接口 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"); // 獲取所定義的feign客戶端接口上的注解@FeignClient屬性 Map<String, Object> attributes = annotationMetadata .getAnnotationAttributes( FeignClient.class.getCanonicalName()); String name = getClientName(attributes); // 將所定義的feign客戶端上的配置屬性作為一個bean注冊到容器 registerClientConfiguration(registry, name, attributes.get("configuration")); // 將所定義的feign客戶端作為一個bean注冊到容器: // bean 定義類型 : GenericBeanDefinition // bean class : FeignClientFactoryBean // autowire 模式 : 根據類型綁定 // @FeignClient注解中的url,path,fallback等屬性會設置為bean定義的屬性 registerFeignClient(registry, annotationMetadata, attributes); } } } } // 輔助工具類,從@EnableFeignClients注解屬性中獲取basePackages屬性: // 參考以下@EnableFeignClients注解屬性 : // 1. value // 2. basePackages // 3. basePackageClasses // 4. 配置類所在的包 // 參數 importingClassMetadata : 使用注解@EnableFeignClients的配置類的元數據 protected Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) { // 注解@EnableFeignClients的屬性 Map<String, Object> attributes = importingClassMetadata .getAnnotationAttributes(EnableFeignClients.class.getCanonicalName()); Set<String> basePackages = new HashSet<>(); for (String pkg : (String[]) attributes.get("value")) { if (StringUtils.hasText(pkg)) { basePackages.add(pkg); } } for (String pkg : (String[]) attributes.get("basePackages")) { if (StringUtils.hasText(pkg)) { basePackages.add(pkg); } } for (Class<?> clazz : (Class[]) attributes.get("basePackageClasses")) { basePackages.add(ClassUtils.getPackageName(clazz)); } if (basePackages.isEmpty()) { basePackages.add( ClassUtils.getPackageName(importingClassMetadata.getClassName())); } return basePackages; }
#registerFeignClients 最終注冊feign客戶端配置的動作交給#registerClientConfiguration完成,注冊feign客戶端的動作交給#registerFeignClient方法完成。
#registerFeignClient – 注冊一個feign客戶端
// 將所定義的feign客戶端作為一個bean注冊到容器: // bean 定義類型 : GenericBeanDefinition // bean class : FeignClientFactoryBean -- 這是一個工廠bean,而不是最終bean實例的class // autowire 模式 : 根據類型綁定 // @FeignClient注解中的url,path,fallback等屬性會設置為bean定義的屬性 // 參數 registry : Spring 容器 // 參數 annotationMetadata : @FeignClient所注解的接口上的注解元數據 // 參數 attributes : @FeignClient 注解屬性信息 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); 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 = name + "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 }); BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); }
從上面的代碼分析可知,FeignClientsRegistrar的主要作用如下 :
注冊缺省feign客戶端配置bean定義;
對於每個@FeignClient注解的feign客戶端定義 :
注冊一個針對該feign客戶端的配置bean定義;
注冊該feign客戶端bean定義,生成bean采用工廠類FeignClientFactoryBean;
而且,上述功能實現在FeignClientsRegistrar實現的接口ImportBeanDefinitionRegistrar所定義的方法registerBeanDefinitions中。而該方法會在應用的@EnableFeignClients注解被處理時被調用執行。具體的執行時調用棧如下所示:
AbstractApplicationContext#invokeBeanFactoryPostProcessors => PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors => foreach BeanDefinitionRegistryPostProcessor : #postProcessBeanDefinitionRegistry => ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry => #processConfigBeanDefinitions => ConfigurationClassBeanDefinitionReader#loadBeanDefinitions => foreach ConfigurationClass : #loadBeanDefinitionsForConfigurationClass => #loadBeanDefinitionsFromRegistrars => foreach ImportBeanDefinitionRegistrar : #registerBeanDefinitions => FeignClientsRegistrar#registerBeanDefinitions
FeignClientFactoryBean生成feign客戶端代理對象
基於上面的分析,我們可以得知,開發人員所定義的feign客戶端和相關配置會以bean定義的形式注冊到bean容器中,這樣當使用@Autowired注入一個feign客戶端時,容器會使用工廠類FeignClientFactoryBean為其生成一個實例。下面我們來看其具體工作過程。
FeignClientFactoryBean#getObject生成feign客戶端代理對象
// 該方法由接口FactoryBean約定 @Override public Object getObject() throws Exception { return getTarget(); } <T> T getTarget() { // 從應用上下文中獲取創建 feign 客戶端的上下文對象 FeignContext // FeignContext 針對每個feign客戶端定義會生成一個不同的 AnnotationConfigApplicationContext, // 這些應用上下文的parent都設置為當前應用的主應用上下文 // 參考 : FeignAutoConfiguration FeignContext context = applicationContext.getBean(FeignContext.class); // 為目標feign客戶端對象構建一個 builder,該builder最終生成的目標feign客戶端是一個 // 動態代理,使用 InvocationHandler : ReflectiveFeign$FeignInvocationHandler Feign.Builder builder = feign(context); if (!StringUtils.hasText(this.url)) { // @FeignClient 屬性 url 屬性沒有指定的情況 // 根據屬性 name , path 拼裝一個 url, // 這種通常是需要在多個服務節點之間進行負載均衡的情況 if (!this.name.startsWith("http")) { url = "http://" + this.name; } else { url = this.name; } // 方法cleanPath()加工屬性path,使其以/開頭,不以/結尾 url += cleanPath(); // 這里形成的url格式類似 : http://test-service/test // 其中 test-service 是服務名,不是服務所在節點的IP,主機名或者域名 // 函數loadBalance 做如下動作 : // 1. 將builder和一個LoadBalancerFeignClient bean實例關聯起來 // 2. 使用一個HystrixTargeter將builder和一個 HardCodedTarget bean實例關聯起來 // 這里 HardCodedTarget 表示對應 url 為 http://test-service/test 的遠程服務(可能 // 包含多個服務方法) // 3. 生成最終的feign client 實例 : ReflectiveFeign$FeignInvocationHandler 的動態代理對象, // 使用 InvocationHandler : ReflectiveFeign$FeignInvocationHandler。 // 每個遠程服務方法會對應到一個@FeignClient注解的接口方法上(依據方法上的注解進行匹配) return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, url)); } // @FeignClient 屬性 url 屬性被指定的情況 // 這種通常是明確指出了服務節點的url的情況,實際上不需要負載均衡 if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) { this.url = "http://" + this.url; } String url = this.url + cleanPath(); // 將builder和一個LoadBalancerFeignClient bean實例關聯起來 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 // 因為指定了明確的服務節點url,所以這里不需要負載均衡, // 所以這里盡管client是LoadBalancerFeignClient,所以 // 實際上可以獲取其所代理的對象作為最終的client, // 相當於去掉了LoadBalancerFeignClient這層的代理功能 client = ((LoadBalancerFeignClient)client).getDelegate(); } builder.client(client); } // 使用一個HystrixTargeter將builder和一個 HardCodedTarget bean實例關聯起來 Targeter targeter = get(context, Targeter.class); // 生成最終的feign client 實例 : ReflectiveFeign$FeignInvocationHandler 的動態代理對象, // 使用 InvocationHandler : ReflectiveFeign$FeignInvocationHandler。 // 每個遠程服務方法會對應到 一個@FeignClient注解的接口方法上(依據方法上的注解進行匹配) return (T) targeter.target(this, builder, context, new HardCodedTarget<>( this.type, this.name, url)); }
方法FeignClientFactoryBean#feign – 創建feign客戶端構建器
protected Feign.Builder feign(FeignContext context) { FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class); Logger logger = loggerFactory.create(this.type); // 從上下文獲取一個 Feign.Builder 上, // 並從上下文獲得 Encoder, Decoder, Contract 設置到該 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)); // 對 builder 進行其他屬性設置 configureFeign(context, builder); return builder; }
方法FeignClientFactoryBean#loadBalance – 生成具備負載均衡能力的feign客戶端
為feign客戶端構建器綁定負載均衡客戶端,綁定目標服務端點,並生成最終的feign客戶端實例。
// 對builder設置負載均衡客戶端,綁定到目標服務端點,構建最終的feign客戶端對象 protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) { // 從上下文context獲取一個Client,缺省是 LoadBalancerFeignClient Client client = getOptional(context, Client.class); if (client != null) { // 將client設置到builder上 builder.client(client); // 從上下文中獲取一個 targeter,缺省是一個 HystrixTargeter Targeter targeter = get(context, Targeter.class); // 上面獲取得到的 targeter 會根據 builder 的類型決定如何將 target // 綁定到 builder 並設置有關的其他屬性和功能,然后生成最終的feign客戶端對象 return targeter.target(this, builder, context, target); } throw new IllegalStateException( "No Feign Client for loadBalancing defined. Did you forget to include " + "spring-cloud-starter-netflix-ribbon?"); }
從上面分析可以看出,缺省情況下,所使用的feign客戶端構建器類為Feign.Builder,並且Targeter是一個HystrixTargeter。HystrixTargeter#target方法的參數builder為Feign.Builder時,會直接調用該builder的target方法,如下所示 :
class HystrixTargeter implements Targeter { @Override public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, Target.HardCodedTarget<T> target) { if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) { return feign.target(target); } // ... 省略其他代碼 } }
接下來再來看Feign.Builder#target是如何工作的。
// 執行構建並且創建相應的feign客戶端實例 public <T> T target(Target<T> target) { return build().newInstance(target); } // 構建過程,最終根據各種配置生成一個 ReflectiveFeign 對象 public Feign build() { SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, logLevel, decode404, closeAfterDecode, propagationPolicy); ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, errorDecoder, synchronousMethodHandlerFactory); return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder); } }
然后再看ReflectiveFeign#newInstance方法。
// 創建最終的feign客戶端實例 : 一個 ReflectiveFeign$FeignInvocationHandler 的動態代理對象 @Override public <T> T newInstance(Target<T> target) { Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); for (Method method : target.type().getMethods()) { if (method.getDeclaringClass() == Object.class) { continue; } else if (Util.isDefault(method)) { // 對於每個缺省方法,使用 DefaultMethodHandler DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { // 對於每個對應服務功能端點的方法,缺省使用nameToHandler獲取的MethodHandler,缺省是 // SynchronousMethodHandler methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } // 創建feign客戶端實例 ReflectiveFeign$FeignInvocationHandler, // 該對象包含了上面所創建的methodToHandler,用於對應各個開發者定義的@FeignClient接口方法 InvocationHandler handler = factory.create(target, methodToHandler); // 創建feign客戶端實例的動態代理對象 T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[] {target.type()}, handler); // 將缺省方法處理器綁定到feign客戶端實例的動態代理對象上 for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; }
從上面的分析我們不難看出,為什么最終注入的testService最終是一個ReflectiveFeign$FeignInvocationHandler動態代理實例了。
總結
從上面的分析可以看出,當我們使用注解@EnableFeignClients 時,相當於啟用了feign客戶端定義的掃描和注冊機制,從而可以發現開發人員通過注解@FeignClient定義的feign客戶端,並最終作為bean定義注冊到容器中。而通過@Autowired自動裝配注解,這些feign客戶端會以ReflectiveFeign$FeignInvocationHandler動態代理的形式被注入到使用方。該feign客戶端包含了對每個接口方法的處理器MethodHandler,接口缺省方法對應DefaultMethodHandler,服務功能端點方法對應SynchronousMethodHandler。