当前位置:首页 > 技术分析 > 正文内容

关于OpenFeign那点事儿 - 源码篇(openfeign底层原理)

ruisui883个月前 (02-03)技术分析19

引言

Hello 大家好,这里是Anyin。

在上一篇 关于OpenFeign那点事儿 - 使用篇[1] 我们聊了关于OpenFeign的使用以及在某些特殊场景下的解决方案。

今天我们来聊一聊,OpenFeign的底层是怎么实现的,为什么一个接口一个注解就可以实现远程的接口调用。

基础知识:Java动态代理

在我们日常开发中,最常见到的代码就是一个接口,一个实现类,通过实现类来执行具体的逻辑。如下:

    public interface FeignApi {
        ApiResponse infoByMobile(String mobile);
    }
    public class FeignApiImpl implements  FeignApi{   
        public ApiResponse infoByMobile(String mobile){
            log.info("this is execute impl method");
            return ApiResponse.success("impl method");
        }
    }

OpenFeign的使用方法却是只使用了一个接口和一个注解,并没有实现类,即可实现了远程调用。神奇吗 ?

其实在我们的Java基础当中,Java动态代理即可实现该功能。这里,我们来自己手动实现下。

首先,我们先创建一个接口:

    public interface FeignApi {
        ApiResponse infoByMobile(String mobile);
    }

接着,创建一个实现类FeignApiProxy,并实现InvocationHandler接口,在这个实现类来实现我们具体要执行的操作,其实就是用来代替FeignApi接口的实现类。

    @Slf4j
    public static class FeignApiProxy implements InvocationHandler{
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 如果进来是一个实现的具体类,直接调用
            if(Object.class.equals(method.getDeclaringClass())){
                return method.invoke(this, args);
            }else{ // 如果是一个接口
                log.info("this is execute proxy method");
                return ApiResponse.success("proxy method");
            }
        }
    }

然后,我们再创建一个创建动态代理对象的类ProxyFactory,它根据传递进来的类型,进行动态代理,创建代理对象。

    public class ProxyFactory {
        public  T getInstance(Class clazz){
            // 自定义实现的代理类
            FeignApiProxy proxy = new FeignApiProxy();
            // 返回代理对象
            return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, proxy);
        }
    }

最后,我们来测试下:

    @Test
    public void test(){
        // 创建代理工厂
        ProxyFactory factory = new ProxyFactory();
        // 创建代理对象
        FeignApi api = factory.getInstance(FeignApi.class);
        // 执行方法
        ApiResponse response = api.infoByMobile("anyin");
        log.info("response: {}", JSONUtil.toJsonStr(response));
    }

结果显示:

通过以上方法,我们就实现了一个简单的代理功能,只有接口,没有实现类的情况下,可以通过代理对象来执行具体的逻辑。

有人可能会疑问,虽然我们没有FeignApi接口的实现类,但是我们却额外实现了InvocationHandler接口的实现类,类的数量并没有变少,好像也没有省事多少。其实不然,FeignApi接口的实现类是具体的某个类型类,而InvocationHandler实现类是抽象的,我们可以在实现方法中处理各种逻辑。

例如:具体的使用场景就是我们场景的MybatisDao层接口,OpenFeign组件的FeignClien接口。

基础知识:Spring Cloud NamedContextFactory

在了解了Java动态代理之后,我们再了解下Spring Cloud 的父子容器机制。

Spring Cloud 它为了实现不同的微服务具有不同的配置,例如不同的FeignClient会使用不同的ApplicationContext,从各自的上下文中获取不同配置进行实例化。在什么场景下我们会需要这种机制呢? 例如,认证服务是会高频访问的服务,它的客户端超时时间应该要设置得比较小;而报表服务因为涉及到大量的数据查询和统计,它的超时时间就应该设置得比较大。

在Spring Cloud 中NamedContextFactory就是为了实现该机制而设计的。我们可以自己手动实现下这个机制。

首先,创建AnyinContext类和AnyinSpecification类。AnyinContext就是我们的上下文类,或者容器类,它是一个子容器。AnyinSpecification类是对应的配置类保存类,根据不通过的上下文名称(name字段)来获取配置类。

public static class AnyinSpecification implements NamedContextFactory.Specification {
        private String name;
        private Class[] configurations;
        public AnyinSpecification(String name, Class[] configurations) {
            this.name = name;
            this.configurations = configurations;
        }
        @Override
        public String getName() {
            return name;
        }
        @Override
        public Class[] getConfiguration() {
            return configurations;
        }
    }
    public static class AnyinContext extends NamedContextFactory{
        private static final String PROPERTY_SOURCE_NAME = "anyin";
        private static final String PROPERTY_NAME = PROPERTY_SOURCE_NAME + ".context.name";
        public AnyinContext(Class defaultConfigType) {
            super(defaultConfigType, PROPERTY_SOURCE_NAME, PROPERTY_NAME);
        }
    }

接着,我们创建三个bean类,它们会分别置于父容器配置、子容器公共配置、子容器配置类中。如下:

    public static class Parent {}
    public static class AnyinCommon{}
    @Getter
    public static class Anyin {
        private String context;
        public Anyin(String context) {
            this.context = context;
        }
    }

然后,再创建三个配置类,如下:

    // 父容器配置类
    @Configuration(proxyBeanMethods = false)
    public static class ParentConfig {
        @Bean
        public Parent parent(){
            return new Parent();
        }
        @Bean
        public Anyin anyin(){
            return new Anyin("anyin parent=============");
        }
    }
    // 子容器公共配置类
    @Configuration(proxyBeanMethods = false)
    public static class AnyinCommonCnofig{
        @Bean
        public AnyinCommon anyinCommon(){
            return new AnyinCommon();
        }
    }
    // 子容器1配置类
    @Configuration(proxyBeanMethods = false)
    public static class Anyin1Config{
        @Bean
        public Anyin anyin(){
            return new Anyin("anyin1=============");
        }
    }
    // 子容器2配置类
    @Configuration(proxyBeanMethods = false)
    public static class Anyin2Config{
        @Bean
        public Anyin anyin(){
            return new Anyin("anyin2=============");
        }
    }    

最后,我们来做下代码测试。

    @Test
    public void test(){
        // 创建父容器
        AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
        // 注册父容器配置类
        parent.register(ParentConfig.class);
        parent.refresh();
        // 创建子容器,并且注入默认的功能配置类
        AnyinContext context = new AnyinContext(AnyinCommonCnofig.class);
        // 子容器1配置类
        AnyinSpecification spec1 = new AnyinSpecification("anyin1", new Class[]{Anyin1Config.class});
        // 子容器2配置类
        AnyinSpecification spec2 = new AnyinSpecification("anyin2", new Class[]{Anyin2Config.class});
        // 子容器和父容器绑定关系
        context.setApplicationContext(parent);
        // 子容器注入子容器1/2配置类
        context.setConfigurations(Lists.newArrayList(spec1, spec2));
        // 获取子容器1的Parent实例
        Parent parentBean1 = context.getInstance("anyin1", Parent.class);
        // 获取子容器2的Parent实例
        Parent parentBean2 = context.getInstance("anyin2", Parent.class);
        // 获取父容器的Parent实例
        Parent parentBean3 = parent.getBean(Parent.class);
        // true
        log.info("parentBean1 == parentBean2: {}", parentBean1.equals(parentBean2));
        // true
        log.info("parentBean1 == parentBean3: {}", parentBean1.equals(parentBean3));
        // true
        log.info("parentBean2 == parentBean3: {}", parentBean2.equals(parentBean3));
        // 获取子容器1的AnyinCommon实例
        AnyinCommon anyinCommon1 = context.getInstance("anyin1", AnyinCommon.class);
        // 获取子容器2的AnyinCommon实例
        AnyinCommon anyinCommon2 = context.getInstance("anyin1", AnyinCommon.class);
        // true
        log.info("anyinCommon1 == anyinCommon2: {}", anyinCommon1.equals(anyinCommon2));
        // 报错,没有找到对应的bean
        // AnyinCommon anyinCommon3 = parent.getBean(AnyinCommon.class);
        // 获取子容器1的Anyin对象
        Anyin anyin1 = context.getInstance("anyin1", Anyin.class);
        // anyin1 context: anyin1=============
        log.info("anyin1 context: {}", anyin1.getContext());
        // anyin2 context: anyin2=============
        Anyin anyin2 = context.getInstance("anyin2", Anyin.class);
        log.info("anyin2 context: {}", anyin2.getContext());
        // false
        log.info("anyin1 == anyin2: {}", anyin1.equals(anyin2));
        // anyinParent: anyin parent=============
        Anyin anyinParent = parent.getBean(Anyin.class);
        log.info("anyinParent: {}", anyinParent.getContext());
    }

以上代码可能会比较长,请详细查阅。通过以上测试,我们可以总结以下几点:

?子容器可以拿到父容器的实例

?父容器无法拿到子容器的实例

?实例优先从公共配置类中获取(这点需要在AnyinCommonCnofig配置类添加Anyin实例配置)

源码解析

了解了以上2个基本的机制,这时候我们再来看源码,可能会容易理解一些。

OpenFeign组件的入口就是从@EnableFeignClients注解开始的。它导入了一个FeignClientsRegistrar类,具体的FeignClient就是在这个类内进行初始化和注册的。

FeignClientsRegistrar内,它先注册了默认的配置类,然后再注册具体的FeignClient。这个默认的配置类是需要在@EnableFeignClients注解内进行配置的,一般我们都不会配置,所以这里就暂时不管它。

接下来,我们看看注册具体的FeignClient是如何实现的。

我们主要看看registerFeignClient方法,该方法内通过FeignClientFactoryBean来进行实例的创建,主要代码如下:

接着,我们看看FeignClientFactoryBeangetObject()方法,该方法返回了代理对象。

通过feign方法获取了Feign.Builder实例,它是在FeignClientsConfiguration类中进行配置的。

loadBalance方法中,获取Client实例,该实例用于做客户端负载均衡,Targeter实例是DefaultTargeter,它同样是在FeignClientsConfiguration中进行装配的。

接着代码会回到Feign类的target方法。

我们直接看下ReflectiveFeign类的newInstance方法。

到这里,是不是看到了熟悉的场景,Java动态代理就是在这里处理的。target.type()返回的就是我们的定义的FeignClient接口。InvocationHandler的实现类就是ReflectiveFeign#FeignInvocationHandler,它的invoke方法就是我们在调用FeignClient接口的方法代理的具体实现。

根据方法实例,拿到对应的MethodHandler进行调用。dispatch其实就是一个map数据结构,如下:

返回的MethodHandler接口的实现类就是SynchronousMethodHandler,它的invoke方法就是开始执行具体的http调用逻辑。

最后

以上,就是根据个人理解梳理的OpenFeign源码流程,如果有什么问题欢迎大家指正。

个人认为,只要理解底层的Java动态代理Spring Cloud NamedContextFactory机制,然后再回过头过来梳理源码,整个流程会顺很多。

相关测试用例源码地址:Anyin Cloud[2]

References

[1] 关于OpenFeign那点事儿 - 使用篇: https://juejin.cn/post/7068179877047828517/
[2] Anyin Cloud:
https://gitee.com/anyin/anyin-cloud/tree/master/anyin-center-modules/anyin-center-auth/src/test/java/org/anyin/gitee/cloud/center/auth/test

扫描二维码推送至手机访问。

版权声明:本文由ruisui88发布,如需转载请注明出处。

本文链接:http://www.ruisui88.com/post/1188.html

标签: feign 使用
分享给朋友:

“关于OpenFeign那点事儿 - 源码篇(openfeign底层原理)” 的相关文章

Vue.js 组件通信的 3 大妙招

在 Vue.js 中,组件化是其核心概念之一,允许你将复杂的界面拆分成多个独立的、可复用的组件。在构建大型应用时,如何高效地在组件之间传递数据和触发事件是非常重要的。Vue.js 提供了多种方式来处理组件间的通信,下面是最常用的 3 种方式:1.父子组件通信:通过 Props 和 Events在 V...

「Git迁移」三行命令迁移Git包含提交历史,分支,tag标签等信息

问题描述:公司需要将一个git远程服务器的全部已有项目迁移到一台新服务器的Gitlab中,其中需要包含全部的提交纪录,已有的全部分支与全部打tag标签,目前此工作已全部迁移完毕,特此记录一下操作步骤环境描述:1. 要迁移的远程Git:Gitblit2. 迁移目的Git:Gitlab3. 暂存代码的P...

壹啦罐罐 Android 手机里的 Xposed 都装了啥

这是少数派推出的系列专题,叫做「我的手机里都装了啥」。这个系列将邀请到不同的玩家,从他们各自的角度介绍手机中最爱的或是日常使用最频繁的 App。文章将以「每周一篇」的频率更新,内容范围会包括 iOS、Android 在内的各种平台和 App。本期继续歪楼,由少数派撰稿作者@壹啦罐罐介绍他正在使用的...

BuildKit 镜像构建工具

#暑期创作大赛#快速开始 对于 Kubernetes 部署,请参阅examples/kubernetes。BuildKit 由buildkitd守护进程和buildctl客户端组成。虽然buildctl客户端可用于 Linux、macOS 和 Windows,但buildkitd守护程序目前仅适用于...

2024年,不断突破的一年

迈凯伦F1车队不久前拿下了2024年度总冠军,距离上一次还是二十几年前。在此期间,另一领域内,一个充满革新活力的腕表品牌——RICHARD MILLE理查米尔,正不断发展,与F1运动、帆船、古董车展等领域,共享着对速度与极限的无尽向往。RICHARD MILLE的发展与F1车手们在赛道上的卓越表现交...

全新斯柯达柯珞克Karoq深度评测:大众替代品

“斯柯达柯珞克是一款出色的全能家庭 SUV,具有许多有用的功能”价格36,605 英镑- 49,190 英镑优点方便的 VarioFlex 后排座椅非常适合家庭入住驾驶乐趣缺点保修期短保守的内饰性格比Yeti少结论——斯柯达柯珞克是一辆好车吗?斯柯达柯珞克是在辉煌的七座 斯柯达柯迪亚克之后推出的,因...