【手写实现一个简单版的Dubbo,深刻理解RPC框架的底层实现原理】
手写实现一个简单版的Dubbo,深刻理解RPC框架的底层实现原理
- RPC框架简介
- 了解Dubbo的实现原理
- 服务暴露
- 服务引入
- 服务调用
- 手写实现一个简单版的Dubbo
- 服务暴露
- ServiceBean
- ProxyFactory#getInvoker
- Protocol#export
- RegistryProtocol#export
- 服务引入
- RegistryProto#refer
- DubboProtocol#refer
- 服务调用
- 服务消费者
- 服务提供者
- 与Spring对接
- 代码
我越来越觉得学习一个开源框架或者中间件的底层原理,最好的方法不是看源码,而是自己尝试去写一个。之前看过许多开源框架和中间件的源码,但是随着时间的推移,多多少少都有点遗忘了,遗忘之后又花时间去把它捡一捡,于是就在这捡起又遗忘,遗忘又捡起这样来来回回的折腾中浪费了许多时间。
Dubbo的源码我很早以前就看过了,而且看了不止一遍,现在对于Dubbo的底层原理只记得个大概,至于源码中的细节,已经忘得差不多了。最近想起之前学习过Dubbo源码,为了加深印象,于是乎自己动手写了一个。这自己动手写跟只用眼睛看,效果还真不一样,因此写成文章分享出来。
RPC框架简介
在以前都是单体应用的年代,是不需要RPC框架的,那时候还不知道RPC是什么,所有的数据都是来自于本地的数据库,然后全都是本地的方法调用。

但是随着业务的发展,访问量的不断增加,原先的单体应用已经不能满足需求,于是要做应用拆分,原先的单体应用被拆成了多个服务,然后就出现了服务间调用的问题。

这样复杂度就一下子上来了,如何解决服务间调用的问题呢?我们可以自己写代码,通过http的方式去访问对方,或者通过Socket连上对方的系统进行通信,发送调用的方法名和调用参数等信息。但是这种方式有点笨,因为这种网络通信的代码一般都是重复的,而且也容易出错。
那么有没有什么办法可以让远程调用变成像之前单体应用那样就调一个本地方法,就能获取到返回结果呢?那就是引入一个RPC框架,RPC框架封装了远程调用的底层逻辑,屏蔽掉了网络通信的过程,让API调用工程师们简单调一下方法,就可以获取到远程系统返回的方法调用结果,于是这些API调用工程师们就觉得自己很牛B了。

我们当然不能当这种API调用工程师,要不然35岁之后就要强势加入骑士军团了。

其实RPC框架的原理并没有多复杂。
比如现在有一个 UserService接口,在以前还是单体应用的时候,Spring给我们注入的是一个实现类,我们调用这个接口的方法时就可以调用UserServiceImpl实现类。

而现在这个实现类在别的系统上,我们拿不到,于是RPC框架给我们注入的是一个代理对象,这个代理对象也实现了这个UserService接口,通过这个代理对象可以访问到对方系统的UserServiceImpl。然后RPC框架通过一个stub存根封装了网络通信的逻辑,当我们调用UserService接口的某个方法时,实际上是通过代理对象调用到了底层的存根类,stub存根再把我们调用的方法和参数封装为网络协议包,发送到对端系统。对端系统也有一个stub存根,接收到发送过来的协议包后,解析出要调用的方法和参数,交给UserServiceImpl去处理,然后再把处理结果通过stub存根返回给我们本地的存根,我们本地的存根再把结果返回给我们。

但是A系统怎么知道B系统的ip地址和端口好呢?如果是在A系统上配死那就太low了,一旦B系统改了ip地址或端口,A系统就要改配置重启。因此,还需要一个注册中心存储B系统发布的ip地址和端口,A系统从注册中心拉取B系统的ip地址和端口。此时B系统就是服务提供者,把ip地址和端口注册到注册中心这个动作就是服务暴露,而A系统就是服务消费者,从注册中心拉取服务提供者的ip地址端口等信息这个动作就是服务引入,A系统调用B系统这个动作就叫服务调用。

这样,一个RPC调用框架就形成了。
了解Dubbo的实现原理
但是,理解到这一层,还远远不够,毕竟别人可以面试前把这些背一背,面试的时候忽悠面试官。于是,我们要阅读源码,对开源框架有更深一层的理解,这样才能让面试官从一众的渣渣中把我们区分出来。
Dubbo是众多的RPC框架中用的比较多的一个,我们来参考参考Dubbo的原理,也是分为服务暴露、服务引入、服务调用三块。
服务暴露
首先我们写的类是这样子的:

UserServiceImpl上声明了@Service注解(这个是Dubbo自己定义的@Service注解,不是Spring的那个),就代表我们要把这个UserServiceImpl暴露出来供服务消费者调用。
Dubbo要扫描它,然后把它封装成ServiceBean,ServiceBean是Dubbo封装的一个用来做服务暴露的实现类,它实现了Spring的ApplicationListener<ContextRefreshedEvent>接口,会监听ContextRefreshedEvent容器刷新完成事件触发服务暴露,而ServiceBean的export方法就是服务暴露的入口。

Dubbo在服务暴露中,一共做了三件事:
- 把我们的UserServiceImpl对象包装成Invoker,再把Invoker包装成Exporter,再把Exporter放入一个map中,key是根据实现类计算出来的一个唯一的serviceKey。这一步相当于是本地注册,方便后面接收到远程调用请求时,根据请求信息计算出serviceKey,找到目标实现类。
- 开启Netty服务端,用于接收远程调用请求。
- ip地址、端口、协议等服务信息注册到注册中心,这些信息注册到注册中心后,消费方就可以从注册中心中获取到服务提供者的信息了。

服务引入
至于服务引入,我们写的消费者类一般是这样:

属性上声明了这个@Reference注解,表示这个属性需要通过Dubbo的服务引入来进行属性注入。
那么,Dubbo就是扫描所有被@Reference注解修饰的属性,进行属性注入。Dubbo这里处理属性注入的逻辑与Spring处理@Autowired的逻辑大体是相同的,这里先不用管(后面会讲解)。扫描到的属性,通过ReferenceBean的get()方法触发服务引入,返回一个代理对象,注入到该属性中。

而服务引入里面做了些啥呢?其实就是从注册中心中获取服务提供者的ip端口等信息,开启NettyClient连接上服务提供者,然后封装到一个Invoker中,这个Invoker就等同于上面说的存根类。然后通过动态代理返回一个代理对象,这个代理对象会调用Invoker进行远程方法调用。

服务调用
服务调用的逻辑自然是被隐藏在了Invoker里面。服务间通过Netty进行网络通信,服务消费者把调用的方法名,接口的类全限定名,方法参数等封装成一个Invocation对象,然后序列化成二进制,通过Netty发送给服务提供者,服务提供者的Netty接收到服务消费者发来的数据时,会反序列化成Invocation对象,根据服务消费者提供的信息,找到具体的实现类进行处理,处理结果再由Netty返回给服务消费者。

最终整体流程就是这样:

没有看过源码的,也可以记一下这张图,然后面试的时候喷给面试官,也能拿个60分。
按照这个思路,我们就可以动手写代码了。
手写实现一个简单版的Dubbo
我们写代码的顺序还是按照服务暴露、服务引入、服务调用的顺序来,但是我们实现的迷你版Dubbo是要与Spring对接的,所以最后还要实现与Spring对接的逻辑。
这里要注意,下面还是以一边讲解一边画图的方式进行,不会贴代码。因为如果贴代码的话,这文章没个几万字是结束不了的。想看代码的可以到代码仓去下载,文末会附上代码仓的地址。
服务暴露
ServiceBean
服务暴露的逻辑从ServiceBean开始,我们就先从ServiceBean开始实现我们的迷你版Dubbo。
我们的ServiceBean也是实现ApplicationListener<ContextRefreshedEvent>接口,在onApplicationEvent(ContextRefreshedEvent event)方法中调用export()方法触发服务暴露。

export()方法首先要读取注册中心的配置,Dubbo是通过RegistryConfig类型封装注册中心的配置的,我们也定义一个RegistryConfig类封装注册中心的配置,这里通过RegistryConfig获取到注册中心的ip地址和端口等信息。
Dubbo是通过URL对象去封装服务暴露的信息,比如协议、ip地址、端口、访问路径(调哪个类的哪个方法)、方法参数等信息,然后注册到注册中心的也是这个URL对象转成的url格式的字符串。我们这里也定义一个URL对象去存这些信息,我们组装我们的URL对象。
URL中包含的协议、端口等信息,可以通过ProtocolConfig进行配置,然后从ProtocolConfig中取出放入到URL对象中。
封装好URL对象后,接下来就是通过ProxyFactory(代理工厂)生成服务提供方的Invoker,这个Invoker保存了真正的服务实现类。
最后就是通过Protocol对象的export方法进行服务暴露,这里也是参考Dubbo的,Dubbo真正的服务暴露逻辑是封装在Protocol的export方法里面的,本地注册、开启Netty、注册相关信息到注册中心等动作都是在这里面进行。我们也定义一个Protocol接口。
这样看来,ServiceBean需要RegistryConfig和ProtocolConfig这两个对象,我们可以通过ApplicationContext去获取,因此ServiceBean需要实现ApplicationContextAware这个接口,通过这个接口获取到ApplicationContext,然后ServiceBean还要实现InitializingBean接口,在InitializingBean接口的afterPropertiesSet()方法里面通过ApplicationContext获取RegistryConfig和ProtocolConfig这两个对象。
这里的ApplicationContextAware、InitializingBean、ApplicationListener等接口都是Spring的接口,不熟悉的可以去补一下Spring的知识。

ProxyFactory#getInvoker
ProxyFactory.getInvoker(ref, this.interfaceClass, url) 这一步是封装具体实现类ref为Invoker的。这个Invoker的invoke方法会从Invocation中获取方法名、方法参数类型和方法参数,然后通过反射调用具体实现类ref。

Protocol#export
protocol.export(registryURL, invoker);这一步是真正进行服务暴露的,但是这里的protocol其实是一个代理类,Dubbo通过他自己实现的SPI机制,会调用到具体的Protocol实现类。我们这里也是一个代理类,使用的时JDK的动态代理,但是我们就不实现自己的SPI机制了,我们就使用Java的SPI机制。在InvocationHandler的invoke方法中通过Java的SPI机制加载所有的实现类,循环遍历进行匹配,匹配逻辑是看URL中的protocol属性,也就是url中的协议,然后反射调用匹配到的具体实现类。

现在的整体流程就走到这里:

RegistryProtocol#export
这里的URL的protocol属性是registry,因此这里匹配到的是RegistryProtocol类型的Protocol,于是会调用到RegistryProtocol的export方法。
RegistryProtocol的export方法又会再次调用protocol的export方法,这里的protocol依然是代理对象,但是这次的URL的protocol属性是dubbo,因此会调用到DubboProtocol的export方法。
DubboProtocol的export方法会把Invoker包装成Exporter,用接口类全限定名作为key,Exporter作为value,放入到一个map,这样当接收到远程调用请求时,就可以通过接口名找到对应的Invoker,进而调用里面的实现类。
DubboProtocol的export方法接下来会开启Netty服务端,用于接收远程调用请求。
DubboProtocol的export方法结束后返回到RegistryProtocol的export方法,接下来会调用注册中心的客户端把服务暴露信息注册到注册中心,比如注册中心是Zookeeper,则通过Zookeeper的客户端把服务暴露信息发布到Zookeeper注册中心。这里注册中心的具体类型还是通过动态代理加上Java的SPI机制来动态进行匹配的,而注册到注册中心的信息也是url格式的。

到这里,服务暴露的流程就结束了:

服务引入
服务引入的入口是在ReferenceBean的get()方法,我们ReferenceBean的get()的会返回一个代理对象,注入到被@Reference注解修饰的属性上。这里先不用管ReferenceBean的get()方法如何被调用到,我们先完成ReferenceBean的get()方法的服务引入逻辑。
ReferenceBean的get()方法第一步也是通过RegistryConfig获取到注册中心的ip和端口等信息。
然后调用Protocol的refer方法进行服务引入,这个方法会返回一个Invoker对象,这个Invoker对象的invoke方法会通过Netty向远端发起远程调用。
最后通过ProxyFactory的getProxy方法把Invoker封装到一个代理对象中,这里我们还是使用JDK的动态代理,InvocationHandler的invoke方法会调用Invoker的invoke方法。

RegistryProto#refer
Protocol的refer方法是真正进行服务引入的方法,这里还是通过动态代理,首先走到RegistryProto的refer方法
RegistryProto的refer方法返回的Invoker对象是比较复杂的,有个几层的嵌套。
考虑到服务提供者有可能是以集群的形式部署的,因此我们这里定义了一个ClusterInvoker类型的Invoker与之对应。ClusterInvoker包装了一个RegistryDirectory对象,它是一个服务目录,里面保存了一个List<Invoker>,这里的一个Invoker对应一个服务提供者。这样我们的服务就具备一定的容错能力,可以通过负载均衡选出一个Invoker,一个调不通,可以换下一个。

那List<Invoker>里面的Invoker是怎么来的呢?
首先,我们会监听注册中心,一旦服务提供者上下线导致注册中心中的信息有变动,我们会收到通知,收到通知后我们重新从注册中心中查询服务提供者的url。同时在创建RegistryDirectory对象时,RegistryDirectory的构造方法也会主动去注册中心查一次。
然后,我们定义了一个NotifyListener接口,我们的RegistryDirectory实现了NotifyListener接口,NotifyListener的notify方法接收从注册中心查询回来的url,循环调用protocol.refer(url, serviceType)方法。protocol.refer(url, serviceType)会调用到DubboProtocol的refer方法,DubboProtocol的refer方法就会返回与服务提供者对应的一个Invoker。

DubboProtocol#refer
DubboProtocol的refer方法会从参数url中解析出服务提供者的ip地址和端口号,开启Netty客户端,连接到服务提供者,然后包装成一个Invoker返回。

那么服务引入的整体逻辑就是这样:

服务调用
服务消费者
服务消费者的代理对象会通过InvocationHandler的invoke方法,调用到ClusterInvoker的invoke方法。
ClusterInvoker的invoke方法调用RegistryDirectory的list方法获取List<Invoker>,然后通过负载均衡算法选出一个进行调用,调用失败则切换下一个。
负载均衡选出一个Invoker后,调用Invoker的invoke方法就会进入到DubboInvoker的invoke方法。DubboInvoker的invoke方法把接口类全限定名、方法名、方法参数类型、方法参数等信息包装成Invocation对象,调用Netty客户端发送请求。
Netty客户端的编解码器会把Invocation对象序列化成二进制,然后发送到网络。
因为Netty是异步的,因此这里要创建一个Future对象然后绑定一个id,发送请求时把这id带上,服务提供者处理完后返回处理结果时同时返回这个id,服务消费者就能通过id拿到Future,设置返回结果到这个Future上,那么当时发送请求的线程就能通过这个Future拿到返回结果。

服务提供者
服务提供者的Netty接收到请求后,反序列成Invocation对象,根据Invocation中的接口类全限定名,从map中取出Exporter。然后从Exporter中取出Invoker,调用Invoker的invoke方法,Invoker的invoke方法从Invocation取出方法名,方法参数类型、方法参数,反射调用具体实现类,具体实现类处理完成后返回的结果,带上服务消费者传过来的id,一起返回给服务提供者。

与Spring对接
这些都写好以后,一个RPC框架大体就形成了,但是我们还差最后一步,就是与Spring对接。
我们参考Dubbo,定义了一个@EnableDubbo注解,@EnableDubbo注解通过@Import注解Spring容器导入一个DubboComponentScanRegistrar类,DubboComponentScanRegistrar实现了Spring的ImportBeanDefinitionRegistrar接口,重写了ImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法,DubboComponentScanRegistrar的registerBeanDefinitions方法会被Spring回调到。DubboComponentScanRegistrar的registerBeanDefinitions方法往Spring容器中注入两个bean,一个是ServiceAnnotationBeanPostProcessor类型,实现了Spring的BeanDefinitionRegistryPostProcessor接口,用于扫描@Service注解然后注册BeanDefinition到Spring容器中;另一个是ReferenceAnnotationBeanPostProcessor类型,是一个Bean后置处理器,用于扫描@Reference注解进行服务引入的。

ServiceAnnotationBeanPostProcessor通过Spring的ClassPathBeanDefinitionScanner类扫描出@Service注解修饰的类的BeanDefinition,然后创建一个ServiceBean类型的BeanDefinition,配置相应的interfaceClass和ref等属性,然后注册到容器中。
ClassPathBeanDefinitionScanner是Spring提供的一个用来扫描指定包路径获取BeanDefinition的工具类。

ReferenceAnnotationBeanPostProcessor参考Spring处理@Autowired的逻辑,ReferenceAnnotationBeanPostProcessor继承了InstantiationAwareBeanPostProcessorAdapter,并重写了InstantiationAwareBeanPostProcessorAdapter的postProcessPropertyValues方法,ReferenceAnnotationBeanPostProcessor的postProcessPropertyValues方法会被Spring回调到,给当前bean进行属性注入,然后postProcessPropertyValues方法中收集当前bean中被@Reference注解修饰的属性,创建ReferenceBean,对ReferenceBean进行相应配置,调用ReferenceBean的get()方法返回一个代理对象,注入到该属性中。
InstantiationAwareBeanPostProcessorAdapter是Spring提供的一个Bean后置处理器,可以通过继承InstantiationAwareBeanPostProcessorAdapter并重写postProcessPropertyValues方法定制我们的属性注入逻辑。

代码
到这里,我们的RPC框架就大功告成了。基本上是参考Dubbo来写的,保留了Dubbo的核心逻辑,又去掉了一些细枝末节,比起原来的Dubbo代码简化了许多,但是确实能够帮助我们比较深刻的理解Dubbo的原理和逻辑。
本文章涉及的所有代码,都上传到git代码仓库了,由于篇幅有限,这些代码就不贴到本篇文章中了,有兴趣的可以到代码仓库下载下来看一下。
git代码仓库地址:https://gitcode.net/weixin_43889578/mini-dubbo

相关文章:
【手写实现一个简单版的Dubbo,深刻理解RPC框架的底层实现原理】
手写实现一个简单版的Dubbo,深刻理解RPC框架的底层实现原理 RPC框架简介了解Dubbo的实现原理服务暴露服务引入服务调用 手写实现一个简单版的Dubbo服务暴露ServiceBeanProxyFactory#getInvokerProtocol#exportRegistryProtocol#export 服务引入RegistryProto#referD…...
计数问题+约瑟夫问题(map)
目录 一、计数问题 二、约瑟夫问题 一、计数问题 #include<iostream> #include<map> using namespace std; int main() {int n,x;cin>>n>>x;map<int,int>m;for(int i1;i<n;i){if(i>1 && i<10){m[i];}else{int temp i;while (…...
Maven聚合项目发布至私服指定模块
无论是从事框架开发工作还是公共服务模块开发,为了解决通用性问题,常常需要发布一些依赖组件至maven私服。然而通常我们得maven工程都是由多个模块组成得聚合工程(一个父工程下有多个模块)。 这个时候可能会面临两个窘境…...
SpringCloud 微服务全栈体系(十六)
第十一章 分布式搜索引擎 elasticsearch 六、DSL 查询文档 elasticsearch 的查询依然是基于 JSON 风格的 DSL 来实现的。 1. DSL 查询分类 Elasticsearch 提供了基于 JSON 的 DSL(Domain Specific Language)来定义查询。常见的查询类型包括࿱…...
「快学Docker」监控和日志记录容器的健康和性能
「快学Docker」监控和日志记录容器的健康和性能 1. 容器健康状态监控2. 性能监控3. 日志记录几种采集架构图 4. 监控工具和平台cAdvisor(Container Advisor)PrometheusGrafana 5. 自动化运维 1. 容器健康状态监控 方法1:需要实时监测容器的运…...
midjourney过时了?如何使用基于LCM的绘图技术画出你心中的画卷。
生成 AI 艺术在近年来迅速发展,吸引了数百万用户。然而,传统的生成 AI 艺术需要等待几秒钟或几分钟才能生成,这对于快节奏的现代社会来说并不理想。 近日,中国清华大学和 AI 代码共享平台 HuggingFace 联合开发了一项新的机器学习…...
【代码随想录】算法训练计划28
回溯 1、子集 题目: 给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 输入:nums [1,2,3] 输出:[[],[1],[2…...
量化交易:筹码理论的探索-筹码分布计算的实现
前言 很多朋友习惯了同花顺、大智慧等看盘软件,经常问到筹码分布如何计算。 说起来筹码分布的理论在庄股时代堪称是一个划时代产品,虽然历经level2数据、资金流统计、拆单算法与反拆单算法等新型技术的变革,庄股时代也逐渐淡出市场…...
常用Redis的键命令参考
一、DEL DEL key [key …] 删除给定的一个或多个 key 。 不存在的 key 会被忽略。 #删除单个键127.0.0.1:6379> set name zhangsan OK 127.0.0.1:6379> del name (integer) 1# 删除一个不存在的 key, 失败,没有 key 被删除127.0.0.1:6379> E…...
Lombok @With 的纯弊端及如何避免
由于是第一篇写关于 Lombok 的日志,所以有些不情愿去开门见山直接触及 With, 而要先提一提本人对 Lombok 的接触过程。 两三年之前写 Java 代码一直都是全手工打造。一个数据类,所有必须的 setter/getter, toString, hashcode() 等全体现在源代码中&…...
C语言每日一题(38)无重复字符的最长字串
力扣 3 无重复字符的最长字串 题目描述 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: s "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。示例 2: 输入: s…...
Azure Machine Learning - Azure可视化图像分类操作实战
目录 一、数据准备二、创建自定义视觉资源三、创建新项目四、选择训练图像五、上传和标记图像六、训练分类器七、评估分类器概率阈值 八、管理训练迭代 在本文中,你将了解如何使用Azure可视化页面创建图像分类模型。 生成模型后,可以使用新图像测试该模型…...
PaddleOCR学习笔记
Paddle 功能特性 PP-OCR系列模型列表 https://github.com/PaddlePaddle/PaddleOCR#%EF%B8%8F-pp-ocr%E7%B3%BB%E5%88%97%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8%E6%9B%B4%E6%96%B0%E4%B8%AD PP-OCR系列模型列表(V4,2023年8月1日更新) 配置文…...
安卓用SQLite数据库存储数据
什么是SQLite? SQLite是安卓中的轻量级内置数据库,不需要设置用户名和密码就可以使用。资源占用较少,运算速度也比较快。 SQLite支持:null(空)、integer(整形)、real(小…...
MMFN-AL
MMFN means ‘multi-modal fusion network’ 辅助信息 作者未提供代码...
7、独立按键控制LED状态
按键的抖动 对于机械开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个开关在闭合时不回马上稳定地接通,在断开时也不会一下子断开,所以在开关闭合及断开的瞬间会伴随一连串的抖动 #include <REGX52.H…...
香蕉派BPI-M4 Zero单板计算机采用全志H618,板载2GRAM内存
Banana Pi BPI-M4 Zero 香蕉派 BPI-M4 Zero是BPI-M2 Zero的最新升级版本。它在性能上有很大的提高。主控芯片升级为全志科技H618 四核A53, CPU主频提升25%。内存升级为2G LPDDR4,板载8G eMMC存储。它支持5G WiFi 和蓝牙, USB接口也升级为type-C。 它具有与树莓派 …...
微信小程序内部跳到外部小程序
要在微信小程序中跳转到外部小程序,可以使用wx.navigateToMiniProgram函数。以下是一个示例: wx.navigateToMiniProgram({appId: 外部小程序的appId,path: 外部小程序的路径,extraData: {id: xxx},success(res) {// 跳转成功} })在这个示例中࿰…...
Spring Boot中设置文件上传大小限制
在Spring Boot中,可以通过以下步骤来设置上传文件的大小: 在application.properties或application.yml文件中,添加以下配置: 对于application.properties: spring.servlet.multipart.max-file-size128MB spring.se…...
8、独立按键控制LED显示二进制
独立按键控制LED显示二进制 #include <REGX52.H>void Delay(unsigned int xms) //12.000MHz {unsigned char i, j;while(xms--){i 2;j 239;do{while (--j);} while (--i);} }void main() {//数据类型刚好是8位与51单片机IO口寄存器位数相同(默认高电平&am…...
业务系统对接大模型的基础方案:架构设计与关键步骤
业务系统对接大模型:架构设计与关键步骤 在当今数字化转型的浪潮中,大语言模型(LLM)已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中,不仅可以优化用户体验,还能为业务决策提供…...
云原生核心技术 (7/12): K8s 核心概念白话解读(上):Pod 和 Deployment 究竟是什么?
大家好,欢迎来到《云原生核心技术》系列的第七篇! 在上一篇,我们成功地使用 Minikube 或 kind 在自己的电脑上搭建起了一个迷你但功能完备的 Kubernetes 集群。现在,我们就像一个拥有了一块崭新数字土地的农场主,是时…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
安卓基础(aar)
重新设置java21的环境,临时设置 $env:JAVA_HOME "D:\Android Studio\jbr" 查看当前环境变量 JAVA_HOME 的值 echo $env:JAVA_HOME 构建ARR文件 ./gradlew :private-lib:assembleRelease 目录是这样的: MyApp/ ├── app/ …...
HarmonyOS运动开发:如何用mpchart绘制运动配速图表
##鸿蒙核心技术##运动开发##Sensor Service Kit(传感器服务)# 前言 在运动类应用中,运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据,如配速、距离、卡路里消耗等,用户可以更清晰…...
NPOI Excel用OLE对象的形式插入文件附件以及插入图片
static void Main(string[] args) {XlsWithObjData();Console.WriteLine("输出完成"); }static void XlsWithObjData() {// 创建工作簿和单元格,只有HSSFWorkbook,XSSFWorkbook不可以HSSFWorkbook workbook new HSSFWorkbook();HSSFSheet sheet (HSSFSheet)workboo…...
关于easyexcel动态下拉选问题处理
前些日子突然碰到一个问题,说是客户的导入文件模版想支持部分导入内容的下拉选,于是我就找了easyexcel官网寻找解决方案,并没有找到合适的方案,没办法只能自己动手并分享出来,针对Java生成Excel下拉菜单时因选项过多导…...
VisualXML全新升级 | 新增数据库编辑功能
VisualXML是一个功能强大的网络总线设计工具,专注于简化汽车电子系统中复杂的网络数据设计操作。它支持多种主流总线网络格式的数据编辑(如DBC、LDF、ARXML、HEX等),并能够基于Excel表格的方式生成和转换多种数据库文件。由此&…...
前端开发者常用网站
Can I use网站:一个查询网页技术兼容性的网站 一个查询网页技术兼容性的网站Can I use:Can I use... Support tables for HTML5, CSS3, etc (查询浏览器对HTML5的支持情况) 权威网站:MDN JavaScript权威网站:JavaScript | MDN...
在golang中如何将已安装的依赖降级处理,比如:将 go-ansible/v2@v2.2.0 更换为 go-ansible/@v1.1.7
在 Go 项目中降级 go-ansible 从 v2.2.0 到 v1.1.7 具体步骤: 第一步: 修改 go.mod 文件 // 原 v2 版本声明 require github.com/apenella/go-ansible/v2 v2.2.0 替换为: // 改为 v…...
