白话DUBBO原理,通俗易记,再也不怕面试时讲不清楚了
liuian 2025-06-15 17:36 68 浏览
现在的各种面试免不了要问些中间件,尤其是互联网公司,更注重获选人对中间件的掌握情况。在中间件中,有一大类是关于RPC框架的,Dubbo即是阿里出品的一款很著名的RPC中间件,很多互联网公司都在用,面试也是经常被问到的。了解其原理在面试时才能有的放矢,游刃有余。现在关于Dubbo原理讲的有很多,本文以RPC的本质为视角来白话其原理,希望能够帮助大家更好理解和记忆。
RPC的本质
RPC的本质是什么?通俗地讲RPC就是要解决远程服务间的调用问题,也就是管理服务配置并提供便捷可靠高效的服务间调用。
我们来看看dubbo的定义:dubbo是一个分布式的服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
通过定义,我们提出以下几个问题,并通过这几个问题来介绍DUBBO。
- DUBBO的实现思想(总体架构)什么?
- DUBBO是如何实现透明化使用的?
- DUBBO中服务配置与实际调用是怎么结合的(如何实现远程服务调用)?
总体架构
先附DUBBO官网的架构图,
简单解释下架构图,
DUBBO分为四个模块,分别为:注册中心(Registry)、提供者(Provider)、消费者(Consumer)和监控(Monitor)。
- 注册中心(Registry):可以是zookeeper、redis、multicast、simple(官方推荐使用Zookeeper);
- 提供者(Provider):服务启动时,Provider引用容器中的服务,并向Registry注册服务,同时暴露服务(Consumer是直接和Provider通讯实现服务调用的)。
- 消费者(Consumer):服务启动时,Consumer向Registry订阅服务,如果没有订阅到自己想获得的服务,它会不断的尝试订阅。新的服务注册到注册中心以后,注册中心会将这些服务通过notify到消费者。Consumer直接调用Provider提供的服务。
- 监控(Monitor):Consumer和Provider会通过异步的方式定时向Monitor发送消息,报告服务的状态。Monitor在整个架构中是可选的,Monitor功能需要单独配置,不配置或者配置后挂掉并不会影响服务的调用。
所以,DUBBO的实现思路是通过注册中心实现服务的动态注册与发现,Provider暴露服务,Consumer直接和Provider通讯实现服务间的调用的。
透明化使用
本节需要对Spring的Bean加载机制有一定的了解,如果大家感兴趣,后续我可以再详细介绍
用过DUBBO的,应该都知道,使用DUBBO时,只需要进行一些Spring的配置即能享受DUBBO远程调用的便利。显而易见,DUBBO就是利用的Spring的扩展性,通过全Spring配置方式,使得用户能够透明化接入应用,并且对应用没有任何API侵入。
Dubbo通过扩展Spring Schema完成自定义bean的IoC容器注入。
- 自定义了XML Schema文件META-INF/dubbo.xsd描述自定义元素;
- 自定义继承自org.springframework.beans.factory.xml.NamespaceHandlerSupport抽象类的DubboNamespaceHandler处理器类;
- 自定义实现了org.springframework.beans.factory.xml.BeanDefinitionParser接口的DubboBeanDefinitionParser解析器类;
这样,DUBBO很好地将配置(dubbo:provider和dubbo:reference标签定义的bean)转化成bean,基于Spring容器的应用能够方便地使用。
public class DubboNamespaceHandler extends NamespaceHandlerSupport {
static {
Version.checkDuplicate(DubboNamespaceHandler.class);
}
public void init() {
registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
}
}如上,DubboNamespaceHandler实现NamespaceHandlerSupport接口,通过注册DubboBeanDefinitionParser解析器,将对应配置解析后转化成ApplicationConfig、ModuleConfig、RegistryConfig、MonitorConfig、ProviderConfig、ConsumerConfig、ProtocolConfig、AnnotationBean、ServiceBean和ReferenceBean。
我们着重看ServiceBean和ReferenceBean,这两个bean是服务的调用bean,业务上我们调用的服务就是通过这两个bean来交互的。
服务调用
在看ServiceBean和ReferenceBean前,我们先看一张图,
图例说明:
图中小方块 Protocol, Cluster, Proxy, Service, Container, Registry, Monitor
代表层或模块,蓝色的表示与业务有交互,绿色的表示只对 Dubbo 内部交互。 图中背景方块 Consumer, Provider,
Registry, Monitor 代表部署逻辑拓扑节点。 图中蓝色虚线为初始化时调用,红色虚线为运行时异步调用,红色实线为运行时同步调用。
图中只包含 RPC 的层,不包含 Remoting 的层,Remoting 整体都隐含在 Protocol 中。
图中我们可以看出,Consumer和Provider之间是通过Protocol交互的,不错,DUBBO可以自定义Protocol完成服务调用(Protocol是什么时候初始化,什么时候调用的,可以看后续对ReferenceBean和ServiceBean介绍)。
@SPI("dubbo")
public interface Protocol {
/**
* 获取缺省端口,当用户没有配置端口时使用。
*
* @return 缺省端口
*/
int getDefaultPort();
/**
* 暴露远程服务:<br>
* 1. 协议在接收请求时,应记录请求来源方地址信息:RpcContext.getContext().setRemoteAddress();<br>
* 2. export()必须是幂等的,也就是暴露同一个URL的Invoker两次,和暴露一次没有区别。<br>
* 3. export()传入的Invoker由框架实现并传入,协议不需要关心。<br>
*
* @param <T> 服务的类型
* @param invoker 服务的执行体
* @return exporter 暴露服务的引用,用于取消暴露
* @throws RpcException 当暴露服务出错时抛出,比如端口已占用
*/
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
/**
* 引用远程服务:<br>
* 1. 当用户调用refer()所返回的Invoker对象的invoke()方法时,协议需相应执行同URL远端export()传入的Invoker对象的invoke()方法。<br>
* 2. refer()返回的Invoker由协议实现,协议通常需要在此Invoker中发送远程请求。<br>
* 3. 当url中有设置check=false时,连接失败不能抛出异常,并内部自动恢复。<br>
*
* @param <T> 服务的类型
* @param type 服务的类型
* @param url 远程服务的URL地址
* @return invoker 服务的本地代理
* @throws RpcException 当连接服务提供方失败时抛出
*/
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
/**
* 释放协议:<br>
* 1. 取消该协议所有已经暴露和引用的服务。<br>
* 2. 释放协议所占用的所有资源,比如连接和端口。<br>
* 3. 协议在释放后,依然能暴露和引用新的服务。<br>
*/
void destroy();
}Protocol的接口有export方法和refer方法,export方法负责暴露远程Invoker服务,而refer方法获取远程服务的Invoker实现。
这里,Invoker是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
public interface Invoker<T> extends Node {
/**
* get service interface.
*
* @return service interface.
*/
Class<T> getInterface();
/**
* invoke.
*
* @param invocation
* @return result
* @throws RpcException
*/
Result invoke(Invocation invocation) throws RpcException;
}Invoker接口中的invoke方法需要一个Invocation类型的参数,很容易想象,Invocation是就是会话域,它持有调用过程中的变量,比如方法名,参数等。
Protocol、Invoker和Invocation是Dubbo中有3个很重要的概念,一切都是围绕这三个展开的。
现在我们继续来看ServiceBean和ReferenceBean。
配置与实际调用的关联
我们知道ServiceBean和ReferenceBean,也知道了远程调用是通过Protocol交互的,但是ServiceBean、ReferenceBean和Protocol间又是怎么关联的呢?
Consumer之ReferenceBean
ReferenceBean实现了FactoryBean接口,用于根据加载的配置(xml中dubbo:reference节点的配置或者@Reference注解的配置)创建bean实例。创建实例的过程就是创建引用接口的代理,ReferenceConfig中有个ref属性用于引用该接口的代理。
public class ReferenceBean<T> extends ReferenceConfig<T> implements FactoryBean, ApplicationContextAware, InitializingBean, DisposableBean {
public Object getObject() throws Exception {
return get();
}
}
public class ReferenceConfig<T> extends AbstractReferenceConfig {
private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
// 接口代理类引用
private transient volatile T ref;
private transient volatile Invoker<?> invoker;
public synchronized T get() {
if (destroyed){
throw new IllegalStateException("Already destroyed!");
}
if (ref == null) {
init();
}
return ref;
}
private void init() {
...
// 创建引用的代理,这里的map是一些配置信息,如interface、methods、retried、application、dubbo(版本)等信息
ref = createProxy(map);
...
}
private T createProxy(Map<String, String> map) {
...
// Protocol映射的远程服务的Invoker,有对应Protocol有对应的Invoker实现
invoker = refprotocol.refer(interfaceClass, urls.get(0));
...
// 根据invoker创建服务代理
return (T) proxyFactory.getProxy(invoker);
}
}根据代码,我们可以发现ReferenceBean在get Bean的时候就是调用ReferenceConfig的get方法,而get方法最终调用的是createProxy方法,着重看这个方法,会先通过refprotocol的refer方法获得Invoker,refprotocol实际上就是通过
ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension()创建的Protocol,而后再由Invoker创建该接口的代理。这样ReferenceBean中接口类代理的创建过程就很清晰了。
Provider之ServiceBean
ServiceBean实现了ApplicationListener接口,在容器初始化后调用Protocol的export方法暴露服务。
public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware {
public void onApplicationEvent(ApplicationEvent event) {
if (ContextRefreshedEvent.class.getName().equals(event.getClass().getName())) {
if (isDelay() && ! isExported() && ! isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
export();
}
}
}
}
public class ServiceConfig<T> extends AbstractServiceConfig {
private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
// 接口实现类引用
private T ref;
public synchronized void export() {
...
doExport();
...
}
protected synchronized void doExport() {
...
doExportUrls();
...
}
/**
* 广播注册中心暴露发现服务地址
**/
private void doExportUrls() {
// 将registry配置转化成url,例如:registry://host:port/com.alibaba.dubbo.registry.RegistryService?application=xxx&pid=xxx(R)istry=zookeeper×tamp=xxx
List<URL> registryURLs = loadRegistries(true);
// 这里的protocols是List<ProtocolConfig>,按指定支持的协议暴露服务
for (ProtocolConfig protocolConfig : protocols) {
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
...
// 将接口实体类引用转化成Invoker
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
Exporter<?> exporter = protocol.export(invoker);
exporters.add(exporter);
...
}
}根据代码,我们可以发现在Spring在调用onApplicationEvent方法的时候,会调用export方法,而这个方法最终会调用doExportUrlsFor1Protocol方法,方法中很重要的一段逻辑是通过ProxyFactory根据引用的接口对象和拼装的URL生成Invoker,而后通过Protocol暴露出来。Provider暴露一个服务的详细过程如下:
现在我们就可以回答Protocol是如何和服务建立联系的?
ReferenceConfig和ServiceConfig都有一个属性Protocol,服务与Protocol间就是通过这个属性建立联系的。
细心地你会发现ReferenceConfig和ServiceConfig的Protocol属性是static final的,我们知道Dubbo是支持多协议的,那又是怎么实现的呢?
扩展点加载ExtensionLoader
ReferenceConfig和ServiceConfig的Protocol属性赋值是通过
ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension()赋值的,深入代码,我们可以发现,它返回的是动态生成的实现Protocol接口的类,生成的类会对打了@Adaptive标注的方法重新生成方法,生成的方法调用逻辑是根据传入参数的协议号动态调用对应协议的方法。
public class ExtensionLoader<T> {
public T getAdaptiveExtension() {
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
...
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
...
}
return (T) instance;
}
private T createAdaptiveExtension() {
try {
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can not create adaptive extenstion " + type + ", cause: " + e.getMessage(), e);
}
}
private Class<?> getAdaptiveExtensionClass() {
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
private Class<?> createAdaptiveExtensionClass() {
String code = createAdaptiveExtensionClassCode();
ClassLoader classLoader = findClassLoader();
com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}
private String createAdaptiveExtensionClassCode() {
StringBuilder codeBuidler = new StringBuilder();
...
codeBuidler.append("package " + type.getPackage().getName() + ";");
codeBuidler.append("\nimport " + ExtensionLoader.class.getName() + ";");
codeBuidler.append("\npublic class " + type.getSimpleName() + "$Adpative" + " implements " + type.getCanonicalName() + " {");
...
return codeBuidler.toString();
}
}附.通过ExtensionLoader动态生成的实现Protocol接口的类
package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
public void destroy() {
throw new UnsupportedOperationException(
"method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}
public int getDefaultPort() {
throw new UnsupportedOperationException(
"method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}
public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0)
throws com.alibaba.dubbo.rpc.Invoker {
if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
if (arg0.getUrl() == null) throw new IllegalArgumentException(
"com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
com.alibaba.dubbo.common.URL url = arg0.getUrl();
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
if (extName == null) throw new IllegalStateException(
"Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url("
+ url.toString() + ") use keys([protocol])");
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.export(arg0);
}
public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1)
throws java.lang.Class {
if (arg1 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg1;
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
if (extName == null) throw new IllegalStateException(
"Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url("
+ url.toString() + ") use keys([protocol])");
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.refer(arg0, arg1);
}
}小结
Dubbo通过扩展Spring Schema完成自定义bean的IoC容器注入。在Spring注入和初始化bean时,通过Protocol和ProxyFactory配合完成服务的暴露和服务代理的创建。这种全Spring的配置方式,使得用户能够透明化接入应用,并且对应用没有任何API侵入。
在Dubbo的服务实际调用中都是围绕着Protocol、Invoker和Invocation三者,这也是DUBBO的核心领域模型:
- Protocol 是服务域,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。
- Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
- Invocation 是会话域,它持有调用过程中的变量,比如方法名,参数等。
相关推荐
-
- 驱动网卡(怎么从新驱动网卡)
-
网卡一般是指为电脑主机提供有线无线网络功能的适配器。而网卡驱动指的就是电脑连接识别这些网卡型号的桥梁。网卡只有打上了网卡驱动才能正常使用。并不是说所有的网卡一插到电脑上面就能进行数据传输了,他都需要里面芯片组的驱动文件才能支持他进行数据传输...
-
2026-01-30 00:37 liuian
- win10更新助手装系统(微软win10更新助手)
-
1、点击首页“系统升级”的按钮,给出弹框,告诉用户需要上传IMEI码才能使用升级服务。同时给出同意和取消按钮。华为手机助手2、点击同意,则进入到“系统升级”功能华为手机助手华为手机助手3、在检测界面,...
- windows11专业版密钥最新(windows11专业版激活码永久)
-
Windows11专业版的正版密钥,我们是对windows的激活所必备的工具。该密钥我们可以通过微软商城或者通过计算机的硬件供应商去购买获得。获得了windows11专业版的正版密钥后,我...
-
- 手机删过的软件恢复(手机删除过的软件怎么恢复)
-
操作步骤:1、首先,我们需要先打开手机。然后在许多图标中找到带有[文件管理]文本的图标,然后单击“文件管理”进入页面。2、进入页面后,我们将在顶部看到一行文本:手机,最新信息,文档,视频,图片,音乐,收藏,最后是我们正在寻找的[更多],单击...
-
2026-01-29 23:55 liuian
- 一键ghost手动备份系统步骤(一键ghost 备份)
-
步骤1、首先把装有一键GHOST装系统的U盘插在电脑上,然后打开电脑马上按F2或DEL键入BIOS界面,然后就选择BOOT打USDHDD模式选择好,然后按F10键保存,电脑就会马上重启。 步骤...
- 怎么创建局域网(怎么创建局域网打游戏)
-
1、购买路由器一台。进入路由器把dhcp功能打开 2、购买一台交换机。从路由器lan端口拉出一条网线查到交换机的任意一个端口上。 3、两台以上电脑。从交换机任意端口拉出网线插到电脑上(电脑设置...
- 精灵驱动器官方下载(精灵驱动手机版下载)
-
是的。驱动精灵是一款集驱动管理和硬件检测于一体的、专业级的驱动管理和维护工具。驱动精灵为用户提供驱动备份、恢复、安装、删除、在线更新等实用功能。1、全新驱动精灵2012引擎,大幅提升硬件和驱动辨识能力...
- 一键还原系统步骤(一键还原系统有哪些)
-
1、首先需要下载安装一下Windows一键还原程序,在安装程序窗口中,点击“下一步”,弹出“用户许可协议”窗口,选择“我同意该许可协议的条款”,并点击“下一步”。 2、在弹出的“准备安装”窗口中,可...
- 电脑加速器哪个好(电脑加速器哪款好)
-
我认为pp加速器最好用,飞速土豆太懒,急速酷六根本不工作。pp加速器什么网页都加速,太任劳任怨了!以上是个人观点,具体性能请自己试。ps:我家电脑性能很好。迅游加速盒子是可以加速电脑的。因为有过之...
- 任何u盘都可以做启动盘吗(u盘必须做成启动盘才能装系统吗)
-
是的,需要注意,U盘的大小要在4G以上,最好是8G以上,因为启动盘里面需要装系统,内存小的话,不能用来安装系统。内存卡或者U盘或者移动硬盘都可以用来做启动盘安装系统。普通的U盘就可以,不过最好U盘...
- u盘怎么恢复文件(u盘文件恢复的方法)
-
开360安全卫士,点击上面的“功能大全”。点击文件恢复然后点击“数据”下的“文件恢复”功能。选择驱动接着选择需要恢复的驱动,选择接入的U盘。点击开始扫描选好就点击中间的“开始扫描”,开始扫描U盘数据。...
- 系统虚拟内存太低怎么办(系统虚拟内存占用过高什么原因)
-
1.检查系统虚拟内存使用情况,如果发现有大量的空闲内存,可以尝试释放一些不必要的进程,以释放内存空间。2.如果系统虚拟内存使用率较高,可以尝试增加系统虚拟内存的大小,以便更多的应用程序可以使用更多...
-
- 剪贴板权限设置方法(剪贴板访问权限)
-
1、首先打开iphone手机,触碰并按住单词或图像直到显示选择选项。2、其次,然后选取“拷贝”或“剪贴板”。3、勾选需要的“权限”,最后选择开启,即可完成苹果剪贴板权限设置。仅参考1.打开苹果手机设置按钮,点击【通用】。2.点击【键盘】,再...
-
2026-01-29 21:37 liuian
- 平板系统重装大师(平板重装win系统)
-
如果你的平板开不了机,但可以连接上电脑,那就能好办,楼主下载安装个平板刷机王到你的个人电脑上,然后连接你的平板,平板刷机王会自动识别你的平板,平板刷机王上有你平板的我刷机包,楼主点击下载一个,下载完成...
- 联想官网售后服务网点(联想官网售后服务热线)
-
联想3c服务中心是联想旗下的官方售后,是基于互联网O2O模式开发的全新服务平台。可以为终端用户提供多品牌手机、电脑以及其他3C类产品的维修、保养和保险服务。根据客户需求层次,联想服务针对个人及家庭客户...
- 一周热门
-
-
用什么工具在Win中查看8G大的log文件?
-
如何在 Windows 10 或 11 上通过命令行安装 Node.js 和 NPM
-
Trae IDE 如何与 GitHub 无缝对接?
-
如何修改图片拍摄日期?快速修改图片拍摄日期的6种方法
-
5步搞定动态考勤表!标记节假日、调休日?Excel自动变色!
-
RK3588-HDMIRX(瑞芯微rk3588芯片手册)
-
用纯Python轻松构建Web UI:Remi 动态更新,实时刷新界面内容
-
tplink无线路由器桥接教程(tplink路由器如何进行无线桥接)
-
R语言 | CNS绘图第1款——linkET万物皆可连
-
都说Feign是RPC,没有侵入性,为什么我的代码越来越像 C++
-
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)
