IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 利用Spring扩展点模拟Feign实现远程调用(干货满满)「扩展点实战系列」- 第445篇 -> 正文阅读

[Java知识库]利用Spring扩展点模拟Feign实现远程调用(干货满满)「扩展点实战系列」- 第445篇

?

历史文章(文章累计440+)

国内最全的Spring Boot系列之一

国内最全的Spring Boot系列之二

国内最全的Spring Boot系列之三

国内最全的Spring Boot系列之四

国内最全的Spring Boot系列之五》

SpringBoot/Spring扩展点系列之初始化和销毁的3种办法 - 第438篇

观察者模式实际应用场景「扩展点实战系列」- 第439篇

服务信息上报+记录请求信息+监听项目运行状态还能这么玩「扩展点系列」- 第440篇

配置类信息赋值为Java静态变量「扩展点实战系列》」- 第441篇

3种方案扩展RestTemplate让其具备负载均衡(超级详细)「扩展点实战系列」- 第442篇

一个注解@LoadBalanced就能让RestTemplate拥有负载均衡的能力?「扩展点实战系列」- 第443篇

Spring注解@Qualifier的详细用法你知道几种「扩展点实战系列」- 第444篇

悟纤:师傅,这个有点奇怪耶?

师傅:哪里奇怪了?

悟纤:你看,我在使用MyBatis的注解编程的时候,我写的都是接口,比如这个

悟纤:写了接口,没有任何的实现,为什么能执行呢?接口不是没有实现类,难道也可以执行?java什么时候这么牛逼了?

师傅:这个就有点说了,具体的需要使用到Spring的扩展点以及动态代理的知识,那我们这一节课就来聊一聊 为什么接口 能够执行?

悟纤:师傅,那赶紧的呗,这已经困惑我好几年了?

师傅:好几年,你是不是欠揍?这今年你都不思考的么?

悟纤:这个不是刚入门,基础还没有大扎实么,哪有那个心思去思考这一些。

师傅:你说的也对~,那现在工作了这么多年了,这个要思考一下了,不然面试被问到了,都不知道从何答起。

悟纤:师傅说的是,这一节我一定会认真做笔记的~

导读

用过MyBatis的注解编程和@FeignClient,是不是有一点好奇,我们写的代码都是接口,也没有具体的实现类,接口怎么能执行呢?底层到底发生了事情?通过本节模拟FeignClient将会揭开MyBatis和FeignCleint的面纱。

?👇🏻👇🏻👇🏻扩展点实战系列

01.观察者模式实际应用场景「扩展点实战系列」

02.服务信息上报+记录请求信息+监听项目运行状态还能这么玩🐴「扩展点实战系列」

03.配置类信息赋值为Java静态变量「扩展点实战系列》」

04. 3种方案扩展RestTemplate让其具备负载均衡(超级详细)「扩展点实战系列」

05.一个注解@LoadBalanced就能让RestTemplate拥有负载均衡的能力?「扩展点实战系列」》

06.Spring注解@Qualifier的详细用法你知道几种「扩展点实战系列」

07.利用Spring扩展点模拟FeignClient实现远程调用(干货满满)「扩展点实战系列」

08. 「待拟定」《深入FeignClient源码吃透Spring扩展点「扩展点实战系列」》

一、思路与编码分析

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

可以看到这里是一个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,在这里会返回构造的对象,这个对象当然是代理对象。

编码分析

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

(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 悟纤「公众号SpringBoot」 * @date 2022-09-13 * @slogan 大道至简 悟在天成 */@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Import(FeignClientsRegistrar.class) //FeignClientsRegistrarpublic @interface EnableFeignClients {    String basePackages();}?

说明:

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

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

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

更多@Import的知识,可以关注公众号「SpringBoot」回复关键词[360],查看文章:

《Spring Boot注解@Import小试牛刀 - 第360篇》

更多元注解的知识,可以关注公众号「SpringBoot」回复关键词[404],查看文章:

《Spring Boot @ConditionalOnClass上的注解你了解多少-java元注解和注解 - 第404篇》

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 悟纤「公众号SpringBoot」 * @date 2022-09-13 * @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 悟纤「公众号SpringBoot」 * @date 2022-09-13 * @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 悟纤「公众号SpringBoot」 * @date 2022-09-13 * @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的实现看下面代码。

👉🏻更多BeanDefinition的知识,可以关注公众号「SpringBoot」回复关键词[427],查看文章:

《SpringBoot/Spring扩展点BeanDefinitionRegistryPostProcessor - 第427篇》

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 悟纤「公众号SpringBoot」 * @date 2022-09-13 * @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单独使用吗?大家自行在评论区进行讨论。

👉🏻更多关于FactoryBean的知识,可以关注公众号「SpringBoot」回复关键词[435],查看文章:

《SpringBoot/Spring扩展点系列之FactoryBean让你不在懵逼 - 第435篇》

三、模拟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 悟纤「公众号SpringBoot」 * @date 2022-09-13 * @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 悟纤「公众号SpringBoot」 * @date 2022-09-13 * @slogan 大道至简 悟在天成 */@RestControllerpublic 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

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

返回信息: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地址,从而发起网络请求。

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

我就是我,是颜色不一样的烟火。
我就是我,是与众不同的小苹果。

à悟空学院:https://t.cn/Rg3fKJD

学院中有Spring?Boot相关的课程!点击「阅读原文」进行查看!

SpringBoot视频:http://t.cn/A6ZagYTi

SpringBoot交流平台:https://t.cn/R3QDhU0

SpringSecurity5.0视频:http://t.cn/A6ZadMBe

ShardingJDBC分库分表:http://t.cn/A6ZarrqS

分布式事务解决方案:http://t.cn/A6ZaBnIr

JVM内存模型调优实战:http://t.cn/A6wWMVqG

Spring入门到精通:https://t.cn/A6bFcDh4

大话设计模式之爱你:https://dwz.cn/wqO0MAy7

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-09-30 00:38:26  更:2022-09-30 00:38:58 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年3日历 -2025/3/10 15:24:59-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码