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创建动态代理要使用的InvocationHandler
    InvocationHandler 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<>());
}

写在后面

参考文章列表

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

低代码平台在金融银行中的应用场景

随着数字化转型的推进&#xff0c;商业银行越来越重视技术在业务发展中的作用。在这个背景下&#xff0c;白码低代码平台作为一种新型的开发方式&#xff0c;正逐渐受到广大商业银行的关注和应用。白码低代码平台能够快速构建各类应用程序&#xff0c;提高开发效率&#xff0c;…

网络攻防中应该掌握的进阶工具udp2raw,通过raw socket给UDP包加上TCP或ICMP header,进而绕过UDP屏蔽或QoS

网络攻防中应该掌握的进阶工具udp2raw,通过raw socket给UDP包加上TCP或ICMP header,进而绕过UDP屏蔽或QoS。 udp2raw tunnel,通过raw socket给UDP包加上TCP或ICMP header,进而绕过UDP屏蔽或QoS,或在UDP不稳定的环境下提升稳定性。可以有效防止在使用kcptun或者finalspeed的…

TiDB 7.5 LTS 发版丨提升规模化场景下关键应用的稳定性和成本的灵活性

互联网时代&#xff0c;数据的迅猛增长给数据库带来了可扩展性的挑战&#xff0c;Gen AI 带来的数据暴增更加剧了这种挑战。传统的数据分片已经不能承载新时代数据暴增的需求&#xff0c;更简单且具有前瞻性的方法则是采用原生分布式数据库来解决扩展性问题。在这种规模化场景的…

MYSQL一一外键约束

概念&#xff1a;外键用来让两张表的数据之间建立联系&#xff0c;从而保证数据的一致性和完整性 建立外键&#xff1a; ①这是在建立表的时候建立外键的方法 ②这是在建立完表之后建立外键的方法&#xff1a; 删除外键&#xff1a; 现在有一张员工表&#xff08;emp&#xf…

Linux基本指令(二)

目录 &#x1f4d5;前言 &#x1f4d5;echo 输出重定向&#xff08;>&#xff09; 追加重定向(>>) 输入重定向&#xff08;<&#xff09; &#x1f4d5;more &#x1f4d5;less &#x1f4d5;head &#x1f4d5;tail 查看大文本的中间部分 &#x1f4d5…

Java 8 中的 Stream 轻松遍历树形结构!

可能平常会遇到一些需求&#xff0c;比如构建菜单&#xff0c;构建树形结构&#xff0c;数据库一般就使用父id来表示&#xff0c;为了降低数据库的查询压力&#xff0c;我们可以使用Java8中的Stream流一次性把数据查出来&#xff0c;然后通过流式处理&#xff0c;我们一起来看看…

深入ArkUI:深入实战组件text和text input

文章目录 Text组件介绍Text组件的属性方法Text:文本显示组件4.3TextInput组件实战案例:图片宽度控制页面本文总结要点回顾在今天的课程中,我们将深入学习ArkUI提供的基础组件,着重探讨text和text input两个组件。 Text组件介绍 Text组件是一个用于显示文本的组件,其主要作…

自动化运维平台Spug本地部署结合内网穿透实现远程访问

文章目录 前言1. Docker安装Spug2 . 本地访问测试3. Linux 安装cpolar4. 配置Spug公网访问地址5. 公网远程访问Spug管理界面6. 固定Spug公网地址 前言 Spug 面向中小型企业设计的轻量级无 Agent 的自动化运维平台&#xff0c;整合了主机管理、主机批量执行、主机在线终端、文件…

【HTML】使用canvas添加水印

效果 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>添加水印</title><style>…

WebRTC原理与web端实战开发

什么是WebRTC webRtc(web real-time Communication) &#xff0c;旨在建立一个浏览器间实时通信的平台 谷歌开源跨平台&#xff08;android&#xff0c;IOS&#xff0c;windows&#xff0c;Linux&#xff09;实时传输&#xff08;提供强大的音视频引擎&#xff09; RTC涉及的…

SpringCloud(H版alibaba)框架开发教程---附源码 一

源码地址&#xff1a;https://gitee.com/jackXUYY/springboot-example 创建订单服务&#xff0c;支付服务&#xff0c;公共api服务&#xff08;共用的实体&#xff09;&#xff0c;eureka服务 1.cloud-consumer-order80 2.cloud-provider-payment8001 3.cloud-api-commons 4.…

Vue框架引入Axios

首先已经创建好了 Vue 框架&#xff0c;安装好了 node.js。 没有完成的可按照此博客搭建&#xff1a;搭建Vue项目 之后打开终端&#xff0c;使用命令。 1、命令安装 axios 和 vue-axios npm install axios --save npm install vue-axios --save2、package.json 查看版本 在 p…

Flink1.17实战教程(第五篇:状态管理)

系列文章目录 Flink1.17实战教程&#xff08;第一篇&#xff1a;概念、部署、架构&#xff09; Flink1.17实战教程&#xff08;第二篇&#xff1a;DataStream API&#xff09; Flink1.17实战教程&#xff08;第三篇&#xff1a;时间和窗口&#xff09; Flink1.17实战教程&…

新建虚拟环境并与Jupyter内核连接

第一步:在cmd里新建虚拟环境,shap38是新建的虚拟环境的名字 ,python=3.x conda create -n shap38 python=3.8第二步,安装ipykernel,打开anconda powershell prompt: 虚拟环境的文件夹位置,我的如图所示: 进入文件夹并复制地址: 输入复制的文件夹地址更改文件夹:…

跨境电商获客脚本必备功能有哪些?

随着全球化的加速和电子商务的兴起&#xff0c;跨境电商已经成为了企业拓展市场、提高收益的重要途径。 而在跨境电商运营中&#xff0c;获客脚本的作用不可忽视&#xff0c;本文将为您揭示跨境电商获客脚本必备的五大功能&#xff0c;帮助您在激烈的市场竞争中抢占先机。 一…

基于零和收益的DEA模型研究python实现

传统的DEA模型往往假设相关的是相互独立的,此时DEA模型只能计算出相对效率,无法进行效率的调整。如在碳排放分配问题上,碳排放总量保持不变,利用DEA模型只能计算出每个省份分配的相对效率,这在讨论固定资源下的碳排放配额分配问题时具有明显的局限性。在这种情况下,利用Z…

网络拓扑图怎么画最好?

画拓扑图的方式有很多&#xff0c;在线软件&#xff0c;Visio&#xff0c;PPT&#xff0c;都是方法。 问题是你要怎么从0到1&#xff0c;怎么样用拓扑图完美地把你的网络逻辑结构、思路呈现出来。 没经验的朋友真的不知道从哪里上手。 今天就给你来一篇绘制拓扑图详解&#…

Mybatis-plus实际开发步骤

一、Mybatis-plus基本配置 1、pom.xml导入Mybatis-plus依赖 <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.2</version> </dependency>2、application.yml文…

Local Binary Convolutional Neural Networks (LBCNN)

论文&#xff1a;https://arxiv.org/abs/1608.06049 代码&#xff1a;GitHub - juefeix/lbcnn.torch: Torch implementation of CVPR17 - Local Binary Convolutional Neural Networks http://xujuefei.com/lbcnn.html 摘要&#xff1a; 我们提出了局部二值卷积(LBC)&#x…

【Redis】八、哨兵模式

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