开始

初始化Feign客户端当然是整个过程中的核心部分,毕竟初始化完毕就等着调用了,初始化时候准备的什么,流程就走什么。

内容

从上一篇中,我们已经知道,对于扫描到的每一个有@FeignClient,都会组装一个FactoryBean即FeignClientFactoryBean注册到spring容器中,如此在spring 容器初始化的时候,创建FeignClient的Bean时都会调用FeignClientFactoryBean的getObject方法。

FeignClientFactoryBean是Spring的FactoryBean,在Spring的世界里可以通过xml定义bean,也可以通过@Bean注解的方法组装bean,但如果我们要的bean产生过程比较复杂,使用配置或单纯的new不好解决,这时候使用FactoryBean就比较合适了,在Spring中想要找某个类型的bean时,如果是FactoryBean定义的,就会调用它的getObject获取这个bean。

FeignClientFactoryBean的getObject方法:

public Object getObject() throws Exception {
FeignContext context = applicationContext.getBean(FeignContext.class);
// 构建Feign.Builder
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(this.url)) {
String url;
if (!this.name.startsWith("http")) {
url = "http://" + this.name;
}
else {
url = this.name;
}
url += cleanPath();
return loadBalance(builder, context, new HardCodedTarget<>(this.type,
this.name, 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) {
// not lod 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 targeter.target(this, builder, context, new HardCodedTarget<>(
this.type, this.name, url));
}

构建feign.builder时会向FeignContext获取配置的Encoder,Decoder等各种信息。FeignContext在上篇中已经提到会为每个Feign客户端分配了一个容器,它们的父容器就是spring容器,凡是在子容器中找不到的对象,再从父容器中找。

我们可以在Feign.Builder中看全部的可配置的属性,会发现有些信息在feignclient注解上有可以直接通过注解属性字段进行设置,比如ecode404,而有些属性是只能通过注解属性configuration配置configuration类来注入配置信息,比如:Retryer。另外除了通过在注解属性上进行配置信息外,也可以通过FeignClientProperties来配置这些信息。

在configureFeign方法中看到可以通通过defaultToProperties属性来控制两者的优先级,默认为true,比如defaultToProperties设置为false时,则会先向Feign.Builder放配置文件配置的信息,然后再放注解上配置的,后放的当然可以覆盖先放的,所以注解配置的优先级就算高的(除了RequestInterceptor,这个是没有什么优先级的,是add上去的)。

protected Feign.Builder feign(FeignContext context) {
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(this.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));
// @formatter:on
configureFeign(context, builder);
return builder;
}
protected void configureFeign(FeignContext context, Feign.Builder builder) {
FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
if (properties != null) {
if (properties.isDefaultToProperties()) {
configureUsingConfiguration(context, builder);
configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
configureUsingProperties(properties.getConfig().get(this.name), builder);
} else {
configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
configureUsingProperties(properties.getConfig().get(this.name), builder);
configureUsingConfiguration(context, builder);
}
} else {
configureUsingConfiguration(context, builder);
}
}
protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) {
Logger.Level level = getOptional(context, Logger.Level.class);
if (level != null) {
builder.logLevel(level);
}
Retryer retryer = getOptional(context, Retryer.class);
if (retryer != null) {
builder.retryer(retryer);
}
ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class);
if (errorDecoder != null) {
builder.errorDecoder(errorDecoder);
}
Request.Options options = getOptional(context, Request.Options.class);
if (options != null) {
builder.options(options);
}
Map<String, RequestInterceptor> requestInterceptors = context.getInstances(
this.name, RequestInterceptor.class);
if (requestInterceptors != null) {
builder.requestInterceptors(requestInterceptors.values());
}
if (decode404) {
builder.decode404();
}
}

无论是通过配置文件还是注解属性,能够控制的都是一个feignclient整体的配置。而我们在写feign接口的方法是,还需要定义这个接口方法的http描述信息,比如请求路径,请求方式,参数定义等等。也就是说,对于一个单独的请求来说,完整配置的粒度要到feign接口里的方法级别。

在getObject方法的最后会调用Targeter.target方法来组装对象,Targeter是可以被扩展的,先不展开了,在默认的实现中会调用前面组装好的Feign.Builder的target方法:

class DefaultTargeter implements Targeter {
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target) {
return feign.target(target);
}
}

Feign.Builder的target方法会触发建造者的构建操作:

public <T> T target(Target<T> target) {
return build().newInstance(target);
}
public Feign build() {
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
}

可以想象,我们只是定义了接口,通过接口的方法我们需要达成一个请求应用的操作,肯定是需要产生一个类来实现这些接口的,这里使用动态代理非常合适,那么事情就变得简单了,通过jdk自带的动态代理方式为接口产生一个代理实现类。这个实现思路可以借鉴到其他的场景,比如比较熟悉的mybatis定义的mapper接口,也是不需要实现的,实现的方式和这里是一模一样。

这个实现从ReflectiveFeign的newInstance(target)方法开始:

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 handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
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;
}

从实现的代码中可以看到熟悉的Proxy.newProxyInstance方法产生代理类。而这里需要对每个定义的接口方法进行特定的处理实现,所以这里会出现一个MethodHandler的概念,就是对应方法级别的InvocationHandler。

for循环是在过滤不必要的方法,有意思的一个地方:Util.isDefault(method)这个方法展开看一下:

/**
* Identifies a method as a default instance method.
*/
public static boolean isDefault(Method method) {
// Default methods are public non-abstract, non-synthetic, and non-static instance methods
// declared in an interface.
// method.isDefault() is not sufficient for our usage as it does not check
// for synthetic methods. As a result, it picks up overridden methods as well as actual default methods.
final int SYNTHETIC = 0x00001000;
return ((method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC | SYNTHETIC)) ==
Modifier.PUBLIC) && method.getDeclaringClass().isInterface();
}

注释说,没有使用Method.isDefault()是因为嫌弃它不够全面的识别,说应该过滤掉合成(synthetic)方法,synthetic methods是编译时自动加入的方法。

另外,Map<String, MethodHandler>的key是用Feign.configKey(target.type(), method)生成的,我觉得是可以通用:

public static String configKey(Class targetType, Method method) {
StringBuilder builder = new StringBuilder();
builder.append(targetType.getSimpleName());
builder.append('#').append(method.getName()).append('(');
for (Type param : method.getGenericParameterTypes()) {
param = Types.resolve(targetType, targetType, param);
builder.append(Types.getRawType(param).getSimpleName()).append(',');
}
if (method.getParameterTypes().length > 0) {
builder.deleteCharAt(builder.length() - 1);
}
return builder.append(')').toString();
}

targetToHandlersByName.apply(target);会解析接口方法上的注解,从而解析出方法粒度的特定的配置信息,然后生产一个SynchronousMethodHandler

然后需要维护一个<method,MethodHandler>的map,放入InvocationHandler的实现FeignInvocationHandler中。

在FeignInvocationHandler中的的invoke方法实现:

public 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);
}

当代理类接到执行请求时, 通过一个map分发给对应的MethodHandler执行,如此就实现了针对每个方法的个性化代理实现。

所以,结构就是一个InvocationHandler对应多个MethodHandler:

MethodHandler的实现这里是使用SynchronousMethodHandler,它实现的invoke方法如下:

public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template);
} catch (RetryableException e) {
retryer.continueOrPropagate(e);
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}

到这里就会创建http请求模版,这部分后续再深入。

结束

可以看到产生的FeignClient的代理对象,代理了接口方法,实际会生成一个http请求模版,进行请求操作。

回到前面触发的地方是spring调用FeignClientFactoryBean的getObject方法,所以产生的这个FeignClient的代理对象会在spring容器中,我们直接可以从spring容器中拿来使用。

Feign源码解析系列-核心初始化的更多相关文章

  1. Feign源码解析系列-注册套路

    感谢不知名朋友的打赏,感谢你的支持! 开始 在追寻Feign源码的过程中发现了一些套路,既然是套路,就可以举一反三,所以值得关注. 这篇会详细解析Feign Client配置和初始化的方式,这些方式大 ...

  2. Feign源码解析系列-那些注解们

    开始 Feign在Spring Cloud体系中被整合进来作为web service客户端,使用HTTP请求远程服务时能就像调用本地方法,可见在未来一段时间内,大多数Spring Cloud架构的微服 ...

  3. Feign源码解析系列-最佳实践

    前几篇准备写完feign的源码,这篇直接给出Feign的最佳实践,考虑到目前网上还没有一个比较好的实践解释,对于新使用spring cloud的同学会对微服务之间的依赖产生一些迷惑,也会走一些弯路.这 ...

  4. 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新

    [原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...

  5. 【安卓网络请求开源框架Volley源码解析系列】定制自己的Request请求及Volley框架源码剖析

    通过前面的学习我们已经掌握了Volley的基本用法,没看过的建议大家先去阅读我的博文[安卓网络请求开源框架Volley源码解析系列]初识Volley及其基本用法.如StringRequest用来请求一 ...

  6. Feign源码解析

    1. Feign源码解析 1.1. 启动过程 1.1.1. 流程图 1.1.2. 解释说明 Feign解析过程依赖Spring的初始化,它通过实现ImportBeanDefinitionRegistr ...

  7. TiKV 源码解析系列文章(三)Prometheus(上)

    本文为 TiKV 源码解析系列的第三篇,继续为大家介绍 TiKV 依赖的周边库 rust-prometheus,本篇主要介绍基础知识以及最基本的几个指标的内部工作机制,下篇会介绍一些高级功能的实现原理 ...

  8. SpringBoot源码解析系列文章汇总

    相信我,你会收藏这篇文章的 本篇文章是这段时间撸出来的SpringBoot源码解析系列文章的汇总,当你使用SpringBoot不仅仅满足于基本使用时.或者出去面试被面试官虐了时.或者说想要深入了解一下 ...

  9. Spring源码解析系列汇总

    相信我,你会收藏这篇文章的 本篇文章是这段时间撸出来的Spring源码解析系列文章的汇总,总共包含以下专题.喜欢的同学可以收藏起来以备不时之需 SpringIOC源码解析(上) 本篇文章搭建了IOC源 ...

随机推荐

  1. PHP左侧菜单栏的管理与实现

    以thinkPHP5.0为例 后台config.php文件里配置 //配置文件设置菜单内容属性 'menu' => [ [ 'name' => '菜单栏1', 'url' => '/ ...

  2. SSM框架:解决后台传数据到前台中文乱码问题,使用@ResponseBody返回json 中文乱码

    解决方法一:@RequestMapping(value="/getphone",produces = "text/plain;charset=utf-8") / ...

  3. PhpStorm代码提示(省电模式)的设置与使用

    PhpStorm中有个,Power Save Mode(省电模式),开启则代码不能自动提示,关闭则可以. 开启/关闭: 1.点击“File”菜单,最下面,Power Save Mode,勾选或取消: ...

  4. UOJ#129. 【NOI2015】寿司晚宴 动态规划

    原文链接www.cnblogs.com/zhouzhendong/p/UOJ129.html 题解 考虑把大于等于 $\sqrt n$ 的质数和小于 $\sqrt  n$ 的分开考虑: 1. 小于等于 ...

  5. linux - word frequency

    linux  输出某个文件的单词出现频率 解决方式 cat words.txt |awk '{for(i=1;i<=NF;i++) print $i;}'|sort|uniq -c|sort - ...

  6. Android进阶:七、Retrofit2.0原理解析之最简流程【下】

    紧接上文Android进阶:七.Retrofit2.0原理解析之最简流程[上] 一.请求参数整理 我们定义的接口已经被实现,但是我们还是不知道我们注解的请求方式,参数类型等是如何发起网络请求的呢? 这 ...

  7. C++ 初读迭代器

    迭代器 这是个啥? string对象或vector对象可以通过下标访问每一个元素,迭代器也具有同样的效果.那又有什么不同呢?事实上并不是所有的容器到可以使用下标访问每一个元素,即在容器上迭代器更具普适 ...

  8. 给Ionic写一个cordova(PhoneGap)插件

    给Ionic写一个cordova(PhoneGap)插件 之前由javaWeb转html5开发,由于面临新技术,遂在适应的过程中极为挣扎,不过还好~,这个过程也极为短暂:现如今面临一些较为复杂的需求还 ...

  9. 阿里云消息队列(MQ)服务

    A.首先在阿里云上申请消息队列MQ服务: B.然后创建一个Topic(主题,一级主题):然后创建生产者与消费者: C.不过此时还没有结束 ,还需要创建一个AccessKey和AccessSecret( ...

  10. JS与IOS、Android的交互

    一.JS与Android 放在了assets文件夹下了(注意若使用的是AS这个IDE,assets文件夹应放在src/main目录下) <!DOCTYPE html> <html&g ...