利用Spring扩展点模拟Feign实现远程调用(干货满满)

一、思路与编码分析

一般,我们会这么使用Feign:

图片.png

可以看到这里是一个interface(接口),也就是没有具体的实现类。

那么知道对于接口是没有办法直接new的,那么Spring也就无法获取到具体的实现类,然后进行注入,放到IOC容器中。

思考1:接口没有实现类?

这里我们就要思考,只有接口,没有实现类的话,这种情况下,一般要怎么搞?

一种思路就是动态代理,对于动态代理都可以把一些功能通用化。大部分的框架也是这么实现的,比如Feign,还有MyBatis的注解编程。

思考2:接口怎么扫描以及如何注入?

当使用注解编写了接口之后,那么这个接口是如何被扫描以及如何被注入的呢?

对于扫描的话,一般有两种方式:

(1)其一就是直接在接口上有一个注解,比如Mybatis就是使用了注解@Mapper。

(2)其二全局注解接口包扫描,比如Mybatis使用了@MapperScan

那么这些类是如何被注入呢?

这里很明显就需要使用到Spring的一些扩展点了,比如:导入bean定义以及工厂bean:

(1)
ImportBeanDefinitionRegistrar:使用import的方式注册bean定义,在这里会获取到所有注解了@FeignClient注解的接口,获取构建bean定义信息,具体的实现类呢?使用动态代理进行实现。

(2)FactoryBean:具体的实现类实现了接口FactoryBean,在这里会返回构造的对象,这个对象当然是代理对象。

编码分析

在编码分析之前,我们看下最后的一个代码结构:

图片.png

(1)注解EnableFeignClients:启用FeignClient以及指定要扫描的@FeignClient的包路径。在这个注解上有一个重要的注解@Imort导入一个类FeignClientsRegistrar。

(2)FeignClientsRegistrar:实现接口
ImportBeanDefinitionRegistrar,主要是获取注解EnableFeignClients的包信息,然后扫描该包下注解了@FeiClient的接口信息,并且添加bean定义,以及指定具体的实现类FeignFactoryBean。

(3)FeignFactoryBean:@FeignClient注解的接口具体的实现类。

(4)注解FeignMethod:用于注解请求的接口信息。

二、模拟FeignClient的实现

接下来,模拟FeignClient来看看,但这里还是有一些区别,这里的关键是我们要学习一下,如何使用Spring的扩展点,在实际的项目中进行使用。很多开源的框架都是利用了Spring的扩展点进行和Spring集成的。

2.1 创建项目

使用idea创建一个Spring Boot项目,取名为
spring-boot-myfeign-example,引入依赖starter-web。

这些在每次的项目讲解的时候,我都很详细的说明了,这次就展开了,不懂的看之前的文章。

2.2 @EnableFeignClients

使用注解@EnableFeignClients进行启用,具体的代码如下所示:

package com.kfit.config;

import org.springframework.context.annotation.Import;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**

  • 通过 @EnableFeignClients 引入了FeignClientsRegistrar客户端注册类
  • @author java易「头条号SpringBoot」
  • @date 2023-02-23
  • @slogan 大道至简 悟在天成
    */
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Import(FeignClientsRegistrar.class) //FeignClientsRegistrar
    public @interface EnableFeignClients {
    String basePackages();
    }

说明:

(1)注解@EnableFeignClients就是启用FeignClient的一个注解,主要用于指定要扫描的包路径以及引入一个bean定义的注册类。

(2)@Import:引入Bean定义的注册类FeignClientsRegistrar,这个类的具体实现看后面。

(3)其它注解,比如@Target、@Retention是元注解(注解的注解称为元注解)。

2.3 @FeignClient

注解FeiClient主要是用于接口,用于扫描过滤使用的:

package com.kfit.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**

  • 类注解,用于扫描过滤
  • @author java易「头条号SpringBoot」
  • @date 2022-02-23
  • @slogan 大道至简 悟在天成
    */
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface FeiClient {
    String name();
    }

2.4 @FeignMethod

注解@FeignMethod是指定请求的接口以及方法:

package com.kfit.config;

import org.springframework.http.HttpMethod;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**

  • 方法注解
  • @author java易「头条号SpringBoot」
  • @date 2022-02-23
  • @slogan 大道至简 悟在天成
    */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface FeignMethod {
    String path();
    HttpMethod method() default HttpMethod.POST;
    }

2.5 FeignClientsRegistrar

FeignClientsRegistrar实现了接口
ImportBeanDefinitionRegistrar,实现有点复杂,具体代码如下示例:

package com.kfit.config;

import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;

import java.io.IOException;
import java.util.Map;
import java.util.Set;

/**

  • 借助Spring的ImportBeanDefinitionRegistrar利器,在Spring初始化时注入相关BeanDefinition,
  • 必须为FactoryBean,以便于生成动态代理完成远程调用。
  • @author java易「头条号SpringBoot」
  • @date 2022-02-23
  • @slogan 大道至简 悟在天成
    */
    public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    //获取到注解上的属性 : {basePackages=com.kfit.demo.feign}
    Map<String, Object>  attrs = importingClassMetadata.getAnnotationAttributes(EnableFeignClients.class.getName());
    if(attrs == null && attrs.size() == 0){
        return;
    }
    //获取配置要扫描的包路径:com.kfit.demo.feign
    String basePackages = String.valueOf(attrs.get("basePackages"));
    System.out.println("basePackages:"+basePackages);
    //扫描包路径下的类.
    ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false){
        /**
         * 判断资源是否为候选的组件,这里直接返回true。
         *
         * 如果是底层的实现的话,会通过excludeFilters和includeFilters进行判断。
         * @return
         */
        @Override
        protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
            return true;
        }
    };
    //扫描的类上的注解为:@RMIClient
    scanner.addIncludeFilter(new AnnotationTypeFilter(FeiClient.class));
    //查找获选的组件.
    Set<BeanDefinition> beanDefinitionSet =  scanner.findCandidateComponents(basePackages);
    System.out.println("BeanDefinition:"+beanDefinitionSet.size());
    for(BeanDefinition beanDefinition:beanDefinitionSet){
        System.out.println(beanDefinition);
        // 向构造方法中添加参数,值为目标bean的全路径名,Spring会自动转换成Class对象
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
        beanDefinition.setBeanClassName(FeignFactoryBean.class.getName());
        registry.registerBeanDefinition(beanDefinition.getBeanClassName(),beanDefinition);
    }
}

}

说明:这个类对于没有使用Spring提供的方法进行编码的,会有点复杂,为了大家能够看懂,我已经在每行核心代码都添加了注释。

(1)首先需要获取到@EnableFeignClients的注解属性信息,获取到设置的包路径,比如:com.kfit.*.feign。

(2)使用Spring提供的ClassPathScanning…进行扫描到BeanDefinition,主要是通过设置的包路径和注解@FeiClient进行过滤。

(3)对获取到的BeanDefinition信息,进行属性赋值,最核心的一句代码就是指定bean class为FeignFactoryBean。对于FeignFactoryBean的实现看下面代码。

2.6 FeignFactoryBean

最后看下bean定义的具体实现类,具体代码如下所示:

package com.kfit.config;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.http.HttpMethod;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

/**

  • @author java易「头条号SpringBoot」
  • @date 2022-02-23
  • @slogan 大道至简 悟在天成
    */
    public class FeignFactoryBean<T> implements FactoryBean<T>, InvocationHandler {
    Map<String,String> hosts = new HashMap<>();
// 对象存储目标类型
private Class<T> targetClass;


// 构造方法传入目标类型
public FeignFactoryBean(Class<T> targetClass) {
    hosts.put("user-service","http://127.0.0.1:8080");
    this.targetClass = targetClass;
}


@Override
public T getObject() throws Exception {
    return (T) Proxy.newProxyInstance(targetClass.getClassLoader(), new Class[]{targetClass}, this);
}


@Override
public Class<?> getObjectType() {
    return targetClass;
}


@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String result = null;
    //获取方法上的注解信息.
    FeiClient rmiClient = targetClass.getAnnotation(FeiClient.class);
    FeignMethod rmiMethod = method.getAnnotation(FeignMethod.class);
    if(rmiMethod != null && rmiClient != null){
        String path = rmiMethod.path();
        HttpMethod httpMethod = rmiMethod.method();
        System.out.println("获取到的注解上的信息:" + rmiClient.name() + " -- " + rmiMethod.path());
        String urlPath = hosts.get(rmiClient.name()) + path;


        System.out.println("转换之后的请求路径:" +urlPath);


        //有了地址就可以使用HttpURLConnection、okHttp等发起网络请求.
        // 这里使用HttpURLConnection模拟一下.
        result = post(urlPath);
        System.out.println("网络请求执行的结果:" + result);
    }
    return result;
}




private String post(String urlPath){
    String result = "";
    try {
        URL url = new URL(urlPath);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        // 设置请求方式
        connection.setRequestMethod("POST");
        // 设置是否向HttpURLConnection输出
        connection.setDoOutput(true);
        // 设置是否从httpUrlConnection读入
        connection.setDoInput(true);
        // 设置是否使用缓存
        connection.setUseCaches(false);
        //设置参数类型是json格式
        connection.setRequestProperty("Content-Type", "application/json;charset=utf-8");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if(responseCode == HttpURLConnection.HTTP_OK) {
            //定义 BufferedReader 输入流来读取URL的响应
            BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

}

说明:

(1)FeignFactoryBean实现了两个接口FactoryBean以及InvocationHandler。

(2)实现FactoryBean主要是为了能够返回一个通过动态代理实现的对象。

(3)实现接口InvocationHandler这个就是JDK的动态代理了。

(4)getObject():使用jdk的Proxy构造了一个代理对象。注意这个方法在Spring启动的过程中就执行了,并不是每次访问的时候才执行,所以只执行一次。

(5)invoke():InvocationHandler的回调方法,此方法核心就是获取注解上的信息,构造url,然后使用相应的网络请求工具发起请求。

(6)网络请求方式可以是:HttpURLConnection(java原生)、OkHttp、Apache HttpClient、RestTemplate、Ribbon等发起网络请求。

(7)这里对于服务名称的解析,使用了最简单的Map存储方式,实际框架中会比复杂很多。

在前面我们对于RestTemplate和Ribbon有了一个基本的认知,那么这里有一个疑问?就是Ribbon可以不依赖RestTemplate单独使用吗?大家自行在评论区进行讨论。

三、模拟FeignClient的使用

我们已经通过Spring的扩展点FactoryBean和
ImportBeanDefinitionRegistrar模拟了FeignClient的简单实现,那么是否可用呢,那么就需要写个例子来验证下。

3.1添加注解@EnableFeignClients

在启动类上添加注解@EnableFeignClients:

@SpringBootApplication
@EnableFeignClients(basePackages = "com.kfit.*.feign")

说明:指定报名为com.kfit.*.feign。

3.2添加接口

使用@FeignClient添加接口:

package com.kfit.demo.feign;

import com.kfit.config.FeiClient;
import com.kfit.config.FeignMethod;

/**

  • 请求实例.
  • @author java易「头条号SpringBoot」
  • @date 2022-02-23
  • @slogan 大道至简 悟在天成
    */
    @FeiClient(name = "user-service")
    public interface DemoFeignClient {
    @FeignMethod(path= "/api")
    Object test();
    }

3.3使用DemoFeignClient

由于DemoFeignClient上添加了注解@FeiClient,所以就被注入使用动态代理的方式进行实现,那么就可以直接使用这个接口了(实际上也是类):

package com.kfit.demo;

import com.kfit.demo.feign.DemoFeignClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**

  • @author java易「头条号SpringBoot」
  • @date 2022-02-23
  • @slogan 大道至简 悟在天成
    */
    @RestController
    public class DemoController {
@Autowired
private DemoFeignClient demoFeignClient;


@RequestMapping("/api")
public Object api(){
    return "I love Angelababy!";
}


@RequestMapping("/test")
public Object test(){
    return demoFeignClient.test();
}

}

说明:

(1)/api:用于FeignClent进行调用。

(2)/test:用于项目使用demoFeignClient调用访问。

(3)请求说明:通过浏览器访问/test,然后调用demoFeignClient的test()方法;然后调用代理类FeignClientsRegistrar的invoke()方法,在invoke方法中会获取到接口DemoFeignClient上的注解信息,构建出请求地址,访问/api;最后就是相应的网络请求发起网络请求,返回接口。

3.4测试

启动Spring Boot应用,访问如下地址进行测试:

http://127.0.0.1:8080/test

看下控制台的一些信息打印:


图片.png

返回信息:I love Angelababy,你也喜欢吗?

总结

这一小结用到的知识点特别多,大家可以先收藏起来,然后多看几遍。利用这套思路,可以在实际项目中玩出很多花样。为了大家更好的理解,最后在对一些要点总结一下:

(1)整个程序的入口是@EnableFeignClients,Spring Boot在启动的时候会执行这个注解。

(2)对于@EnableFeignClients最重要的一个点就是通过@Import导入了另外一个类FeignClientsRegistrar。对于package的指定到不是最重要的,如果没有指定可以使用Spring Boot默认扫描的包路径,这个要怎么获取?你知道吗?

(4)对于FeignClientsRegistrar是整个@FeignClient注解的接口可以执行的关键。在该类中主要是扫描指定包下并且注解了@FeignClient的BeanDefinition,说明接口信息是可以被Spring扫描成为bean定义的,默认情况下Spring不扫描的,因为扫描进来了之后Spring不知道接口具体要如何实现。在FeignClientsRegistrar中添加了BeanDefinition,其中最重要的就是要设置具体的实现类Bean Class,这这里指定为FeignFactoryBean。

(5)对于FeignFactoryBean实现了接口FactoryBean、InvocationHandler。对于接口FactoryBean主要是实现方法getObject,通过JDK的代理类Proxy返回代理对象;对于InvocationHandler就是动态代理具体的处理方法了,在这里会获取到注解了@FeignClient接口上的注解信息和方法信息,构造出请求的URL地址,从而发起网络请求。

最后的最后,看没看懂不重要,先收藏起来,在慢慢品尝,这一篇值得你好好学习一下,你面试的话,就可以装逼了~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容