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

前言

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

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 对象以及其他的配置信息。
在这里插入图片描述
一开始使用这个注解导致使用了自定义注解的接口一直扫描不到,后来修改为@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的,只是我还没有发现而已。对于这些技术的应用底层原理还是很简单的,可以发现无非就是没让你做的事情有人帮你做了这么一个过程,只不过这个过程被人封装起来以后就是一个技术或者一个框架了。

猜你喜欢

转载自blog.csdn.net/pengjun_ge/article/details/132628158