SpringCloud源码探析(四)-OpenFeign使用及其原理
1.概述
在SpringCloud中,服务之间的调用方式可以通过ResTemplate进行调用,也可以通过Feign调用。ResTemplate的缺陷在于需要指定请求url,存在硬编码问题,导致代码难以复用和修改。而Feign调用就相对比较优雅,只需要配置服务名称即可。本文将介绍OpenFeign的使用及其原理。
2.OpenFeign使用及原理
2.1 SpringCloud集成OpenFeign
2.1.1 引入依赖
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId><version>2.2.1.RELEASE</version></dependency>
openfeign的版本与springcloud版本需要对应,我这里springcloud的版本是Hoxton.SR3。
2.1.2 编写Feign接口
@FeignClient(name = "userservice")
public interface FeignClientUser {@GetMapping("/user/findOrderByUserId")String getUserById();}
2.1.3 启动类添加注解
@Slf4j
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {public static void main(String[] args) {log.info("这是新的OrderApplication");SpringApplication.run(OrderApplication.class, args);}
}
启动类上添加@EnableFeignClients接口,进行Feign接口扫描。
2.2 Feign日志配置
feign的日志配置主要有两种方式,一种是通过在配置文件配置的方式(这种方式通常是全局feign日志配置);另一种是通过注入Bean配置,可以实现不同类feign接口不同配置。
2.2.1 配置文件
配置文件添加如下配置:
feign.client.config.default.logger-level=FULL
2.2.2 注入Bean配置
//Feign配置,Bean注入到容器中
public class FeignClientConfiguration {@Beanpublic Logger.Level feignLogLevel() {return Logger.Level.FULL;}
}//Feign接口上指定配置
@FeignClient(name = "userservice", configuration = FeignClientConfiguration.class)
public interface FeignClientUser {@GetMapping("/user/findOrderByUserId")String getUserById();}
如果是引用外部Feign包,需要添加扫描包路径,如下:
@EnableFeignClients(basePackages = {"com.eckey.lab"})
2.3 Feign自定义配置
2.4 OpenFeign使用原理
2.4.1 Feign调用流程图
feign调用的流程图如下所示:
1.开启Feign注解: 核心注解@FeignClient和@EnableFeignClients,开启Feign接口声明和Feign接口扫描;
2.服务器启动扫描注解,创建JDK注解: 服务启动时进行扫描,通过FeignInvocationHandler为每个远程接口创建JDK Proxy代理对象,并将这些对象注入Spring容器中;
3.找到MethodHandler方法处理器: FeignInvocationHandler根据要调用的方法找到对应的MethodHandler方法处理器;
4.构造Request对象并调用Encoder进行编码: MethodHandler方法处理器通过RequestTemplate构造参数和url,封装Request对象,并调用Encoder进行编码;
5.发送Request请求获取Response对象并进行解码: Client接口根据选择的Http框架,发送Request对象并接收返回的Response对象,进行判空和Decoder解码。
其实上述图变得好看一点就成了下面这张图(图片来源于知乎@黄青):
2.4.2 Feign源码分析
以上述代码为例来讲解,首先分析注解@EnableFeignClients和@FeignClient。@EnableFeignClients一般放于启动类接口上,@FeignClient放于Feign接口上。当程序开始运行时,@EnableFeignClients注解的作用是扫描所有@FeignClient注解修饰的接口,通过JDK底层的动态代理来创建接口,然后注入到容器中。@EnableFeignClients注解源码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {/*** Alias for the {@link #basePackages()} attribute. Allows for more concise annotation* declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of* {@code @ComponentScan(basePackages="org.my.pkg")}.* @return the array of 'basePackages'.*/String[] value() default {};/*** Base packages to scan for annotated components.* <p>* {@link #value()} is an alias for (and mutually exclusive with) this attribute.* <p>* Use {@link #basePackageClasses()} for a type-safe alternative to String-based* package names.** @return the array of 'basePackages'.*/String[] basePackages() default {};/*** Type-safe alternative to {@link #basePackages()} for specifying the packages to* scan for annotated components. The package of each class specified will be scanned.* <p>* Consider creating a special no-op marker class or interface in each package that* serves no purpose other than being referenced by this attribute.** @return the array of 'basePackageClasses'.*/Class<?>[] basePackageClasses() default {};/*** A custom <code>@Configuration</code> for all feign clients. Can contain override* <code>@Bean</code> definition for the pieces that make up the client, for instance* {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.** @see FeignClientsConfiguration for the defaults*/Class<?>[] defaultConfiguration() default {};/*** List of classes annotated with @FeignClient. If not empty, disables classpath scanning.* @return*/Class<?>[] clients() default {};
}
在上述源码中,引入了类FeignClientsRegistrar,该类在启动时会调用registerBeanDefinitions()方法,这个方法的内部只调用了registerDefaultConfiguration()和registerFeignClients()方法,registerDefaultConfiguration()方法主要检查是否有@EnableFeignClients注解,如果有的话,完成Feign框架的一些配置内容注册。registerBeanDefinitions方法代码如下:
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {.......@Overridepublic void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {registerDefaultConfiguration(metadata, registry);registerFeignClients(metadata, registry);}private void registerDefaultConfiguration(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {String name;if (metadata.hasEnclosingClass()) {name = "default." + metadata.getEnclosingClassName();}else {name = "default." + metadata.getClassName();}registerClientConfiguration(registry, name,defaultAttrs.get("defaultConfiguration"));}}......}
这里还有一个重要的方法就是registerFeignClients(),这个方法主要扫描@FeignClient注解修饰的类,将类的内容解析为BeanDefinition,最终通过调用Spring框架的BeanDefinitionReaderUtils.resgisterBeanDefinition 将解析处理过的 FeignClientBeanDeifinition 添加到 spring 容器中。具体代码如下:
public void registerFeignClients(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {ClassPathScanningCandidateComponentProvider scanner = getScanner();scanner.setResourceLoader(this.resourceLoader);Set<String> basePackages;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) {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() {@Overrideprotected 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) {Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);for (BeanDefinition candidateComponent : candidateComponents) {if (candidateComponent instanceof AnnotatedBeanDefinition) {// verify annotated class is an interfaceAnnotatedBeanDefinition 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);//每一个被@FeignClient注解修饰的接口就会被映射成一个BeanFactoryregisterClientConfiguration(registry, name,attributes.get("configuration"));//注册Feign客户端registerFeignClient(registry, annotationMetadata, attributes);}}}}
registerFeignClient方法源码如下,该方法内部组装BeanDefinition,然后注册到Spring IOC容器:
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);String contextId = getContextId(attributes);definition.addPropertyValue("contextId", contextId);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 = contextId + "FeignClient";AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be// nullbeanDefinition.setPrimary(primary);String qualifier = getQualifier(attributes);if (StringUtils.hasText(qualifier)) {alias = qualifier;}//获取bean定义并进行注册BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,new String[] { alias });BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);}
registerFeignClient()方法做了很多事情,它重新构造了一个类型是FeignClientFactoryBean的BeanDefinition,FeignClientFactoryBean类实现了FactoryBean接口,spring在生成bean时,如果发现BeanDefinition中的bean的class是由FactoryBean实现,就会调用实现类的getObject()方法来获取对象。至此,@EnableFeignClients的整个工作流程如下:
1.扫描指定路径(不指定就默认路径)下所有@FeignClient注解的类,然后每个类都生成一个BeanDefinition;
2.遍历每个BeanDefinition,取出每个@FeignClient注解的属性,构造新的BeanDefinition,传入FeignClientFactoryBean的class,然后注入到spring容器中。
当上述接口都被注入到容器之后,就要生成Feign客户端接口的动态代理。具体源码如下:
首先要分析的时feign在SpringCloud的核心配置类FeignAutoConfiguration,核心代码如下:
@Configuration
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.class})
public class FeignAutoConfiguration {@Autowired(required = false)private List<FeignClientSpecification> configurations = new ArrayList<>();@Beanpublic HasFeatures feignFeature() {return HasFeatures.namedFeature("Feign", Feign.class);}@Beanpublic FeignContext feignContext() {FeignContext context = new FeignContext();context.setConfigurations(this.configurations);return context;}
}
FeignClientSpecification是每个Feign客户端的配置类,上文registerClientConfiguration()方法中注入到spring容器中的就是这个内容,这些配置会被封装成FeignContext再注入到容器中。FeignContext 源码如下:
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {public FeignContext() {super(FeignClientsConfiguration.class, "feign", "feign.client.name");}}
FeignContext 继承了NamedContextFactory,构造方法中传入了FeignClientsConfiguration,属性propertySourceName和propertyName。NamedContextFactory的作用主要是用来进行配置隔离的,它实现了ApplicationContextAware,定义了一个属性parent(springboot所使用的ApplicationContext类),源码如下:
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>implements DisposableBean, ApplicationContextAware {public interface Specification {String getName();Class<?>[] getConfiguration();}//一个Feign客户端对应一个AnnotationConfigApplicationContextprivate Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();//一个客户与其对应的配置类private Map<String, C> configurations = new ConcurrentHashMap<>();//spring容器ApplicationContextprivate ApplicationContext parent;//默认的配置类private Class<?> defaultConfigType;private final String propertySourceName;private final String propertyName;public NamedContextFactory(Class<?> defaultConfigType, String propertySourceName,String propertyName) {this.defaultConfigType = defaultConfigType;this.propertySourceName = propertySourceName;this.propertyName = propertyName;}@Overridepublic void setApplicationContext(ApplicationContext parent) throws BeansException {this.parent = parent;}//根据客户端名称从context中获取AnnotationConfigApplicationContext,如果不存在,就向context中放入AnnotationConfigApplicationContext protected AnnotationConfigApplicationContext getContext(String name) {if (!this.contexts.containsKey(name)) {synchronized (this.contexts) {if (!this.contexts.containsKey(name)) {this.contexts.put(name, createContext(name));}}}return this.contexts.get(name);}//创建了一个AnnotationConfigApplicationContext对象,遍历配置,将配置类放入,最后放入父容器parent,所有的客户端最终都有一个共同的父容器,这里为每一个客户端构建了一个AnnotationConfigApplicationContext,然后基于这个ApplicationContext来解析配置类,通过这种方式实现配置隔离
protected AnnotationConfigApplicationContext createContext(String name) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();if (this.configurations.containsKey(name)) {for (Class<?> configuration : this.configurations.get(name).getConfiguration()) {context.register(configuration);}}for (Map.Entry<String, C> entry : this.configurations.entrySet()) {if (entry.getKey().startsWith("default.")) {for (Class<?> configuration : entry.getValue().getConfiguration()) {context.register(configuration);}}}context.register(PropertyPlaceholderAutoConfiguration.class,this.defaultConfigType);context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(this.propertySourceName,Collections.<String, Object> singletonMap(this.propertyName, name)));if (this.parent != null) {// Uses Environment from parent as well as beanscontext.setParent(this.parent);}context.refresh();return context;}}
在动态代理过程中,主要是通过FeignClientFactoryBean的getObject方法来获取到代理对象,代码如下:
@Overridepublic Object getObject() throws Exception {FeignContext context = applicationContext.getBean(FeignContext.class);Feign.Builder builder = feign(context);//判断是否指定url和url的具体位置,@FeignClient中指定的url属性,如果配置http://ip:port这种形式,就是直接访问,不经过注册中心if (!StringUtils.hasText(this.url)) {String url;if (!this.name.startsWith("http")) {url = "http://" + this.name;}else {url = this.name;}url += cleanPath();//走到这里是:http://+服务名,意味着是经过注册中心,然后通过loadBalance负载均衡方法获取一个Clientreturn loadBalance(builder, context, new HardCodedTarget<>(this.type,this.name, url));}//指定了url且不是以http开头,则拼接httpif (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 unwrapclient = ((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));}
它的流程主要是先从Spring容器中获取到FeignContext,FeignContext里面封装了每个Feign客户端的配置,然后通过FeignContext获取到一个Feign.Builder,Feign.Builder是用来构建动态代理类的,通过这个类的target方法,就能生成Feign动态代理。feign()方法源码如下:
protected Feign.Builder feign(FeignContext context) {FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);Logger logger = loggerFactory.create(this.type);// @formatter:offFeign.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:onconfigureFeign(context, builder);return builder;}
这个方法的主要作用就是从每个FeignClient对应的Spring容器中获取配置,填充到Feign.Builder中。
最后就是调用Feign.Builder的tartget方法:
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);}}
先调用build方法,这个方法就是将最开始填充到Feign.Builder给封装起来,构建了一个ReflectiveFeign,然后调用ReflectiveFeign的newInstance方法,传入HardCodedTarget。最后就到了newInstance()方法,通过target拿到接口类型,获取到所有方法并遍历处理,然后放入methodToHandler,通过InvocationHandlerFactory的create方法,传入methodToHandler和Target,获取到一个InvocationHandler,最后通过jdk动态代理,生成代理对象返回。
@Overridepublic <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;}
2.3 Feign优化
Feign底层默认使用URLConnection,不支持连接池,并发性能有限制。因此可以使用带连接池的Http客户端,例如Apache HttpClient、OKHttp。
3.小结
1.本文分析了openfeign的使用方式及原理,探索了openfeign的动态代理过程;
2.openfeign底层默认使用URLConnection,有一定的性能瓶颈;
3.openfeign严格意义上也是使用了一个RPC框架模型,它与dubbo的区别在于:dubbo通过TCP长连接的方式进行通信,适合数据量小、高并发和服务提供者远远少于消费者的场景;openfeign是通过REST API实现的远程调用,基于Http传输协议,服务提供者需要对外暴露Http接口供消费者调用,通过短连接的方式进行通信,不适合高并发的访问。
4.参考文献
1.https://www.bilibili.com/video/BV1LQ4y127n4
2.https://www.bilibili.com/video/BV13a41137JF
3.https://www.zhihu.com/question/298707085
4.https://zhuanlan.zhihu.com/p/78286377
SpringCloud源码探析(四)-OpenFeign使用及其原理相关推荐
- SpringCloud源码探析(三)-Nacos集群搭建与配置管理
1.概述 上一篇文章SpringCloud源码探析(二)-Nacos注册中心分析了nacos单机版的部署以及SpringBoot整合nacos,nacos不仅仅可以作为注册中心,也可以作为配置中心.本 ...
- matplotlib工具栏源码探析四(自定义工具项图标)
在matplotlib工具栏源码探析二(添加.删除内置工具项)和matplotlib工具栏源码探析三(添加.删除自定义工具项)两篇文章中,仔细观察会发现,不论内置工具项还是自定义工具项都没有图标,但是 ...
- SpringCloud源码探析(六)-消息队列RabbitMQ
1.概述 RabbitMQ是一个开源的消息代理和队列服务器,它是基于Erlang语言开发,并且是基于AMQP协议的.由于Erlang语言最初使用与交换机领域架构,因此使得RabbitMQ在Broker ...
- Selenium3 Python WebDriver API源码探析(19)加载FireFox用户配置文件
FireFox用户配置文件 Firefox 将用户个人信息(例如书签.密码.首选项.扩展.Cookie.证书等)保存在一系列文件中,它们被叫做用户配置文件,它们与 Firefox 的程序文件保存在不同 ...
- spring源码分析第四天------springmvc核心原理及源码分析
spring源码分析第四天------springmvc核心原理及源码分析 1.基础知识普及 2. SpringMVC请求流程 3.SpringMVC代码流程 4.springMVC源码分析 4.1 ...
- Anbox源码分析(四)——Anbox渲染原理(源码分析)
Anbox源码分析(四) 上篇文章我们从源码分析了一下Anbox是怎样一步步的准备了OpenGL ES的渲染环境的,这篇文章,我们继续分析Android的渲染指令是如何到达宿主机进行渲染的. 宿主机端 ...
- Forest源码探析
Forest 是一个开源的 Java HTTP 客户端框架,它能够将 HTTP 的所有请求信息(包括 URL.Header 以及 Body 等信息)绑定到您自定义的 Interface 方法上,能够通 ...
- Selenium3 Python WebDriver API源码探析(10):动作链(ActionChains):鼠标事件和键盘事件
鼠标.键盘事件是我们利用Selenium操控浏览器的重要交互手段,主要由selenium\webdriver\common\action_chains.py中的ActionChains类实现.该类通过 ...
- grpc-go源码剖析十四之round_robin平衡器原理介绍
本小节主要是介绍一下round_robin类型的平衡器的基本原理: 其实,就是如何处理两个场景: 场景一:跟grpc服务器端链接的策略是什么,比方说同一个服务,可能是由多个grpc服务器端提供的: ...
最新文章
- Java中的图像锐化操作
- 项目管理5大过程组与10大知识领域
- ajax框架dwr开发
- Swoole安装步骤
- 终于!华为在欧盟注册新商标“Harmony”;亚马逊AWS宣布张文翊“新官上任”;甲骨文失去竞购100亿美元国防部云计算合同资格...
- 备忘: Visual Studio 2013 VC++ IDE 使用小贴示。
- 计算机图形学-第一八分象限的DDA算法
- c语言的链表ppt,C语言链表详解ppt.ppt
- 简单实用算法——二分查找法(BinarySearch)
- Lucene.Net的简单练习
- 分享一款程序员起名神器,让你从此起名不再头秃
- Java求两点的中点坐标_计算两点坐标距离与中点坐标
- python unpacking_Python函数调用时unpacking参数特性
- python绘制五子棋棋盘_4.Python画一个五子棋棋盘
- 钉钉移动端和PC免登
- CTF MD5之守株待兔,你需要找到和系统锁匹配的钥匙
- 学Python的90个建议
- 你与未来感爆棚的智能城市 2.0 之间,只差一个分毫不差的精准时空
- 关于Oracle的参数是游标,如何处理(mirth)
- 抖音无人直播具体技术教程丨国仁网络资讯
热门文章
- oracle 监听程序服务无法启动,ORA-12500: TNS: 监听程序无法启动专用服务器进程
- 做好网络舆情监测监控的重要性,TOOM网络舆情监控平台建设方案?
- 按照规则输出5组不重复的双色球(6个红色球+1个蓝色球)
- 时尚促销好物推广产品宣传PR广告模板MOGRT
- javaSE从入门到精通的二十万字总结(一)
- python中pyecharts安装_Pyecharts安装及使用
- linux signals
- 头歌平台(EduCoder)————软件测试(测试过程与策略)
- BUCK电路中续流二极管的选择
- 分享一个64位的PS安装包