SpringCloud Feign原理剖析
Feign是什么?
簡單來說,feign是用在微服務(wù)中,各個(gè)微服務(wù)間的調(diào)用。它是通過聲明式的方式來定義接口,而不用實(shí)現(xiàn)接口。接口的實(shí)現(xiàn)由它通過spring bean的動(dòng)態(tài)注冊來實(shí)現(xiàn)的。
feign讓服務(wù)間的調(diào)用變得簡單,不用各個(gè)服務(wù)去處理http client相關(guān)的邏輯。并且它里面集成了ribbon用來做負(fù)載均衡,通過集成了hystrix用來做服務(wù)熔斷和降級。
在feign的使用中,我們主要用到它的兩個(gè)注解,下面一一來說明。
注解
1、@EnableFeignClients
- 用于表示該客戶端開啟Feign方式調(diào)用
- 創(chuàng)建一個(gè)關(guān)于FeignClient的工廠Bean,這個(gè)工廠Bean會通過@FeignClient收集調(diào)用信息(服務(wù)、接口等),創(chuàng)建動(dòng)態(tài)代理對象
2、@FeignClient
負(fù)責(zé)標(biāo)識一個(gè)用于業(yè)務(wù)調(diào)用的Client,給FactoryBean提供創(chuàng)建代理對象,提供基礎(chǔ)數(shù)據(jù)(類名、方法、服務(wù)名、URI等),作用是提供這些靜態(tài)配置
實(shí)現(xiàn)原理
一般對于一個(gè)spring boot服務(wù)來說,如果要使用feign,都會這樣定義:
@Configuration
@EnableScheduling
@EnableDiscoveryClient
@EnableFeignClients(value = {"com.ts"})
@MapperScan(value = {"com.ts.xx.xx"}, nameGenerator = FullBeanNameGenerator.class)
public class xxxxAutoConfig {
}這里就使用到了上面說的EnableFeignClients這個(gè)注解,這個(gè)注解有什么用呢?我們進(jìn)入該注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {}可以看到該注解導(dǎo)入了FeignClientsRegistrar類,我們進(jìn)入其中:
class FeignClientsRegistrar
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {}
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
} 整個(gè)過程大概就是,通過配置類,或者package路徑做掃描,收集FeignClient的靜態(tài)信息,每個(gè)Client會把他的基本信息,類名、方法、服務(wù)名等綁定到FactoryBean上,這樣就就具備了生成一個(gè)動(dòng)態(tài)代理類的基本條件。
這里穿插2個(gè)知識點(diǎn)
1、spring bean的動(dòng)態(tài)注冊
在spring中有兩類bean:
- 普通的bean:通過xml配置或者注解配置
- 工廠bean:也是一個(gè)Bean,這個(gè)Bean我們業(yè)務(wù)中不會直接用到,它主要是用于生成其他的一些Bean,內(nèi)部進(jìn)行了高度封裝,非常容易實(shí)現(xiàn)配置化的管理,屏蔽了實(shí)現(xiàn)細(xì)節(jié)。動(dòng)態(tài)注冊是解決什么問題,根據(jù)客戶端的配置動(dòng)態(tài)的,也就是可以按需做bean的注冊。
1.1 使用方式
實(shí)現(xiàn)ImportBeanDefinitionRegistrar接口,重點(diǎn)實(shí)現(xiàn)registerBeanDefinitions方法,該接口需要配合@Configuration和@Import注解,
@Configuration定義Java格式的Spring配置文件,@Import注解導(dǎo)入實(shí)現(xiàn)了ImportBeanDefinitionRegistrar接口的類。這也就是上面我們看到的FeignClientsRegistrar。
那feign為什么要這樣做呢?因?yàn)樾枰刹煌拇眍惖膶?shí)現(xiàn)bean。
1.2 實(shí)現(xiàn)原理
所有實(shí)現(xiàn)了該接口的類的都會被ConfigurationClassPostProcessor處理,ConfigurationClassPostProcessor實(shí)現(xiàn)了BeanFactoryPostProcessor接口,所以ImportBeanDefinitionRegistrar中動(dòng)態(tài)注冊的bean是優(yōu)先于依賴它的bean初始化的,也能被aop、validator等機(jī)制處理。
2、FactoryBean
FactoryBean的特殊之處在于它可以向容器中注冊兩個(gè)Bean,一個(gè)是它本身,一個(gè)是FactoryBean.getObject()方法返回值所代表的Bean。
我們知道:在Spring容器啟動(dòng)階段,會調(diào)用到refresh()方法,在refresh()中有調(diào)用了finishBeanFactoryInitialization()方法,最終會調(diào)用到beanFactory.preInstantiateSingletons()方法。
在getObjectForBeanInstance()方法中會先判斷bean是不是FactoryBean,如果不是,就直接返回Bean。如果是FactoryBean,且name是以&符號開頭,那么表示的是獲取FactoryBean的原生對象,也會直接返回。如果name不是以&符號開頭,那么表示要獲取FactoryBean中getObject()方法返回的對象。會先嘗試從FactoryBeanRegistrySupport類的factoryBeanObjectCache這個(gè)緩存map中獲取,如果緩存中存在,則返回,如果不存在,則去調(diào)用getObjectFromFactoryBean()方法。
getObjectFromFactoryBean()方法中,主要是通過調(diào)用doGetObjectFromFactoryBean()方法得到bean,然后對bean進(jìn)行處理,最后放入緩存。而且還會針對單例bean和非單例bean做區(qū)分處理,對于單例bean,會在創(chuàng)建完后,將其放入到緩存中,非單例bean則不會放入緩存,而是每次都會重新創(chuàng)建。
注冊FeignClient
我們回到FeignClientsRegistrar類中,下面接著看下注冊FeignClient的實(shí)現(xiàn),代碼如下:
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//創(chuàng)建一個(gè)類掃描器
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
//獲取EnableFeignClients注解包含的屬性
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
//這是一個(gè)FeignClient注解過濾器
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
//準(zhǔn)備出待掃描的路徑
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();
//該類必須是接口,做強(qiáng)制校驗(yàn)
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
//獲取該類的所有屬性
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
//獲取服務(wù)名稱
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)建一個(gè)BeanDefinitionBuilder,內(nèi)含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 });
//動(dòng)態(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實(shí)現(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繼承了它,通過調(diào)用實(shí)現(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);
//先校驗(yàn)基礎(chǔ)屬性,基礎(chǔ)屬性是在FeignClientsRegistrar中給動(dòng)態(tài)Bean,添加屬性addPropertyValue時(shí)候賦值的
//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)建動(dòng)態(tài)的代理對象,采用JDK動(dòng)態(tài)代理
return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,
this.name, url));
}
//含有url,也就是FeignClient的注解接口里是一個(gè)絕對地址
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,這種方式一般用于調(diào)試環(huán)境,直接指定一個(gè)服務(wù)的絕對地址,這種情況下不會走負(fù)載均衡,走默認(rè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ù)載均衡的,通過如下代碼發(fā)現(xiàn)Feign的負(fù)載均衡也是基于Ribbon實(shí)現(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);
}
}相關(guān)配置
1、服務(wù)配置
大多數(shù)情況下,我們對于服務(wù)調(diào)用的超時(shí)時(shí)間可能會根據(jù)實(shí)際服務(wù)的特性做 一 些調(diào)整,所以僅僅依靠默認(rèn)的全局配置是不行的。
在使用SpringCloud Feign的時(shí)候,針對各個(gè)服務(wù)客戶端進(jìn)行個(gè)性化配置的方式與使用SpringCloud Ribbon時(shí)的配置方式是 一 樣的, 都采用. ribbon.key=value 的格式進(jìn)行 設(shè)置。
在定義Feign客戶端的時(shí)候, 我們使用了@FeignClient注解。在初始化過程中,SpringCloud Feign會根據(jù)該注解的name屬性或value屬性指定的服務(wù)名, 自動(dòng)創(chuàng)建一 個(gè)同名的Ribbon客戶端。
也就是說,在之前的示例中,使用@FeignClient(value= "cloud-provider")來創(chuàng) 建 Feign 客 戶 端 的 時(shí) 候 , 同時(shí)也創(chuàng)建了一個(gè) 名為cloud-provider的Ribbon客戶端。既然如此, 我們就可以使用@FeignClient注解中的name或value屬性值來設(shè)置對應(yīng)的Ribbon參數(shù), 比如:
cloud-provider.ribbon.ConnectTimeout = 500 //請求連接的超時(shí)時(shí)間。 cloud-provider.ribbon.ReadTimeout = 2000 //請求處理的超時(shí)時(shí)間。 cloud-provider.ribbon.OkToRetryOnAllOperations = true //對所有操作請求都進(jìn)行重試。 cloud-provider.ribbon.MaxAutoRetriesNextServer = 2 //切換實(shí)例的重試次數(shù)。 cloud-provider.ribbon.MaxAutoRetries = 1 //對當(dāng)前實(shí)例的重試次數(shù)。
2、日志配置
Spring Cloud Feign 在構(gòu)建被 @FeignClient 注解修飾的服務(wù)客戶端時(shí),會為每 一 個(gè)客戶端都創(chuàng)建 一 個(gè) feign.Logger 實(shí)例,我們可以利用該日志對象的 DEBUG 模式來幫助分析 Feign 的請求細(xì)節(jié)。
可以在 application.properties 文件中使用 logging.level. 的參數(shù)配置格式來開啟指定 Feign 客戶端的 DEBUG 日志, 其中 為 Feign 客戶端定義接口的完整路徑, 比如針對本文中我們實(shí)現(xiàn)的 HelloService 可以按如下配置開啟:
logging.level.com.wuzz.demo.HelloService = DEBUG
但是, 只是添加了如上配置, 還無法實(shí)現(xiàn)對 DEBUG 日志的輸出。這時(shí)由于 Feign 客戶端默認(rèn)的 Logger.Level 對象定義為 NONE 級別, 該級別不會記錄任何 Feign 調(diào)用過程中的信息, 所以我們需要調(diào)整它的級別, 針對全局的日志級別, 可以在應(yīng)用主類中直接加入 Logger.Level 的 Bean 創(chuàng)建, 具體如下:
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}或者添加個(gè)配置類:
@Configuration
public class FullLogConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@FeignClient(name = "cloud-provider", configuration = FullLogConfiguration.class)
public interface TestService {
}3、服務(wù)降級
根據(jù)目標(biāo)接口,創(chuàng)建一個(gè)實(shí)現(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";
}
};
}
}在目標(biāo)接口上的@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原理剖析的詳細(xì)內(nèi)容,更多關(guān)于SpringCloud Feign原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java date format時(shí)間格式化操作示例
這篇文章主要介紹了Java date format時(shí)間格式化操作,結(jié)合具體實(shí)例形式分析了java針對日期時(shí)間進(jìn)行格式化操作的相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-03-03
SpringCloud-Alibaba-Sentinel服務(wù)降級,熱點(diǎn)限流,服務(wù)熔斷
這篇文章主要介紹了SpringCloud-Alibaba-Sentinel服務(wù)降級,熱點(diǎn)限流,服務(wù)熔斷,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12
eclipse 如何創(chuàng)建 user library 方法詳解
這篇文章主要介紹了eclipse 如何創(chuàng)建 user library 方法詳解的相關(guān)資料,需要的朋友可以參考下2017-04-04
SpringCloud自定義loadbalancer實(shí)現(xiàn)標(biāo)簽路由的詳細(xì)方案
本文介紹了通過標(biāo)簽路由解決前端開發(fā)環(huán)境接口調(diào)用慢的問題,實(shí)現(xiàn)方案包括在本地服務(wù)注冊元數(shù)據(jù)、自定義負(fù)載均衡器、以及網(wǎng)關(guān)配置等步驟,通過環(huán)境變量設(shè)置標(biāo)簽,網(wǎng)關(guān)根據(jù)請求頭中的標(biāo)簽進(jìn)行路由,從而實(shí)現(xiàn)前后端互不干擾的開發(fā)調(diào)試,感興趣的朋友一起看看吧2025-02-02
淺談Springboot實(shí)現(xiàn)攔截器的兩種方式
本文詳細(xì)的介紹了Springboot攔截器的兩種方式實(shí)現(xiàn),一種就是用攔截器,一種就是過濾器,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
基于request.getAttribute與request.getParameter的區(qū)別詳解
本篇文章小編為大家介紹,基于request.getAttribute與request.getParameter的區(qū)別詳解。需要的朋友參考下2013-04-04
SpringBoot集成FastDFS依賴實(shí)現(xiàn)文件上傳的示例
這篇文章主要介紹了SpringBoot集成FastDFS依賴實(shí)現(xiàn)文件上傳,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05
Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(11)
下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧,希望可以幫到你2021-07-07

