首页 > 其他分享 >手写Openfeign实现原理——极简版

手写Openfeign实现原理——极简版

时间:2023-09-03 22:01:00浏览次数:32  
标签:Openfeign Spring 简版 代理 接口 bean 注解 手写 public



文章目录

  • 前言
  • Openfeign实现思路
  • 前期准备
  • 基本依赖项
  • 开始实现
  • 自定义注解
  • 自定义代理类
  • 定义创建代理对象的工厂
  • InstantiationAwareBeanPostProcessor实现bean的注入
  • OpenInstantiationAwareBeanPostProcessor 自定义
  • feign接口
  • 启动类
  • 小结
  • 踩坑记录
  • @Import
  • @Component和@Configuration区别是什么
  • 总结


前言

最近开发cloud项目,里面涉及到服务间调用,最后使用的openfeign解决的,于是对于openfeign的底层原理有些兴趣了,提前透露一下底层无非就是使用一些http调用的工具帮助我们实现了请求调用

Openfeign实现思路

手写Openfeign实现原理——极简版_openfeign

前期准备

基本依赖项

  • 首先创建一个springboot项目
  • 有一个发送请求的工具这里使用的ribbon
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
            <version>2.2.9.RELEASE</version>
            <scope>compile</scope>
            <optional>true</optional>
        </dependency>

开始实现

自定义注解

创建两个自定义注解,分别用于在启动类和接口上添加

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@Import(OpenInstantiationAwareBeanPostProcessor.class)//这个类重点注意
public @interface EnableRemoteClient {
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface CustomProxy {
    String servie();

    String address();
}

自定义代理类

代理类中有一个RestTemplate 用于发送请求

public class CustomProxyHandler implements InvocationHandler {
    RestTemplate restTemplate=new RestTemplate();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        //首先获取对象
        Class<?> declaringClass = method.getDeclaringClass();
        //判断当前的被代理对象是否使用了自定的注解
        if(declaringClass.isAnnotationPresent(CustomProxy.class)){
            CustomProxy annotation = declaringClass.getAnnotation(CustomProxy.class);
            String servie = annotation.servie();
            String address = annotation.address();
            String url=address+servie;
            RequestMapping annotation1 = method.getAnnotation(RequestMapping.class);
            url=url+annotation1.value()[0];
            //判断请求方法是否入参
            if(args!=null&&args.length!=0){
                return restTemplate.getForObject(url,method.getReturnType(),args);
            }else {
                return restTemplate.getForObject(url,method.getReturnType());
            }
        }else{
            return null;
        }


    }
}

定义创建代理对象的工厂

用于创建代理对象使用,

public class ProxyFactory {

    /**
     * @description: 创建具体的代理对象
     * @author: 
     * @date: 2023/9/1 17:30
     * @param: [targetInterface]
     * @return: T
     **/
    public static  <T> T createProxy(Class<T> targetInterface) {
        return (T) Proxy.newProxyInstance(
                targetInterface.getClassLoader(),
                new Class[]{targetInterface},
                new CustomProxyHandler()
        );
    }
}

InstantiationAwareBeanPostProcessor实现bean的注入

InstantiationAwareBeanPostProcessor 是 Spring 框架提供的一个扩展接口,它在 Spring 容器实例化 bean 之前和之后对 bean 进行处理。其主要功能如下:

  1. 实例化前置处理(Before Instantiation):在 Spring 容器实例化 bean 之前,可以通过这个接口来自定义 bean 的实例化方式。可以在此处进行一些特殊的准备工作,比如使用自定义的实例化逻辑或者切入点的选择。
  2. 实例化后置处理(After Instantiation):在 Spring 容器实例化 bean 之后,可以通过这个接口对实例化后的 bean 进行处理。可以在此处做一些初始化的操作,比如对属性进行赋值、调用初始化方法等。
  3. 属性设置前置处理(Before Property Set):在 Spring 容器对 bean 的属性进行设置之前,可以通过这个接口来自定义属性的设置方式。可以在此处对属性进行修改或者校验操作。
  4. 属性设置后置处理(After Property Set):在 Spring 容器对 bean 的属性进行设置之后,可以通过这个接口对属性设置后的 bean 进行处理。可以在此处做一些属性设置后的额外操作。
  5. 初始化前置处理(Before Initialization):在 Spring 容器对 bean 进行初始化之前,可以通过这个接口来自定义初始化的方式。可以在此处添加一些额外的初始化逻辑。
  6. 初始化后置处理(After Initialization):在 Spring 容器对 bean 进行初始化之后,可以通过这个接口对初始化后的 bean 进行处理。可以在此处做一些初始化后的额外操作。

通过实现 InstantiationAwareBeanPostProcessor 接口,并重写其中的方法,可以在 Spring 容器实例化和初始化 bean 的各个阶段进行自定义处理,从而灵活地对 bean 进行定制化的操作。

OpenInstantiationAwareBeanPostProcessor 自定义

public class OpenInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor, ApplicationContextAware {


    private ApplicationContext applicationContext;
    /**
     * 实例化前,后面的方法可以不用看了
     * @param beanClass
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {

        log.info("正在为:{}生成代理对象,被代理的类为:{}",beanName,beanClass.getName());
        if(!beanClass.isAnnotationPresent(CustomProxy.class)){
            return null;
        }

        //动态代理里面需要实现的方法,本文采用的是jdk动态代理
        //返回代理对象
        Object object = ProxyFactory.createProxy(beanClass);
        return object;

    }

    /**
     * 实例化后
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        return true;
    }

    @Override
    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
        return pvs;
    }


    /**
     * 初始化钱
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    /**
     * 初始化后
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext=applicationContext;
    }
}

feign接口

这里的servie没有参数是因为我的另一个服务中没有配置context-path,如果你们自己的被调用的那个demo中有配置一定要加上哦

@CustomProxy(servie = "",address = "http://localhost:8082/")
public interface MyService {
    @RequestMapping(value = "/QuerySubselect",method = RequestMethod.GET)
    String  test();
}

启动类

@SpringBootApplication
@EnableRemoteClient
@Import({MyService.class})//接口注册,注意接口上面加@Se
public class OpenfeignDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(OpenfeignDemoApplication.class, args);
    }
    
}

小结

这个版本的实现目前spring还不能识别我添加了注解的这个接口,需要在启动类中使用@Import注解手动配置,所以还不完善,不过后期会继续完善的,目前博主也在学习这个,这个思路就是基于我们正常的发送请求,然后有一个人帮助我们去发送请求这个思路。

踩坑记录

@Import

将接口 MyService 标识为 @Import 的目的是为了在启动类中将该接口的实现类(动态代理对象)注册到 Spring 容器中。

当你使用 @Import({MyService.class}) 注解时,Spring 在启动时会扫描被注解的类,并根据其类型进行相应的处理。在这种情况下,MyService 类型是一个接口,而不是一个具体的类。

由于接口无法直接实例化,因此你需要在某处提供对 MyService 接口的实现类的创建逻辑。通常情况下,使用动态代理是一种常见的方式。

动态代理是一种在运行时生成代理类的机制,可以在不修改原始接口源代码的情况下,通过代理类来增强接口的功能。你可以编写一个动态代理类,实现 MyService 接口,并在动态代理类中实现你所需的增强逻辑。

然后,在启动类上使用 @Import 注解,并传入该动态代理类的类型,告诉 Spring 在启动时需要将该动态代理类注册到容器中。

这样,当其他组件需要使用 MyService 接口时,Spring 容器会从容器中获取该接口的实例,而实际上获得的是动态代理对象,该代理对象会在实际调用接口方法时根据你的增强逻辑进行处理。

  • 因为openfeign的标注的接口没有实现类所以需要这个@Import注解帮助我们告诉spring容器可以去容器中去这个类型的代理对象,目前还没有想到其他的方式不用使用@Import注解声明的。后续再想想办法

@Component和@Configuration区别是什么

@Configuration 注解不能直接添加到接口上,因为 @Configuration 注解是用于标识一个类为配置类的注解,它主要用于定义和配置 Bean 对象以及其他的配置信息。

手写Openfeign实现原理——极简版_openfeign_02


一开始使用这个注解导致使用了自定义注解的接口一直扫描不到,后来修改为@Component注解后就可以了。

@Component 是 Spring Framework 中的一个核心注解之一,用于将一个普通的 Java 类标识为一个可以被 Spring 自动扫描并管理的组件。

具体来说,@Component 注解可以应用于以下场景:

  1. Bean 的自动扫描和注册:通过在类上添加 @Component 注解,Spring 容器会自动扫描并将该类作为 Bean 进行注册,使得我们可以通过依赖注入等方式方便地使用该类的实例。
  2. 类型指定的自动装配:当需要进行自动装配时,Spring 容器会检测所有被 @Component 注解标记的类,并根据类型进行自动装配。

@Component 注解是一个通用的注解,如果对应的类有更具体的角色或作用,还可以使用其他派生注解,例如:

  • @Controller:标识一个类是控制器(Controller)组件,用于接收和处理用户请求。
  • @Service:标识一个类是服务(Service)组件,用于封装业务逻辑。
  • @Repository:标识一个类是数据访问仓库(Repository)组件,用于访问持久化数据。

这些派生注解都是基于 @Component 注解的,继承了 @Component 的功能,并且更明确地表示了对应类的角色和用途。

总结

对于这次去实现这么一个框架的小demo自己思考了一天,不能说很长时间吧,主要是因为对于spring中提供的一些扩展机制还不是很了解,但是我相信它一定提供了能够解决我上面问题的一些api的,只是我还没有发现而已。对于这些技术的应用底层原理还是很简单的,可以发现无非就是没让你做的事情有人帮你做了这么一个过程,只不过这个过程被人封装起来以后就是一个技术或者一个框架了。


标签:Openfeign,Spring,简版,代理,接口,bean,注解,手写,public
From: https://blog.51cto.com/u_15918766/7343567

相关文章

  • Lnton羚通算法算力云平台使用支持向量机来识别手写字符相关步骤
    使用支持向量机(SupportVectorMachine,SVM)来识别手写字符是一个常见的机器学习任务。下面是一个基本的步骤:数据准备:收集手写字符的训练数据集和测试数据集。每个样本应该包括一个手写字符图像和相应的标签或类别。特征提取:从手写字符图像中提取特征。常见的特征提取方法包括使用......
  • 个性化定制界面还是极简版原装界面?我的选择是……
     一、我的观点和选择个性化定制界面和极简版原装界面,二者各有优缺点。(一)极简版原装界面的优缺点1.优点通用性强,容易上手,资源占用相对也比较少,开发工作量也相对较少,更新维护也相对容易。2.缺点无法有针对性地进个性化设置,可能导致审美疲劳,对颜值党吸引力不强。(二)个性化定制界面的......
  • 手写raft(三) 实现日志压缩
    手写raft(三)实现日志压缩在上一篇博客中MyRaft实现了日志复制功能,按照计划接下来需要实现日志压缩。手写raft(一)实现leader选举手写raft(二)实现日志复制1.什么是raft日志压缩?我们知道raft协议是基于日志复制的协议,日志数据是raft的核心。但随着raft集群的持续工作,ra......
  • dbeaver 使用教程(简版)
    dbeaver使用教程(简版)原文链接:https://www.jianshu.com/p/597535271ae1DBeaver脚本窗口字体调整、放大和缩小字体技巧一、菜单栏-窗口-编辑器 快捷键:ctrl++ 技巧二、脚本关键字大小、窗口-首选项-SQL格式化-关键字大小写(调整字体) 技巧三:快捷键汇总ctrl+enter执......
  • JS手写代码实现深拷贝
    /***深拷贝*/constobj1={age:20,name:'xxx',address:{city:'beijing'},arr:['a','b','c']}constobj2=obj1obj2.address.city='shanghai'console.log(o......
  • 手写题
    1.使用js实现二分查找//非递归的方式functionsearch(arr,key){varstart=0;varend=arr.length-1while(start<=end){varmid=parseInt((start+end)/2)if(key===arr[mid]){......
  • 手写RISC-V处理器--1
    由来由于去年工作变动,有幸进入了芯片行业,但主要工作内容为基于RISC-V的嵌入式应用软件开发,几乎接触不到芯片设计的相关知识,然而随着工作的深入,越来越想探究一下运行在软件之下的CPU的世界,于是便产生了自己手写一个RISC-V处理器的想法,同时在博客里记录和分享一下自己学习和探索的......
  • 手写apply-call-bind实现
    call1Function.prototype.myCall=function(thisArg,...args){2letfn=this//隐式调用3thisArg=(thisArg!==null&&thisArg!==undefined)?Object(thisArg):window//传undefinednull指向window全局4thisArg.fn=fn//15letresult=thisArg.fn(......
  • 手写ES6.0-flat()
    一、问题描述已有多级嵌套数组:[1,[2,[3,[4,5]]],6]将其扁平化处理,输出:[1,2,3,4,5,6]二、详细描述题目表示对于一个多维的数组,需要构建一个方法,将其直接转化为一个一维数组的输出三、解法思路(一)递归1.思路查看题目的表述就可发现,其实数组中的每一个元素都是整......
  • java笔试手写算法面试题大全含答案
    1.统计一篇英文文章单词个数。publicclassWordCounting{publicstaticvoidmain(String[]args){try(FileReaderfr=newFileReader("a.txt")){intcounter=0;booleanstate=false;intcurrentChar;while((currentChar=fr.read())!=-1){if(currentChar=='......