又解锁了一种OpenFeign的使用方式!

news2025/7/16 10:32:50

引言

Hello 大家好,这里是Anyin。

在关于OpenFeign那点事儿 - 使用篇 中和大家分享了关于OpenFeign在某些场景下的一些处理和使用方法,而今天Anyin再次解锁了OpenFeign的又一个使用场景,只能说真香。

在我们日常开发中,相信大家都会接触过对接第三方系统。对接第三方系统最烦人的工作可能就是刚开始对接的时候关于认证、加密、验签、JSON正反序列化等一系列的操作了。

我们知道OpenFeign它其实是一个http的客户端,主要的应用场景就是在微服务体系内进行微服务之间的相互调用;那么它是不是也可以实现第三方调用?

很明显是可以的!!!

需求分析

在验证我们的观点:OpenFeign可以实现第三方系统的调用之前,我们先找一个公开的第三方系统协议进行一波简单的需求分析吧。

这里我们使用中电联(中国电力企业联合标准)的协议文档为例。这里附上下载地址,有需要的同学可以自取。

中国电力企业联合标准

以下为协议文档对于密钥的要求。

通过查看协议文档,我们知道整个对接过程会设计到以下几个需求:

  1. 调用方式统一使用POST方式
  2. 传输格式使用JSON
  3. 传输过程业务数据需要进行加密
  4. 传输过程整包数据需要生成签名,因为服务端会进行验签,保证数据没有被篡改
  5. 在进行第三方调用的时候需要像调用其他本地的Service一样丝滑(行为一致)

业务实现

为了通过OpenFeign实现以上需求,我们首先定义一个配置类,用于自定义客户端的配置类。

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(CECOperatorProperties.class)
public class CECFeignClientConfig implements RequestInterceptor {
    @Autowired
    private CECOperatorProperties properties;
    @Override
    public void apply(RequestTemplate requestTemplate) {}    
}
复制代码
  1. 实现RequestInterceptor接口,这里是为了在进行认证拿到access_token之后,可以通过拦截器在header头放入对应的token信息
  2. 注入CECOperatorProperties属性,对于加解密、验签等操作需要的一些秘钥信息,从配置中心获取后,注入该属性类中
  3. @Configuration(proxyBeanMethods = false) 配置该类配置类,并且不会在RootApplicationContext当中注册,只会在使用的时候才会进行相关配置。

这里注意哈,在这个类配置的@Bean实例,只有在当前的FeignClient实例的ApplicaitonContext当中可以访问到,其他地方访问不到。具体可以看

关于OpenFeign那点事儿 - 源码篇

接着,我们需要2个基本的数据传输对象:Request 和 Response

@Data
public class CECRequest<T> {
    @JsonProperty("OperatorID")
    private String operatorID;
    @JsonProperty("Data")
    private T data;
    @JsonProperty("TimeStamp")
    private String timeStamp;
    @JsonProperty("Seq")
    private String seq;
    @JsonProperty("Sig")
    private String sig;
}
@Data
public class CECResponse<T> {
    private Integer Ret;
    private T Data;
    private String Msg;
}
复制代码

这里使用@JsonProperty的原因是协议文档字段的首字母都是大写的,而我们一般的Java字段都是驼峰,为了在进行JSON转换的时候避免无法正常转换。

然后,我们开始自定义编解码器。这里不得不推荐下Hutool 这个类库,是真的强大,因为涉及到的加解密和签名生成,都是现成的。真香!!!

编码器

@Slf4j
public class CECEncoder extends SpringEncoder {
    private final CECOperatorProperties properties;
    private final HMac mac;
    private final AES aes;
    public CECEncoder(ObjectFactory<HttpMessageConverters> messageConverters,
                      CECOperatorProperties properties) {
        super(messageConverters);
        this.properties = properties;
        this.mac = new HMac(HmacAlgorithm.HmacMD5,
                properties.getSigSecret().getBytes(StandardCharsets.UTF_8));
        this.aes = new AES(Mode.CBC, Padding.PKCS5Padding,
                properties.getDataSecret().getBytes(),
                properties.getDataIv().getBytes());
    }

    @Override
    public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {
        // 数据加密
        String data = this.getEncrypt(requestBody);
        CECRequest<String> req = new CECRequest<>();
        req.setData(data);
        req.setSeq("0001");
        req.setTimeStamp(DateUtil.formatDate(DateUtil.now(), DateEnum.YYYYMMDDHHMMSS));
        req.setOperatorID(properties.getOperatorID());
        // 签名计算
        String sig = this.getSig(req);
        req.setSig(sig.toUpperCase());
        super.encode(req, CECRequest.class.getGenericSuperclass(), request);
    }
    private String getEncrypt(Object requestBody){
        String json = JsonUtil.toJson(requestBody);
        return Base64.encode(aes.encrypt(json.getBytes()));
    }
    private String getSig(CECRequest<String> req){
        String str = req.getOperatorID() + req.getData() + req.getTimeStamp() + req.getSeq();
        return mac.digestHex(str);
    }
}
复制代码

可以看到,我们的编码器其实是继承了SpringEncoder,因为在最终编码之后,还是需要转换为JSON发送给服务端,所以在继承SpringEncoder之后,构造器还需要注入ObjectFactory<HttpMessageConverters>的实例。另外,在构造器我们也初始化了HMacAES两个实例,一个为了生成签名,一个为了加密业务数据。

encode方法,我们把传递进来的requestBody包装了下,先对其进行加密,然后放在CECRequest实例的data字段内,并且生成对应的签名,最终请求服务端的时候是一个CECRequest实例的JSON化的结果。

可能有人会疑惑,为什么这里的requestBody就直接是业务数据了,而不是CECRequest<T>实例? 想想我们的第5点需求:在进行第三方调用的时候需要像调用其他本地的Service一样丝滑(行为一致)。为了实现这个需求,我们不会把非业务的参数暴露给业务调用放,而是在编解码的过程中进行处理。

解码器

@Slf4j
public class CECDecoder extends SpringDecoder {
    private final AES aes;
    public CECDecoder(ObjectFactory<HttpMessageConverters> messageConverters,
                      CECOperatorProperties properties) {
        super(messageConverters);
        this.aes = new AES(Mode.CBC, Padding.PKCS5Padding,
                properties.getDataSecret().getBytes(),
                properties.getDataIv().getBytes());
    }
    @Override
    public Object decode(Response response, Type type) throws IOException, FeignException {
        CECResponse<String> resp = this.getCECResponse(response);
        // TODO 应该做对应的异常判断然后抛出异常
        String json = this.aes.decryptStr(resp.getData());
        Response newResp = response.toBuilder().body(json, StandardCharsets.UTF_8).build();
        return super.decode(newResp, type);
    }
    private CECResponse<String> getCECResponse(Response response) throws IOException{
        try (InputStream inputStream = response.body().asInputStream()) {
            String json = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
            TypeReference<CECResponse<String>> reference = new TypeReference<CECResponse<String>>() {};
            return JSONUtil.toBean(json, reference.getType(), true);
        }
    }
}
复制代码

解码器会比较简单,只需要进行数据的解密即可。所以我们从Response中拿到对应的JSON字符串,然后通过反序列化拿到CECResponse实例,接着做对应的异常判断(这里我的代码暂时未实现),然后再做数据的解码,拿到真正的业务数据的JSON字符串,最后通过OpenFeign提供的toBuilder方法重新构造一个新的Response实例交给SpringDecoder进行下一步的处理。

下一步,我们把编解码器注册到配置类中。完整的配置类信息如下

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(CECOperatorProperties.class)
public class CECFeignClientConfig implements RequestInterceptor {
    @Autowired
    private CECOperatorProperties properties;
    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
    @Bean
    public Encoder encoder(){
        return new CECEncoder(messageConverters, properties);
    }
    @Bean
    public Decoder decoder(){
        return new CECDecoder(messageConverters, properties);
    }

    @Override
    public void apply(RequestTemplate requestTemplate) {
        // TODO 添加Token
    }
}
复制代码

完整的配置类会注入从RootApplicationContext中拿到的ObjectFactory<HttpMessageConverters>实例,另外再多配置了一个日志实例Logger.Level,用于在debug的时候打印请求的具体日志。

最后,我们来测试下我们的程序是否正常。简单测试用例如下:

@Slf4j
public class CECTest extends BaseTest{
    @Autowired
    private CECTokenService tokenService;
    @Autowired
    private CECStationService stationService;
    @Autowired
    private CECOperatorProperties properties;

    @Test
    public void test(){
        QueryTokenReq req = new QueryTokenReq();
        req.setOperatorID(properties.getOperatorID());
        req.setOperatorSecret(properties.getOperatorSecret());
        QueryTokenResp resp = tokenService.queryToken(req);
        log.info("resp: {}", JsonUtil.toJson(resp));
    }
}
复制代码

看到吧,是不是和调用本地的Service一样丝滑? 只需要构造对应的入参,即可返回对应的出参,无需关心加密、签名等烦人的操作。相关日志如下:

 

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

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

相关文章

SSM框架-MyBatis基础

1. MyBatis简介 1.1 MyBatis历史 MyBatis最初是Apache的一个开源项目iBatis&#xff0c;2010年6月这个项目由Apache Software Foundation迁移到了Google Code。随着开发团队转投Google Code旗下&#xff0c;iBatis3.x正式更名为MyBatis。代码于2013年11月迁移到Github。 iBa…

Pipelines in Shell

本篇文章内容需要读者知道 shell 的一些语法和作用&#xff0c;知道 shell 的用途&#xff0c;和一些基本的用法。 这里可以查看原文&#xff1a;Pipelines in Shell 学习 shell 脚本必须要理解 pipeline 的概念&#xff0c;知道 command 的输入&#xff08;input&#xff09;和…

编译概念总结

一个很笨很笨的人的编译自救笔记。 1 程序设计语言 程序设计语言用于书写计算机程序的语言。语言的基础是一组记号和一组规则。根据规则由记号构成的记号串的总体就是语言。在程序设计语言中&#xff0c;这些记号串就是程序。 程序设计语言由三个方面的因素&#xff0c;语法…

[附源码]SSM计算机毕业设计商场日常维修管理系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

深度学习(19):nerf论文公式理解

注&#xff1a;有问题欢迎评论留言&#xff0c;但尽量不要喷呀。 1. nerf论文第四章翻译如下&#xff1a; 我们的5D神经辐射场将场景表示为空间任意点的体积密度和定向发射辐射&#xff08;directional emitted radiance&#xff09;。我们使用经典体积渲染&#xff08;class…

嗯哦哎辟 2022 游寄

虽然上次不是假的&#xff0c;但这次是真的寄了。 Day 0 虽然是南京本地人&#xff0c;但因疫情原因&#xff0c;晚上决定去住了酒店。 看了一眼考场&#xff0c;感觉位置小得离谱。不愧是 NOI 2022 团体总分第十的“强省”江苏。 刚开始去了 409&#xff0c;发现房间里一股…

C++ 基础入门

1、变量 作用&#xff1a;给一段指定的内存空间起名&#xff0c;方便操作这段内存。 2、常量 作用&#xff1a;用于记录程序中不可更改的数据 C中定义常量的两种方式&#xff1a; #define 宏常量&#xff1a; #define 常量名 常量值const修饰的变量&#xff1a;const数据类型 …

26k Star, 理解Git太轻松了。。。

程序员宝藏库&#xff1a;gitee.com/sharetech_lee/CS-Books-Store Git是目前使用比较广泛一款版本控制工具&#xff0c;从事开发工作&#xff0c;很难绕开Git。 因此&#xff0c;关于如何快速学习Git使用一直都是一个经久不衰的话题。 前不久我在另外一篇文章中曾提到Git对初…

【药材识别】基于色差色温特征结合SVM实现药材炮制程度判断系统附GUI界面

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

十九种卷积

参考文章:一文看尽深度学习中的20种卷积(附源码整理和论文解读) - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/381839221 一、原始卷积(Vanilla Convolution) CNNs中的卷积,也称为滤波器,是由一组具有固定窗口大小且带可学习参数(learnable paramerters)的卷积核所组…

Java之IO流详解(一)——File类

一、File类创建文件 方法说明public boolean createNewFile()当具有该名称的文件不存在时&#xff0c;创建一个由该抽象路径命名的新空文件public boolean mkdir()创建由此抽象路径命名的目录public boolean mkdirs()创建由此抽象路径命名的目录&#xff0c;包括任何必须但不存…

Go常见错误第15篇:interface使用的常见错误和最佳实践

前言 这是Go常见错误系列的第15篇&#xff1a;interface使用的常见错误和最佳实践。 素材来源于Go布道者&#xff0c;现Docker公司资深工程师Teiva Harsanyi。 本文涉及的源代码全部开源在&#xff1a;Go常见错误源代码&#xff0c;欢迎大家关注公众号&#xff0c;及时获取本…

加载配置文件内容利用反射动态创建对象和调用方法(开闭原则的体现)

反射的应用&#xff1a;根据配置文件来创建对象和调用方法 需求&#xff1a;1&#xff0c;根据配置文件re.properties 指定的信息&#xff0c;创建对象并调用方法 classfullpathsrc.com.liu.Cat methodhi 即通过外部文件配置&#xff0c;在不修改源码的情况下&#xff0c;来控…

GAN详解

前言 GAN是当今作为火热的生成式算法&#xff0c;由Ian Goodfellow&#xff0c;Yoshua Bengio等人在2014年提出的&#xff0c;Yan LeCun表示道GAN是“adversarial training is the coolest thing since sliced bread”。它使用两个神经网络&#xff0c;将一个神经网络与另一个…

ArcGIS中ArcMap栅格图层0值设置为NoData值的简便方法

本文介绍在ArcMap软件中&#xff0c;将栅格图层中的0值或其他指定数值作为NoData值的方法。 在处理栅格图像时&#xff0c;有时会发现如下图所示的情况——我们对某一个区域的栅格数据进行分类着色后&#xff0c;其周边区域&#xff08;即下图中浅蓝色的区域&#xff09;原本应…

C语言学习记录(十五)C预处理器和C库

文章目录一、C预处理器1.1 翻译程序1.2 明示常量&#xff1a;#define1.3 在#define中使用参数1.4 文件包含&#xff1a;#include1.5 其他指令1.5.1 #undef指令1.5.2 从C预处理角度看已定义1.5.3 条件编译1.5.3.1 #ifdef、#else和#endif指令1.5.3.2 #ifndef1.5.3.3 #if和elif指令…

Gof23-创建型-工厂-单例-抽象工厂-建造-原型以及UML的绘制

创建型的设计模式工厂模式单例模式抽象工厂建造者模式原型模式UML图形的绘制工厂模式 工厂模式 Factory Pattern 适用的场景&#xff1a;统一的接口作为统一的零件&#xff0c;实现类作为零件的组合&#xff0c;将实例产品类的生产交给工厂&#xff0c;用户只需要面对工程提取…

XML的创建和读取

rapidxml是一个快速的xml库&#xff0c;由C模板实现的高效率xml解析库&#xff0c;同时也是boost库的property_tree的内置解析库。 当使用rapidxml时&#xff0c;只需要把rapidxml.hpp 、 rapidxml_print.hpp 和 rapidxml_utils.hpp 三个文件拷贝到你的工程目录下&#xff0c;就…

Pytorch中KL loss

1. 概念 KL散度可以用来衡量两个概率分布之间的相似性&#xff0c;两个概率分布越相近&#xff0c;KL散度越小。 上述公式表示P为真实事件的概率分布&#xff0c;Q为理论拟合出来的该事件的概率分布。D(P||Q)&#xff08;P拟合Q&#xff09;和D(Q||P)&#xff08;Q拟合P&…

ajax之Content-Type示例

参考资料: Content-Type详解【SpringBoot】SpringBoot接收请求的n种姿势 目录前期准备0. Content-Type概念解释1. application/x-www-form-urlencoded1.1 form表单示例1.2 jQuery的ajax示例2. application/json2.1 指定contentType为json,不使用RequestBody接收2.2 不指定cont…