springcloud之通过openfeign优化服务调用方式

写在前面

源码 。
在前面的文章中我们实际上已经完成了优惠券模块微服务化的改造,但是其中还是有比较多可以优化和增强的地方,本文就先来对服务间的通信方式进行优化,具体就是使用openfeign来替换调原来的webclient。下面我们就开始吧!

1:为什么要替换webclient

使用webclient进行服务间调用的方式可能如下:

webClientBuilder.build()// 声明这是一个POST方法.post()// 声明服务名称和访问路径.uri("http://coupon-calculation-serv/calculator/simulate")// 传递请求参数的封装.bodyValue(order).retrieve()// 声明请求返回值的封装类型.bodyToMono(SimulationResponse.class)// 使用阻塞模式来获取结果.block()

这段代码有如下的不足:

1:和业务代码耦合,如请求地址,请求方式这些其实和业务是没有任何关系的,不符合指责隔离的原则
2:每个接口调用都需要写类似的重复代码,编码的效率低

针对以上的问题,springcloud给出的解决方案是openfeign ,可以认为openfeign是一种rpc框架允许我们通过好像调用一个本地的方法一样来调用远端的服务。

2:实战改造

2.1:引入openfeign依赖

首先我们需要在coupon-customer-impl的pom中引入openfeign的基础依赖:

<!-- OpenFeign组件 -->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2.2:定义服务的service

我们以调用template服务为例来进行改造,因此首先在coupon-customer-impl模块中定义如下的service:

@FeignClient(value = "coupon-template-serv-feign", path = "/template")
public interface TemplateService {// 读取优惠券@GetMapping("/getTemplate")CouponTemplateInfo getTemplate(@RequestParam("id") Long id);// 批量获取@GetMapping("/getBatch")Map<Long, CouponTemplateInfo> getTemplateInBatch(@RequestParam("ids") Collection<Long> ids);
}

在注解@FeignClient中定义了要访问的服务名称以及要web接口的基础路径这样就不用重复在方法上配置了,通过注解@XxxMapping定义的接口的访问路径信息,通过方法的参数来定义入参信息,这样发起服务调用的完整信息就都全了。

2.3:改造接口调用

我们来修改接口/coupon-customer/simulateOrder 来执行试算,当前代码如下:

public SimulationResponse simulateOrderPrice(SimulationOrder order) {...return webClientBuilder.build().post()
//                .uri("http://coupon-calculation-serv/calculator/simulate").uri("http://coupon-calculation-serv-feign/calculator/simulate").bodyValue(order).retrieve().bodyToMono(SimulationResponse.class).block();
}            

修改为openfeign后如下:

@Autowired
private CalculationService calculationService;
public SimulationResponse simulateOrderPrice(SimulationOrder order) {List<CouponInfo> couponInfos = Lists.newArrayList();...System.out.println("calculate by openfeign...");return calculationService.simulate(order);
}

最后还需要在main函数上增加注解@EnableFeignClients(basePackages = { "dongshi.daddy" })来设置需要扫描的openfeign服务接口所在的包路径。具体的大家可自行测试。效果是一样的。

3:openfeign原理分析

实战重要,但原理更重要,所以一起来看一波原理吧!

当我们在main上增加了@EnableFeignClients(basePackages = { "dongshi.daddy" })注解后,就会扫描指定包路径下标注了@FeignClient注解的接口,使用jdk的动态代理技术生成动态代理类,之后会将这个生成的动态代理类放到spring容器中,最后注入到需要的类中,这个过程如下:
http://wiki.hendp.com/pages/viewpage.action?pageId=122987084
看到这里不知道你有没有疑问,这个扫描包的过程是怎么开始的,其实秘密藏在@EnableFeignClients注解中,该注解如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {...
}

注意在注解上使用了@Import注解,spring会调用类FeignClientsRegistrar的registerBeanDefinitions方法,如下:

org.springframework.cloud.openfeign.FeignClientsRegistrar#registerBeanDefinitions
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {...// 注册feign客户端(重要!!!)registerFeignClients(metadata, registry);
}

registerFeignClients方法如下:

public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {// 最终存储所有openfeign的接口LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");if (clients == null || clients.length == 0) {ClassPathScanningCandidateComponentProvider scanner = getScanner();...Set<String> basePackages = getBasePackages(metadata);for (String basePackage : basePackages) {candidateComponents.addAll(scanner.findCandidateComponents(basePackage));}}else {...}for (BeanDefinition candidateComponent : candidateComponents) {if (candidateComponent instanceof AnnotatedBeanDefinition) {// verify annotated class is an interface...// 注册feign客户端registerFeignClient(registry, annotationMetadata, attributes);}}
}

registerFeignClients方法如下:

private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,Map<String, Object> attributes) {String className = annotationMetadata.getClassName();...FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();...BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {...// 获取基于jdk的动态代理类return factoryBean.getObject();});...
}

factoryBean.getObject方法最终调用到如下方法:

feign.ReflectiveFeign#newInstance
public <T> T newInstance(Target<T> target) {// 解析openfeign方法为MethodHandler,作为方法代理Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();for (Method method : target.type().getMethods()) {...}// 封装methodToHandler创建动态代理要使用的InvocationHandlerInvocationHandler handler = factory.create(target, methodToHandler);// 生成动态代理T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),new Class<?>[] {target.type()}, handler);...// 返回动态代理return proxy;}

到这里就成功获取动态代理类了。总结这个过程如下:

1:项目加载:在项目的启动阶段,EnableFeignClients 注解扮演了“启动开关”的角色,它使用 Spring 框架的 Import 注解导入了 FeignClientsRegistrar 类,开始了 OpenFeign 组件的加载过程。
2:扫包:FeignClientsRegistrar 负责 FeignClient 接口的加载,它会在指定的包路径下扫描所有的 FeignClients 类,并构造 FeignClientFactoryBean 对象来解析 FeignClient 接口。
3:解析 FeignClient 注解:FeignClientFactoryBean 有两个重要的功能,一个是解析 FeignClient 接口中的请求路径和降级函数的配置信息;另一个是触发动态代理的构造过程。其中,动态代理构造是由更下一层的 ReflectiveFeign 完成的。
4:构建动态代理对象:ReflectiveFeign 包含了 OpenFeign 动态代理的核心逻辑,它主要负责创建出 FeignClient 接口的动态代理对象。ReflectiveFeign 在这个过程中有两个重要任务,一个是解析 FeignClient 接口上各个方法级别的注解,将其中的远程接口 URL、接口类型(GET、POST 等)、各个请求参数等封装成元数据,并为每一个方法生成一个对应的 MethodHandler 类作为方法级别的代理;另一个重要任务是将这些 MethodHandler 方法代理做进一步封装,通过 Java 标准的动态代理协议,构建一个实现了 InvocationHandler 接口的动态代理对象,并将这个动态代理对象绑定到 FeignClient 接口上。这样一来,所有发生在 FeignClient 接口上的调用,最终都会由它背后的动态代理对象来承接。

最后上述流程中解析接口中方法和注解信息为MethodHandler的过程在如下方法中完成:

// org.springframework.cloud.openfeign.support.SpringMvcContract#processAnnotationOnMethod
// 解析FeignClient接口方法级别上的RequestMapping注解
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {// 省略部分代码...// 如果方法上没有使用RequestMapping注解,则不进行解析// 其实GetMapping、PostMapping等注解都属于RequestMapping注解if (!RequestMapping.class.isInstance(methodAnnotation)&& !methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) {return;}// 获取RequestMapping注解实例RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);// 解析Http Method定义,即注解中的GET、POST、PUT、DELETE方法类型RequestMethod[] methods = methodMapping.method();// 如果没有定义methods属性则默认当前方法是个GET方法if (methods.length == 0) {methods = new RequestMethod[] { RequestMethod.GET };}checkOne(method, methods, "method");data.template().method(Request.HttpMethod.valueOf(methods[0].name()));// 解析Path属性,即方法上写明的请求路径checkAtMostOne(method, methodMapping.value(), "value");if (methodMapping.value().length > 0) {String pathValue = emptyToNull(methodMapping.value()[0]);if (pathValue != null) {pathValue = resolve(pathValue);// 如果path没有以斜杠开头,则补上/if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {pathValue = "/" + pathValue;}data.template().uri(pathValue, true);if (data.template().decodeSlash() != decodeSlash) {data.template().decodeSlash(decodeSlash);}}}// 解析RequestMapping中定义的produces属性parseProduces(data, method, methodMapping);// 解析RequestMapping中定义的consumer属性parseConsumes(data, method, methodMapping);// 解析RequestMapping中定义的headers属性parseHeaders(data, method, methodMapping);data.indexToExpander(new LinkedHashMap<>());
}

写在后面

参考文章列表

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/582034.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C# 根据指定的类型,动态转换object数据到指定类型

封装类 namespace EFCoreDynamicCondition.Helptool {public class Helptool{public static T ConvertToType<T>(object value){try{return (T)Convert.ChangeType(value, typeof(T));}catch (InvalidCastException){// 转换失败的处理Console.WriteLine($"Convers…

Redis实现限流

1. 基于Redis的zset数据结构实现滑动窗口限流 我们可以将请求打造成一个zset数组&#xff0c;当每一次请求进来的时候&#xff0c;value保持唯一&#xff0c;可以用UUID生成&#xff0c;而score可以用当前时间戳表示&#xff0c;因为score我们可以用来计算当前时间戳之内有多少…

【Redis】八、哨兵模式

文章目录 一、概述这里的哨兵有两个作用多个哨兵 二、哨兵测试1、配置哨兵配置文件 sentinel.conf2、启动哨兵3、断开Master节点 三、哨兵模式优点&#xff1a;缺点&#xff1a; 哨兵模式的全部配置 参考&#xff1a;狂神说Java bilibili哨兵模式 一、概述 自动选取老大的模式…

在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序

如果您有 Android 设备&#xff0c;您可能会将个人和专业的重要文件保存在设备的 SD 卡上。这些文件包括照片、视频、文档和各种其他类型的文件。您绝对不想丢失这些文件&#xff0c;但当您的 SD 卡损坏时&#xff0c;数据丢失是不可避免的。 幸运的是&#xff0c;您不需要这样…

Appium+python自动化(一)- 环境搭建—上(超详解)

简介 今天是高考各地由于降水&#xff0c;特别糟糕&#xff0c;各位考生高考加油&#xff0c;全国人民端午节快乐。最近整理了一下自动化的东西&#xff0c;先前整理的python接口自动化已经接近尾声。即将要开启新的征程和篇章&#xff08;Appium&python&#xff09;。那么…

2023-12-27 语音转文字的whisper应用部署

点击 <C 语言编程核心突破> 快速C语言入门 语音转文字的whisper应用部署 前言一、部署whisper二、部署whisper.cpp总结 前言 要解决问题: 需要一款开源的语音转文字应用, 用于视频自动转换字幕. 想到的思路: openai的whisper以及根据这个模型开发的whisper.cppC应用. …

代码随想录算法训练营第三十天|332.重新安排行程、51. N皇后 、37. 解数独

332.重新安排行程 题目链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 文档讲解&#xff1a;代码随想录 C代码&#xff1a; class Solution { public: unordered_map<string, map<string, int>> targets;bool backtrack…

Java 已死、前端已凉?

文章目录 Java 的现状前端技术的现状分析结论 关于“Java 已死、前端已凉”的言论&#xff0c;这种说法更多地反映了行业对技术趋势的一种情绪化反应&#xff0c;而不一定是基于事实的判断。下面我来具体分析这个话题。 Java 的现状 Java 的普及与稳定性&#xff1a;Java 作为一…

一套基于springboot、mybaits、avue技术开发的医院绩效考核系统源码,可适应医院多种绩效核算方式

医院绩效定义&#xff1a; “医院工作量绩效方案”是一套以工作量&#xff08;RBRVS&#xff0c;相对价值比率&#xff09;为核算基础&#xff0c;以工作岗位、技术含量、风险程度、服务数量等业绩为主要依据&#xff0c;以工作效率和效益、工作质量、患者满意度等指标为综合考…

边缘计算网关:在智慧储能系统中做好储能通信管家

背景 目前储能系统主要由储能单元和监控与调度管理单元组成&#xff0c;储能单元包含储能电池组(BA)、电池管理系统(BMS)、储能变流器(PCS)等&#xff1b;监控与调度管理单元包括中央控制系统(MGCC)、能量管理系统(EMS)等。 2021年8月&#xff0c;国家发改委发布《电化学储能…

解析正交镜像滤波器组

正交镜像滤波器组&#xff08;Orthogonal Mirror Filter Banks&#xff09;是一种在信号处理领域中常用的滤波器组结构&#xff0c;它在信号分析、多尺度表示和图像压缩等领域发挥着重要作用。本文将着重介绍正交镜像滤波器组的原理、特点以及在信号处理和图像压缩中的应用。 …

2023年中职“网络安全”——B-5:网络安全事件响应(Server2216)

B-5&#xff1a;网络安全事件响应 任务环境说明&#xff1a; 服务器场景&#xff1a;Server2216&#xff08;开放链接&#xff09; 用户名:root密码&#xff1a;123456 1、黑客通过网络攻入本地服务器&#xff0c;通过特殊手段在系统中建立了多个异常进程&#xff0c;找出启…

javaEE -19(9000 字 JavaScript入门 - 4)

一&#xff1a; jQuery jQuery是一个快速、小巧且功能丰富的JavaScript库。它旨在简化HTML文档遍历、事件处理、动画效果以及与后端服务器的交互等操作。通过使用jQuery&#xff0c;开发者可以以更简洁、更高效的方式来编写JavaScript代码。 jQuery提供了许多易于使用的方法和…

SQL 解析 — 如何轻松实现新增语句

KaiwuDB 支持多种不同类型的 SQL 语句&#xff0c;例如 create、insert 等。本文将介绍在 KaiwuDB SQL Parser&#xff08;下文统称解析器&#xff09;中添加新语句的过程及其实现。我们将了解如何使用 goyacc 工具更新解析器&#xff0c;以及执行器和查询计划器&#xff08;pl…

使用Python Flask搭建一个简单的Web站点并发布到公网上访问

文章目录 前言1. 安装部署Flask并制作SayHello问答界面2. 安装Cpolar内网穿透3. 配置Flask的问答界面公网访问地址4. 公网远程访问Flask的问答界面 前言 Flask是一个Python编写的Web微框架&#xff0c;让我们可以使用Python语言快速实现一个网站或Web服务&#xff0c;本期教程…

遇到跨端开发或多项目开发时,遇到的一些问题探讨,后端开发语言如何选择?

最近有同学问我&#xff0c;做后端开发项目时用php&#xff0c;java&#xff0c;c#&#xff0c;go&#xff0c;pathon…哪个好&#xff0c;从最近阿里云、美团服务器崩溃来看&#xff0c;我想给你最直接的回答是&#xff0c;没有完美的&#xff0c;只有适合自己的。咱们讨论最多…

探索Go语言的魅力:一门简洁高效的编程语言

介绍Go语言&#xff1a; Go&#xff0c;也被称为Golang&#xff0c;是由Google开发的一门开源编程语言。它结合了现代编程语言的优点&#xff0c;拥有高效的并发支持和简洁的语法&#xff0c;使其成为构建可伸缩、高性能应用的理想选择。 Go语言的特性&#xff1a; 并发编程…

Leetcode 56 合并区间

题意理解&#xff1a; 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。 合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组。 该数组需恰好覆盖输入中的所有区间 。 目标&#xff1a;合并…

计算机基础面试题总结

47、OSI、TCP/IP、五层协议的体系结构以及各层协议 OSI分层&#xff08;7层&#xff09;&#xff1a;物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。 TCP/IP分层&#xff08;4层&#xff09;&#xff1a;网络接口层、网际层、运输层、应用层。 五层协议&…

k8s集群etcd备份与恢复

一、前言 k8s集群使用etcd集群存储数据&#xff0c;如果etcd集群崩溃了&#xff0c;k8s集群的数据就会全部丢失&#xff0c;所以需要日常进行etcd集群数据的备份&#xff0c;预防etcd集群崩溃后可以使用数据备份进行恢复&#xff0c;也可用于重建k8s集群进行数据恢复 二、备份…