OpenFeign是一個遠端客戶端請求代理,它的基本作用是讓開發者能夠以面向介面的方式來實現遠端呼叫,從而遮蔽底層通訊的複雜性,它的具體原理如下圖所示。
在今天的內容中,我們需要詳細分析OpenFeign它的工作原理及原始碼,我們繼續回到這段程式碼。
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
IGoodsServiceFeignClient goodsServiceFeignClient;
@Autowired
IPromotionServiceFeignClient promotionServiceFeignClient;
@Autowired
IOrderServiceFeignClient orderServiceFeignClient;
/**
* 下單
*/
@GetMapping
public String order(){
String goodsInfo=goodsServiceFeignClient.getGoodsById();
String promotionInfo=promotionServiceFeignClient.getPromotionById();
String result=orderServiceFeignClient.createOrder(goodsInfo,promotionInfo);
return result;
}
}
從這段程式碼中,先引出對於OpenFeign功能實現的思考。
- 宣告
@FeignClient
註解的介面,如何被解析和注入的? - 通過
@Autowired
依賴注入,到底是注入一個什麼樣的例項 - 基於FeignClient宣告的介面被解析後,如何儲存?
- 在發起方法呼叫時,整體的工作流程是什麼樣的?
- OpenFeign是如何整合Ribbon做負載均衡解析?
帶著這些疑問,開始去逐項分析OpenFeign的核心原始碼
OpenFeign註解掃描與解析
思考, 一個被宣告瞭@FeignClient
註解的介面,使用@Autowired
進行依賴注入,而最終這個介面能夠正常被注入例項。
從這個結果來看,可以得到兩個結論
- 被
@FeignClient
宣告的介面,在Spring容器啟動時,會被解析。 - 由於被Spring容器載入的是介面,而介面又沒有實現類,因此Spring容器解析時,會生成一個動態代理類。
EnableFeignClient
@FeignClient
註解是在什麼時候被解析的呢?基於我們之前所有積累的知識,無非就以下這幾種
- ImportSelector,批量匯入bean
- ImportBeanDefinitionRegistrar,匯入bean宣告並進行註冊
- BeanFactoryPostProcessor , 一個bean被裝載的前後處理器
在這幾個選項中,似乎ImportBeanDefinitionRegistrar
更合適,因為第一個是批量匯入一個bean的string集合,不適合做動態Bean的宣告。 而BeanFactoryPostProcessor
是一個Bean初始化之前和之後被呼叫的處理器。
而在我們的FeignClient宣告中,並沒有Spring相關的註解,所以自然也不會被Spring容器載入和觸發。
那麼@FeignClient
是在哪裡被宣告掃描的呢?
在整合FeignClient時,我們在SpringBoot的main方法中,宣告瞭一個註解@EnableFeignClients(basePackages = "com.gupaoedu.ms.api")
。這個註解需要填寫一個指定的包名。
嗯,看到這裡,基本上就能猜測出,這個註解必然和@FeignClient
註解的解析有莫大的關係。
下面這段程式碼是@EnableFeignClients
註解的宣告,果然看到了一個很熟悉的面孔FeignClientsRegistrar
。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}
FeignClientsRegistrar
FeignClientRegistrar,主要功能就是針對宣告@FeignClient
註解的介面進行掃描和注入到IOC容器。
class FeignClientsRegistrar
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
}
果然,這個類實現了ImportBeanDefinitionRegistrar
介面
public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
this.registerBeanDefinitions(importingClassMetadata, registry);
}
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
}
這個介面有兩個過載的方法,用來實現Bean的宣告和註冊。
簡單給大家演示一下ImportBeanDefinitionRegistrar的作用。
在
gpmall-portal
這個專案的com.gupaoedu
目錄下,分別建立
- HelloService.java
- GpImportBeanDefinitionRegistrar.java
- EnableGpRegistrar.java
- TestMain
- 定義一個需要被裝載到IOC容器中的類HelloService
public class HelloService {
}
- 定義一個Registrar的實現,定義一個bean,裝載到IOC容器
public class GpImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
BeanDefinition beanDefinition=new GenericBeanDefinition();
beanDefinition.setBeanClassName(HelloService.class.getName());
registry.registerBeanDefinition("helloService",beanDefinition);
}
}
- 定義一個註解類
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(GpImportBeanDefinitionRegistrar.class)
public @interface EnableGpRegistrar {
}
- 寫一個測試類
@Configuration
@EnableGpRegistrar
public class TestMain {
public static void main(String[] args) {
ApplicationContext applicationContext=new AnnotationConfigApplicationContext(TestMain.class);
System.out.println(applicationContext.getBean(HelloService.class));
}
}
- 通過結果演示可以發現,
HelloService
這個bean 已經裝載到了IOC容器。
這就是動態裝載的功能實現,它相比於@Configuration配置注入,會多了很多的靈活性。 ok,再回到FeignClient的解析中來。
FeignClientsRegistrar.registerBeanDefinitions
registerDefaultConfiguration
方法內部從SpringBoot
啟動類上檢查是否有@EnableFeignClients
, 有該註解的話, 則完成Feign
框架相關的一些配置內容註冊。registerFeignClients
方法內部從classpath
中, 掃描獲得@FeignClient
修飾的類, 將類的內容解析為BeanDefinition
, 最終通過呼叫 Spring 框架中的BeanDefinitionReaderUtils.resgisterBeanDefinition
將解析處理過的FeignClient BeanDeifinition
新增到spring
容器中
//BeanDefinitionReaderUtils.resgisterBeanDefinition
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//註冊@EnableFeignClients中定義defaultConfiguration屬性下的類,包裝成FeignClientSpecification,註冊到Spring容器。
//在@FeignClient中有一個屬性:configuration,這個屬性是表示各個FeignClient自定義的配置類,後面也會通過呼叫registerClientConfiguration方法來註冊成FeignClientSpecification到容器。
//所以,這裡可以完全理解在@EnableFeignClients中配置的是做為兜底的配置,在各個@FeignClient配置的就是自定義的情況。
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
這裡面需要重點分析的就是registerFeignClients
方法,這個方法主要是掃描類路徑下所有的@FeignClient
註解,然後進行動態Bean的注入。它最終會呼叫registerFeignClient
方法。
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
registerFeignClient(registry, annotationMetadata, attributes);
}
FeignClientsRegistrar.registerFeignClients
registerFeignClients方法的定義如下。
//# FeignClientsRegistrar.registerFeignClients
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
//獲取@EnableFeignClients註解的後設資料
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
//獲取@EnableFeignClients註解中的clients屬性,可以配置@FeignClient宣告的類,如果配置了,則需要掃描並載入。
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
//預設TypeFilter生效,這種模式會查詢出許多不符合你要求的class名
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class)); //新增包含過濾的屬性@FeignClient。
Set<String> basePackages = getBasePackages(metadata); //從@EnableFeignClients註解中獲取basePackages配置。
for (String basePackage : basePackages) {
//scanner.findCandidateComponents(basePackage) 掃描basePackage下的@FeignClient註解宣告的介面
candidateComponents.addAll(scanner.findCandidateComponents(basePackage)); //新增到candidateComponents,也就是候選容器中。
}
}
else {//如果配置了clients,則需要新增到candidateComponets中。
for (Class<?> clazz : clients) {
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
}
//遍歷候選容器列表。
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) { //如果屬於AnnotatedBeanDefinition例項型別
// verify annotated class is an interface
//得到@FeignClient註解的beanDefinition
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); //獲取這個bean的註解後設資料
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
//獲取後設資料屬性
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
//獲取@FeignClient中配置的服務名稱。
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
FeignClient Bean的註冊
這個方法就是把FeignClient介面註冊到Spring IOC容器,
FeignClient是一個介面,那麼這裡注入到IOC容器中的物件是什麼呢?
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName(); //獲取FeignClient介面的類全路徑
Class clazz = ClassUtils.resolveClassName(className, null); //載入這個介面,得到Class例項
//構建ConfigurableBeanFactory,提供BeanFactory的配置能力
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
? (ConfigurableBeanFactory) registry : null;
//獲取contextId
String contextId = getContextId(beanFactory, attributes);
String name = getName(attributes);
//構建一個FeignClient FactoryBean,這個是工廠Bean。
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
//設定工廠Bean的相關屬性
factoryBean.setBeanFactory(beanFactory);
factoryBean.setName(name);
factoryBean.setContextId(contextId);
factoryBean.setType(clazz);
//BeanDefinitionBuilder是用來構建BeanDefinition物件的建造器
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(clazz, () -> {
//把@FeignClient註解配置中的屬性設定到FactoryBean中。
factoryBean.setUrl(getUrl(beanFactory, attributes));
factoryBean.setPath(getPath(beanFactory, attributes));
factoryBean.setDecode404(Boolean
.parseBoolean(String.valueOf(attributes.get("decode404"))));
Object fallback = attributes.get("fallback");
if (fallback != null) {
factoryBean.setFallback(fallback instanceof Class
? (Class<?>) fallback
: ClassUtils.resolveClassName(fallback.toString(), null));
}
Object fallbackFactory = attributes.get("fallbackFactory");
if (fallbackFactory != null) {
factoryBean.setFallbackFactory(fallbackFactory instanceof Class
? (Class<?>) fallbackFactory
: ClassUtils.resolveClassName(fallbackFactory.toString(),
null));
}
return factoryBean.getObject(); //factoryBean.getObject() ,基於工廠bean創造一個bean例項。
});
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); //設定注入模式,採用型別注入
definition.setLazyInit(true); //設定延遲華
validate(attributes);
//從BeanDefinitionBuilder中構建一個BeanDefinition,它用來描述一個bean的例項定義。
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
// has a default, won't be null
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);
String[] qualifiers = getQualifiers(attributes);
if (ObjectUtils.isEmpty(qualifiers)) {
qualifiers = new String[] { contextId + "FeignClient" };
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
qualifiers);
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); //把BeanDefinition的這個bean定義註冊到IOC容器。
}
綜上程式碼分析,其實實現邏輯很簡單。
- 建立一個BeanDefinitionBuilder。
- 建立一個工廠Bean,並把從@FeignClient註解中解析的屬性設定到這個FactoryBean中
- 呼叫registerBeanDefinition註冊到IOC容器中
關於FactoryBean
在上述的邏輯中,我們重點需要關注的是FactoryBean,這個大家可能接觸得會比較少一點。
首先,需要注意的是FactoryBean和BeanFactory是不一樣的,FactoryBean是一個工廠Bean,可以生成某一個型別Bean例項,它最大的一個作用是:可以讓我們自定義Bean的建立過程。
而,BeanFactory是Spring容器中的一個基本類也是很重要的一個類,在BeanFactory中可以建立和管理Spring容器中的Bean。
下面這段程式碼是FactoryBean介面的定義。
public interface FactoryBean<T> {
String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
@Nullable
T getObject() throws Exception;
@Nullable
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}
從上面的程式碼中我們發現在FactoryBean中定義了一個Spring Bean的很重要的三個特性:是否單例、Bean型別、Bean例項,這也應該是我們關於Spring中的一個Bean最直觀的感受。
FactoryBean自定義使用
下面我們來模擬一下@FeignClient解析以及工廠Bean的構建過程。
- 先定義一個介面,這個介面可以類比為我們上面描述的FeignClient.
public interface IHelloService {
String say();
}
- 接著,定義一個工廠Bean,這個工廠Bean中主要是針對IHelloService生成動態代理。
public class DefineFactoryBean implements FactoryBean<IHelloService> {
@Override
public IHelloService getObject() {
IHelloService helloService=(IHelloService) Proxy.newProxyInstance(IHelloService.class.getClassLoader(),
new Class<?>[]{IHelloService.class}, (proxy, method, args) -> {
System.out.println("begin execute");
return "Hello FactoryBean";
});
return helloService;
}
@Override
public Class<?> getObjectType() {
return IHelloService.class;
}
}
- 通過實現ImportBeanDefinitionRegistrar這個介面,來動態注入Bean例項
public class GpImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
DefineFactoryBean factoryBean=new DefineFactoryBean();
BeanDefinitionBuilder definition= BeanDefinitionBuilder.genericBeanDefinition(
IHelloService.class,()-> factoryBean.getObject());
BeanDefinition beanDefinition=definition.getBeanDefinition();
registry.registerBeanDefinition("helloService",beanDefinition);
}
}
- 宣告一個註解,用來表示動態bean的注入匯入。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(GpImportBeanDefinitionRegistrar.class)
public @interface EnableGpRegistrar {
}
- 編寫測試類,測試IHelloService這個介面的動態注入
@Configuration
@EnableGpRegistrar
public class TestMain {
public static void main(String[] args) {
ApplicationContext applicationContext=new AnnotationConfigApplicationContext(TestMain.class);
IHelloService is=applicationContext.getBean(IHelloService.class);
System.out.println(is.say());
}
}
- 執行上述的測試方法,可以看到IHelloService這個介面,被動態代理並且注入到了IOC容器。
FeignClientFactoryBean
由上述案例可知,Spring IOC容器在注入FactoryBean時,會呼叫FactoryBean的getObject()方法獲得bean的例項。因此我們可以按照這個思路去分析FeignClientFactoryBean
@Override
public Object getObject() {
return getTarget();
}
構建物件Bean的實現程式碼如下,這個程式碼的實現較長,我們分為幾個步驟來看
Feign上下文的構建
先來看上下文的構建邏輯,程式碼部分如下。
<T> T getTarget() {
FeignContext context = beanFactory != null
? beanFactory.getBean(FeignContext.class)
: applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
}
兩個關鍵的物件說明:
- FeignContext是全域性唯一的上下文,它繼承了NamedContextFactory,它是用來來統一維護feign中各個feign客戶端相互隔離的上下文,FeignContext註冊到容器是在FeignAutoConfiguration上完成的,程式碼如下!
//FeignAutoConfiguration.java
@Autowired(required = false)
private List<FeignClientSpecification> configurations = new ArrayList<>();
@Bean
public FeignContext feignContext() {
FeignContext context = new FeignContext();
context.setConfigurations(this.configurations);
return context;
}
在初始化FeignContext時,會把configurations
放入到FeignContext中。configuration
表示當前被掃描到的所有@FeignClient。
- Feign.Builder用來構建Feign物件,基於builder實現上下文資訊的構建,程式碼如下。
protected Feign.Builder feign(FeignContext context) {
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(type);
// @formatter:off
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)); //contract協議,用來實現模版解析(後面再詳細分析)
// @formatter:on
configureFeign(context, builder);
applyBuildCustomizers(context, builder);
return builder;
}
從程式碼中可以看到,feign
方法,主要是針對不同的服務提供者生成Feign的上下文資訊,比如logger
、encoder
、decoder
等。因此,從這個分析過程中,我們不難猜測到它的原理結構,如下圖所示
父子容器隔離的實現方式如下,當呼叫get方法時,會從context
中去獲取指定type的例項物件。
//FeignContext.java
protected <T> T get(FeignContext context, Class<T> type) {
T instance = context.getInstance(contextId, type);
if (instance == null) {
throw new IllegalStateException(
"No bean found of type " + type + " for " + contextId);
}
return instance;
}
接著,呼叫NamedContextFactory
中的getInstance
方法。
//NamedContextFactory.java
public <T> T getInstance(String name, Class<T> type) {
//根據`name`獲取容器上下文
AnnotationConfigApplicationContext context = this.getContext(name);
try {
//再從容器上下文中獲取指定型別的bean。
return context.getBean(type);
} catch (NoSuchBeanDefinitionException var5) {
return null;
}
}
getContext
方法根據name
從contexts
容器中獲得上下文物件,如果沒有,則呼叫createContext
建立。
protected AnnotationConfigApplicationContext getContext(String name) {
if (!this.contexts.containsKey(name)) {
synchronized(this.contexts) {
if (!this.contexts.containsKey(name)) {
this.contexts.put(name, this.createContext(name));
}
}
}
return (AnnotationConfigApplicationContext)this.contexts.get(name);
}
生成動態代理
第二個部分,如果@FeignClient註解中,沒有配置url
,也就是不走絕對請求路徑,則執行下面這段邏輯。
由於我們在@FeignClient註解中使用的是name
,所以需要執行負載均衡策略的分支邏輯。
<T> T getTarget() {
//省略.....
if (!StringUtils.hasText(url)) { //是@FeignClient中的一個屬性,可以定義請求的絕對URL
if (LOG.isInfoEnabled()) {
LOG.info("For '" + name
+ "' URL not provided. Will try picking an instance via load-balancing.");
}
if (!name.startsWith("http")) {
url = "http://" + name;
}
else {
url = name;
}
url += cleanPath();
//
return (T) loadBalance(builder, context,
new HardCodedTarget<>(type, name, url));
}
//省略....
}
loadBalance方法的程式碼實現如下,其中Client
是Spring Boot自動裝配的時候實現的,如果替換了其他的http協議框架,則client則對應為配置的協議api。
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
//Feign傳送請求以及接受響應的http client,預設是Client.Default的實現,可以修改成OkHttp、HttpClient等。
Client client = getOptional(context, Client.class);
if (client != null) {
builder.client(client); //針對當前Feign客戶端,設定網路通訊的client
//targeter表示HystrixTarger例項,因為Feign可以整合Hystrix實現熔斷,所以這裡會一層包裝。
Targeter targeter = get(context, Targeter.class);
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 or spring-cloud-starter-loadbalancer?");
}
HystrixTarget.target
程式碼如下,我們沒有整合Hystrix,因此不會觸發Hystrix相關的處理邏輯。
//HystrixTarget.java
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
FeignContext context, Target.HardCodedTarget<T> target) {
if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) { //沒有配置Hystrix,則走這部分邏輯
return feign.target(target);
}
//省略....
return feign.target(target);
}
進入到Feign.target
方法,程式碼如下。
//Feign.java
public <T> T target(Target<T> target) {
return this.build().newInstance(target); //target.HardCodedTarget
}
public Feign build() {
//這裡會構建一個LoadBalanceClient
Client client = (Client)Capability.enrich(this.client, this.capabilities);
Retryer retryer = (Retryer)Capability.enrich(this.retryer, this.capabilities);
List<RequestInterceptor> requestInterceptors = (List)this.requestInterceptors.stream().map((ri) -> {
return (RequestInterceptor)Capability.enrich(ri, this.capabilities);
}).collect(Collectors.toList());
//OpenFeign Log配置
Logger logger = (Logger)Capability.enrich(this.logger, this.capabilities);
//模版解析協議(這個介面非常重要:它決定了哪些註解可以標註在介面/介面方法上是有效的,並且提取出有效的資訊,組裝成為MethodMetadata元資訊。)
Contract contract = (Contract)Capability.enrich(this.contract, this.capabilities);
//封裝Request請求的 連線超時=預設10s ,讀取超時=預設60
Options options = (Options)Capability.enrich(this.options, this.capabilities);
//編碼器
Encoder encoder = (Encoder)Capability.enrich(this.encoder, this.capabilities);
//解碼器
Decoder decoder = (Decoder)Capability.enrich(this.decoder, this.capabilities);
InvocationHandlerFactory invocationHandlerFactory = (InvocationHandlerFactory)Capability.enrich(this.invocationHandlerFactory, this.capabilities);
QueryMapEncoder queryMapEncoder = (QueryMapEncoder)Capability.enrich(this.queryMapEncoder, this.capabilities);
//synchronousMethodHandlerFactory, 同步方法呼叫處理器(很重要,後續要用到)
Factory synchronousMethodHandlerFactory = new Factory(client, retryer, requestInterceptors, logger, this.logLevel, this.decode404, this.closeAfterDecode, this.propagationPolicy, this.forceDecoding);
//ParseHandlersByName的作用就是我們傳入Target(封裝了我們的模擬介面,要訪問的域名),返回這個介面下的各個方法,對應的執行HTTP請求需要的一系列資訊
ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, this.errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
build
方法中,返回了一個ReflectiveFeign
的例項物件,先來看ReflectiveFeign
中的newInstance
方法。
public <T> T newInstance(Target<T> target) { //修飾了@FeignClient註解的介面方法封裝成方法處理器,把指定的target進行解析,得到需要處理的方法集合。 Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); //定義一個用來儲存需要處理的方法的集合 Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); //JDK8以後,介面允許預設方法實現,這裡是對預設方法進行封裝處理。 List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); //遍歷@FeignClient介面的所有方法 for (Method method : target.type().getMethods()) { //如果是Object中的方法,則直接跳過 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 {//否則,新增MethodHandler(SynchronousMethodHandler)。 methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } //建立動態代理類。 InvocationHandler handler = factory.create(target, methodToHandler); T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[] {target.type()}, handler); for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy;}
上述程式碼,其實也不難理解。
解析@FeignClient介面宣告的方法,根據不同方法繫結不同的處理器。
- 預設方法,繫結DefaultMethodHandler
- 遠端方法,繫結SynchronousMethodHandler
- 使用JDK提供的Proxy建立動態代理
MethodHandler,會把方法引數、方法返回值、引數集合、請求型別、請求路徑進行解析儲存,如下圖所示。
FeignClient介面解析
介面解析也是Feign很重要的一個邏輯,它能把介面宣告的屬性轉化為HTTP通訊的協議引數。
執行邏輯RerlectiveFeign.newInstance
public <T> T newInstance(Target<T> target) { Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); //here}
targetToHandlersByName.apply(target);會解析介面方法上的註解,從而解析出方法粒度的特定的配置資訊,然後生產一個SynchronousMethodHandler
然後需要維護一個<method,MethodHandler>的map,放入InvocationHandler的實現FeignInvocationHandler中。
public Map<String, MethodHandler> apply(Target target) { List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type()); Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>(); for (MethodMetadata md : metadata) { BuildTemplateByResolvingArgs buildTemplate; if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else if (md.bodyIndex() != null) { buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else { buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target); } if (md.isIgnored()) { result.put(md.configKey(), args -> { throw new IllegalStateException(md.configKey() + " is not a method handled by feign"); }); } else { result.put(md.configKey(), factory.create(target, md, buildTemplate, options, decoder, errorDecoder)); } } return result;}
為了更好的理解上述邏輯,我們可以藉助下面這個圖來理解!
階段性小結
通過上述過程分析,被宣告為@FeignClient註解的類,在被注入時,最終會生成一個動態代理物件FeignInvocationHandler。
當觸發方法呼叫時,會被FeignInvocationHandler#invoke攔截,FeignClientFactoryBean在例項化過程中所做的事情如下圖所示。
總結來說就幾個點:
- 解析Feign的上下文配置,針對當前的服務例項構建容器上下文並返回Feign物件
- Feign根據上下圍配置把 log、encode、decoder、等配置項設定到Feign物件中
- 對目標服務,使用LoadBalance以及Hystrix進行包裝
- 通過Contract協議,把FeignClient介面的宣告,解析成MethodHandler
- 遍歷MethodHandler列表,針對需要遠端通訊的方法,設定
SynchronousMethodHandler
處理器,用來實現同步遠端呼叫。 - 使用JDK中的動態代理機制構建動態代理物件。
遠端通訊實現
在Spring啟動過程中,把一切的準備工作準備就緒後,就開始執行遠端呼叫。
在前面的分析中,我們知道OpenFeign最終返回的是一個#ReflectiveFeign.FeignInvocationHandler的物件。
那麼當客戶端發起請求時,會進入到FeignInvocationHandler.invoke方法中,這個大家都知道,它是一個動態代理的實現。
//FeignInvocationHandler.java@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("equals".equals(method.getName())) { try { Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; return equals(otherHandler); } catch (IllegalArgumentException e) { return false; } } else if ("hashCode".equals(method.getName())) { return hashCode(); } else if ("toString".equals(method.getName())) { return toString(); } return dispatch.get(method).invoke(args);}
接著,在invoke方法中,會呼叫this.dispatch.get(method)).invoke(args)
。this.dispatch.get(method)
會返回一個SynchronousMethodHandler,進行攔截處理。
this.dispatch,其實就是在初始化過程中建立的,private final Map<Method, MethodHandler> dispatch;
例項。
SynchronousMethodHandler.invoke
這個方法會根據引數生成完成的RequestTemplate物件,這個物件是Http請求的模版,程式碼如下,程式碼的實現如下:
@Override
public Object invoke(Object[] argv) throws Throwable { //argv,表示呼叫方法傳遞的引數。
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv); //獲取配置項,連線超時時間、遠端通訊資料獲取超時時間
Retryer retryer = this.retryer.clone(); //獲取重試策略
while (true) {
try {
return executeAndDecode(template, options);
} catch (RetryableException e) {
try {
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
上述程式碼的執行流程中,需要先構造一個Request物件,然後呼叫client.execute方法執行網路通訊請求,整體實現流程如下。
executeAndDecode
經過上述的程式碼,我們已經將RequestTemplate拼裝完成,上面的程式碼中有一個executeAndDecode()
方法,該方法通過RequestTemplate生成Request請求物件,然後利用Http Client獲取response,來獲取響應資訊。
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = targetRequest(template); //把template轉化為請求報文
if (logLevel != Logger.Level.NONE) { //設定日誌level
logger.logRequest(metadata.configKey(), logLevel, request);
}
Response response;
long start = System.nanoTime();
try {
//發起請求,此時client是LoadBalanceFeignClient,需要先對服務名稱進行解析負載
response = client.execute(request, options); // ensure the request is set. TODO: remove in Feign 12 //獲取返回結果
response = response.toBuilder().request(request).requestTemplate(template).build();
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
}
throw errorExecuting(request, e);
}
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
if (decoder != null) //如果設定瞭解碼器,這需要對響應資料做解碼
return decoder.decode(response, metadata.returnType());
CompletableFuture<Object> resultFuture = new CompletableFuture<>();
asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response, metadata.returnType(), elapsedTime);
try {
if (!resultFuture.isDone())
throw new IllegalStateException("Response handling not done");
return resultFuture.join();
} catch (CompletionException e) {
Throwable cause = e.getCause();
if (cause != null)
throw cause;
throw e;
}
}
LoadBalanceClient
@Overridepublic
Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url()); //獲取請求uri,此時的地址還未被解析。
String clientName = asUri.getHost(); //獲取host,實際上就是服務名稱
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(this.delegate, request, uriWithoutHost); //載入客戶端的配置資訊
IClientConfig requestConfig = getClientConfig(options, clientName); //建立負載均衡客戶端(FeignLoadBalancer),執行請求
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
} catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
從上面的程式碼可以看到,lbClient(clientName) 建立了一個負載均衡的客戶端,它實際上就是生成的如下所述的類:
public class FeignLoadBalancer extends AbstractLoadBalancerAwareClient<FeignLoadBalancer.RibbonRequest, FeignLoadBalancer.RibbonResponse> {
整體總結
OpenFeign的整體通訊原理解析,如下圖所示。
版權宣告:本部落格所有文章除特別宣告外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自Mic帶你學架構
!
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力。歡迎關注同名微信公眾號獲取更多技術乾貨!