SpringCloud Feign原理剖析
Feign是什么?
簡單來說,feign是用在微服務中,各個微服務間的調用。它是通過聲明式的方式來定義接口,而不用實現(xiàn)接口。接口的實現(xiàn)由它通過spring bean的動態(tài)注冊來實現(xiàn)的。
feign讓服務間的調用變得簡單,不用各個服務去處理http client相關的邏輯。并且它里面集成了ribbon用來做負載均衡,通過集成了hystrix用來做服務熔斷和降級。
在feign的使用中,我們主要用到它的兩個注解,下面一一來說明。
注解
1、@EnableFeignClients
- 用于表示該客戶端開啟Feign方式調用
- 創(chuàng)建一個關于FeignClient的工廠Bean,這個工廠Bean會通過@FeignClient收集調用信息(服務、接口等),創(chuàng)建動態(tài)代理對象
2、@FeignClient
負責標識一個用于業(yè)務調用的Client,給FactoryBean提供創(chuàng)建代理對象,提供基礎數(shù)據(類名、方法、服務名、URI等),作用是提供這些靜態(tài)配置
實現(xiàn)原理
一般對于一個spring boot服務來說,如果要使用feign,都會這樣定義:
@Configuration @EnableScheduling @EnableDiscoveryClient @EnableFeignClients(value = {"com.ts"}) @MapperScan(value = {"com.ts.xx.xx"}, nameGenerator = FullBeanNameGenerator.class) public class xxxxAutoConfig { }
這里就使用到了上面說的EnableFeignClients
這個注解,這個注解有什么用呢?我們進入該注解:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients {}
可以看到該注解導入了FeignClientsRegistrar
類,我們進入其中:
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {} @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { registerDefaultConfiguration(metadata, registry); registerFeignClients(metadata, registry); }
整個過程大概就是,通過配置類,或者package路徑做掃描,收集FeignClient的靜態(tài)信息,每個Client會把他的基本信息,類名、方法、服務名等綁定到FactoryBean上,這樣就就具備了生成一個動態(tài)代理類的基本條件。
這里穿插2個知識點
1、spring bean的動態(tài)注冊
在spring中有兩類bean:
- 普通的bean:通過xml配置或者注解配置
- 工廠bean:也是一個Bean,這個Bean我們業(yè)務中不會直接用到,它主要是用于生成其他的一些Bean,內部進行了高度封裝,非常容易實現(xiàn)配置化的管理,屏蔽了實現(xiàn)細節(jié)。動態(tài)注冊是解決什么問題,根據客戶端的配置動態(tài)的,也就是可以按需做bean的注冊。
1.1 使用方式
實現(xiàn)ImportBeanDefinitionRegistrar
接口,重點實現(xiàn)registerBeanDefinitions方法,該接口需要配合@Configuration
和@Import
注解,
@Configuration
定義Java格式的Spring配置文件,@Import
注解導入實現(xiàn)了ImportBeanDefinitionRegistrar
接口的類。這也就是上面我們看到的FeignClientsRegistrar
。
那feign為什么要這樣做呢?因為需要生成不同的代理類的實現(xiàn)bean。
1.2 實現(xiàn)原理
所有實現(xiàn)了該接口的類的都會被ConfigurationClassPostProcessor
處理,ConfigurationClassPostProcessor
實現(xiàn)了BeanFactoryPostProcessor
接口,所以ImportBeanDefinitionRegistrar
中動態(tài)注冊的bean是優(yōu)先于依賴它的bean初始化的,也能被aop、validator等機制處理。
2、FactoryBean
FactoryBean的特殊之處在于它可以向容器中注冊兩個Bean,一個是它本身,一個是FactoryBean.getObject()方法返回值所代表的Bean。
我們知道:在Spring容器啟動階段,會調用到refresh()
方法,在refresh()
中有調用了finishBeanFactoryInitialization()
方法,最終會調用到beanFactory.preInstantiateSingletons()
方法。
在getObjectForBeanInstance()
方法中會先判斷bean是不是FactoryBean,如果不是,就直接返回Bean。如果是FactoryBean,且name是以&
符號開頭,那么表示的是獲取FactoryBean的原生對象,也會直接返回。如果name不是以&符號開頭,那么表示要獲取FactoryBean中getObject()
方法返回的對象。會先嘗試從FactoryBeanRegistrySupport
類的factoryBeanObjectCache
這個緩存map中獲取,如果緩存中存在,則返回,如果不存在,則去調用getObjectFromFactoryBean()
方法。
getObjectFromFactoryBean()
方法中,主要是通過調用doGetObjectFromFactoryBean()
方法得到bean,然后對bean進行處理,最后放入緩存。而且還會針對單例bean和非單例bean做區(qū)分處理,對于單例bean,會在創(chuàng)建完后,將其放入到緩存中,非單例bean則不會放入緩存,而是每次都會重新創(chuàng)建。
注冊FeignClient
我們回到FeignClientsRegistrar
類中,下面接著看下注冊FeignClient的實現(xiàn),代碼如下:
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { //創(chuàng)建一個類掃描器 ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(this.resourceLoader); Set<String> basePackages; //獲取EnableFeignClients注解包含的屬性 Map<String, Object> attrs = metadata .getAnnotationAttributes(EnableFeignClients.class.getName()); //這是一個FeignClient注解過濾器 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) { //先掃描出含有@FeignClient注解的類 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")); //循環(huán)對使用@FeignClient注解的不同的類,做工廠bean注冊 registerFeignClient(registry, annotationMetadata, attributes); } } } } private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { //含有@FeignClient該類的名稱 String className = annotationMetadata.getClassName(); //創(chuàng)建一個BeanDefinitionBuilder,內含AbstractBeanDefinition,指定待創(chuàng)建的Bean的名字是FeignClientFactoryBean 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 }); //動態(tài)注冊 BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); } //BeanDefinitionReaderUtils.registerBeanDefinition public static void registerBeanDefinition( BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException { // Register bean definition under primary name. String beanName = definitionHolder.getBeanName(); //BeanDefinitionRegistry.registerBeanDefinition實現(xiàn)具體的注冊,會告知他需要注冊的類名、以及AbstractBeanDefinition registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); // Register aliases for bean name, if any. String[] aliases = definitionHolder.getAliases(); if (aliases != null) { for (String alias : aliases) { registry.registerAlias(beanName, alias); } } }
創(chuàng)建代理類
在上面提到的FactoryBean
,可以看到FeignClientFactoryBean
繼承了它,通過調用實現(xiàn)類的getObject完成代理類的創(chuàng)建:
@Override public Object getObject() throws Exception { return getTarget(); } <T> T getTarget() { FeignContext context = applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context); //先校驗基礎屬性,基礎屬性是在FeignClientsRegistrar中給動態(tài)Bean,添加屬性addPropertyValue時候賦值的 //URL為空,則使用http://serviceName的方式拼接 if (!StringUtils.hasText(this.url)) { String url; if (!this.name.startsWith("http")) { url = "http://" + this.name; } else { url = this.name; } url += cleanPath(); //創(chuàng)建動態(tài)的代理對象,采用JDK動態(tài)代理 return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, url)); } //含有url,也就是FeignClient的注解接口里是一個絕對地址 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) { // not load balancing because we have a url, // but ribbon is on the classpath, so unwrap client = ((LoadBalancerFeignClient)client).getDelegate(); } builder.client(client); } Targeter targeter = get(context, Targeter.class); return (T) targeter.target(this, builder, context, new HardCodedTarget<>( this.type, this.name, url)); }
從上面代碼可以知道,如果用戶在FeignClient的注解中直接使用了URL,這種方式一般用于調試環(huán)境,直接指定一個服務的絕對地址,這種情況下不會走負載均衡,走默認的Client,代碼如下:
@Override public Response execute(Request request, Options options) throws IOException { HttpURLConnection connection = convertAndSend(request, options); return convertResponse(connection).toBuilder().request(request).build(); }
如果用戶在FeignClient中使用了seriveName,那么請求地址將會是http://serviceName,這種情況下是需要走負載均衡的,通過如下代碼發(fā)現(xiàn)Feign的負載均衡也是基于Ribbon實現(xiàn):
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); } }
相關配置
1、服務配置
大多數(shù)情況下,我們對于服務調用的超時時間可能會根據實際服務的特性做 一 些調整,所以僅僅依靠默認的全局配置是不行的。
在使用SpringCloud Feign的時候,針對各個服務客戶端進行個性化配置的方式與使用SpringCloud Ribbon時的配置方式是 一 樣的, 都采用. ribbon.key=value 的格式進行 設置。
在定義Feign客戶端的時候, 我們使用了@FeignClient注解。在初始化過程中,SpringCloud Feign會根據該注解的name屬性或value屬性指定的服務名, 自動創(chuàng)建一 個同名的Ribbon客戶端。
也就是說,在之前的示例中,使用@FeignClient(value= "cloud-provider")來創(chuàng) 建 Feign 客 戶 端 的 時 候 , 同時也創(chuàng)建了一個 名為cloud-provider的Ribbon客戶端。既然如此, 我們就可以使用@FeignClient注解中的name或value屬性值來設置對應的Ribbon參數(shù), 比如:
cloud-provider.ribbon.ConnectTimeout = 500 //請求連接的超時時間。 cloud-provider.ribbon.ReadTimeout = 2000 //請求處理的超時時間。 cloud-provider.ribbon.OkToRetryOnAllOperations = true //對所有操作請求都進行重試。 cloud-provider.ribbon.MaxAutoRetriesNextServer = 2 //切換實例的重試次數(shù)。 cloud-provider.ribbon.MaxAutoRetries = 1 //對當前實例的重試次數(shù)。
2、日志配置
Spring Cloud Feign 在構建被 @FeignClient 注解修飾的服務客戶端時,會為每 一 個客戶端都創(chuàng)建 一 個 feign.Logger 實例,我們可以利用該日志對象的 DEBUG 模式來幫助分析 Feign 的請求細節(jié)。
可以在 application.properties 文件中使用 logging.level. 的參數(shù)配置格式來開啟指定 Feign 客戶端的 DEBUG 日志, 其中 為 Feign 客戶端定義接口的完整路徑, 比如針對本文中我們實現(xiàn)的 HelloService 可以按如下配置開啟:
logging.level.com.wuzz.demo.HelloService = DEBUG
但是, 只是添加了如上配置, 還無法實現(xiàn)對 DEBUG 日志的輸出。這時由于 Feign 客戶端默認的 Logger.Level 對象定義為 NONE 級別, 該級別不會記錄任何 Feign 調用過程中的信息, 所以我們需要調整它的級別, 針對全局的日志級別, 可以在應用主類中直接加入 Logger.Level 的 Bean 創(chuàng)建, 具體如下:
@Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; }
或者添加個配置類:
@Configuration public class FullLogConfiguration { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } @FeignClient(name = "cloud-provider", configuration = FullLogConfiguration.class) public interface TestService { }
3、服務降級
根據目標接口,創(chuàng)建一個實現(xiàn)了FallbackFactory的類
@Component public class HystrixClientService implements FallbackFactory<ClientService> { @Override public ClientService create(Throwable throwable) { return new ClientService() { @Override public String test() { return "test sdfsdfsf"; } }; } }
在目標接口上的@FeignClient中添加fallbackFactory屬性值
@FeignClient(value ="cloud-provider", fallbackFactory = HystrixClientService.class) public interface ClientService { @RequestMapping(value ="/test",method= RequestMethod.GET) String test() ; }
修改 application.yml ,添加一下
feign: hystrix: enabled: true
以上就是SpringCloud Feign原理剖析的詳細內容,更多關于SpringCloud Feign原理的資料請關注腳本之家其它相關文章!
相關文章
SpringCloud-Alibaba-Sentinel服務降級,熱點限流,服務熔斷
這篇文章主要介紹了SpringCloud-Alibaba-Sentinel服務降級,熱點限流,服務熔斷,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12eclipse 如何創(chuàng)建 user library 方法詳解
這篇文章主要介紹了eclipse 如何創(chuàng)建 user library 方法詳解的相關資料,需要的朋友可以參考下2017-04-04SpringCloud自定義loadbalancer實現(xiàn)標簽路由的詳細方案
本文介紹了通過標簽路由解決前端開發(fā)環(huán)境接口調用慢的問題,實現(xiàn)方案包括在本地服務注冊元數(shù)據、自定義負載均衡器、以及網關配置等步驟,通過環(huán)境變量設置標簽,網關根據請求頭中的標簽進行路由,從而實現(xiàn)前后端互不干擾的開發(fā)調試,感興趣的朋友一起看看吧2025-02-02基于request.getAttribute與request.getParameter的區(qū)別詳解
本篇文章小編為大家介紹,基于request.getAttribute與request.getParameter的區(qū)別詳解。需要的朋友參考下2013-04-04SpringBoot集成FastDFS依賴實現(xiàn)文件上傳的示例
這篇文章主要介紹了SpringBoot集成FastDFS依賴實現(xiàn)文件上傳,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-05-05