白话DUBBO原理,通俗易记,再也不怕面试时讲不清楚了
liuian 2025-06-15 17:36 64 浏览
现在的各种面试免不了要问些中间件,尤其是互联网公司,更注重获选人对中间件的掌握情况。在中间件中,有一大类是关于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 是会话域,它持有调用过程中的变量,比如方法名,参数等。
相关推荐
- 宽带连接错误651是什么意思(宽带连接错误651是什么意思呀)
-
宽带连接错误651为调制解调器(modem)发出的错误报告,一般情况下是未找到指定的端口。以下介绍以下这种错误发生的情况和具体的解决方法:【宽带连接错误651是什么意思】 遇到宽带连接错误651时,...
- 电脑重装系统在哪里(电脑重装系统去哪里比较好)
-
重装电脑系统是一个相对复杂的过程,首先需要备份重要数据,然后准备一个可启动的安装介质,如U盘或光盘。接下来,进入BIOS设置,将启动顺序调整为首先从安装介质启动。然后,按照屏幕上的指示进行系统安装,选...
- win10专业版激活密钥无法激活
-
如果您的Windows10专业版激活密钥被阻止,您可以尝试以下几种方法:1. 检查激活密钥是否正确:请确保您输入的激活密钥是正确的,并且与您的Windows10专业版版本匹配。2...
- 闪迪内存卡数据恢复(闪迪sd卡数据恢复)
-
首先,不要继续往闪迪内存卡中写入新的数据,以免覆盖原有数据。然后,可以使用数据恢复软件来尝试恢复误删的文件,如Recuva、EaseUSDataRecoveryWizard等,这些软件可以扫描内...
- 电脑内存条通用吗(电脑内存条通用吗怎样区分)
-
不是通用的。内存条是CPU可通过总线寻址,并进行读写操作的电脑部件。电脑上的内存条是不通用的,选择内存条的时候,需要查清主板支持的内存条的种类,在电脑上安装主板支持的内存条。内存条分为DDRDDR2...
- 系统维护中怎么解决(系统维护中一般要多久才能恢复)
-
系统正在维护中意思是系统正在更新或者升级中。网站或软件代码程序出现了问题,或者网站、软件需要更新升级的时候,一般会关闭进行查找问题,处理问题,内容进行更新。处理完了才会开通网站访问。为了清除系统运行中...
- 电脑桌面隐藏的文件夹怎么找出来
-
要查看电脑上隐藏的文件夹,可以采取以下几种方法:方法一:使用文件夹选项按下键盘上的Windows键和E键打开“文件资源管理器”。找到“查看”和“选项”两个选项并勾选。选中“显示隐藏的文件、文件夹和驱动...
- 搜狗中文手写输入法下载(下载搜狗手写版)
-
点击输入框弹出搜狗键盘,进入搜狗工具栏的设置图标,手写设置,勾选“手写注音”。在输入法点击那个键盘,就会有那个输入方式的选项,选择手写输入就可以了。呼出输入法,点击输入法上方的键盘图样,找到手写输入,...
- 免费云服务器怎么样(云服务器哪些是免费)
-
搞软件开发的人都知道,对于一些网络应用软件必须要租用服务器存放才能被更多用户使用。如一些应用于浏览软件、传输软件、远程登录软件等。如果是像以往那样租用物理机服务器,成本费用非常高昂。但是现在有云服务器...
- 斐讯路由器登录密码忘记了怎么办
-
1.斐讯无线路由器密码忘记,要重设密码,首先长按路由器背面的reset键,将路由器恢复出厂设置。2.路由器恢复出厂设置以后,在电脑浏览器地址栏中输入:192.168.1.1并按下回车键,“用户名”输...
- 软件之家软件分享(软件之家软件合集)
-
驱动精灵,驱动人生,360驱动大师。云之家挺好用的,通讯录是按组织架构的,跟微信一样可以看到所有人的动态,还有小组一般是按公司、部门、行业、兴趣分的,老板开讲可以把最上级的意思快速传达给所有员工。我们...
- 鲁大师下载安装免费版(鲁大师app下载)
-
解决方法吧。第一种方法:一般情况下,我们安装的时候软件也需要一定的时间,我们可以多等待一会。第二种方法:安装到99%你直接关掉安装。然后自己到安装目录文件里找到启动程序,就可以用了。第三种方法:1、可...
- ssd坏道检测与修复(ssd硬盘坏道检测)
-
1、固态硬盘(SSD硬盘)损坏分两方面,一方面是出现硬盘坏块,另一方面是出现物理损坏。2、固态硬盘如果出现坏块是不可修复的。SSD硬盘坏块不是所谓的逻辑坏块。SSD硬盘内部是晶体管,损坏后不能恢复,这...
- win10专业工作版激活密钥(windows10 专业版激活密钥)
-
要获得Windows10专业版永久激活密钥,可以通过微软官方零售渠道或可靠的第三方卖家购买正版产品密钥。使用第三方卖家时,务必注意其信誉和真实性。激活后,密钥将与您的Microsoft帐户关...
- 一周热门
-
-
飞牛OS入门安装遇到问题,如何解决?
-
如何在 iPhone 和 Android 上恢复已删除的抖音消息
-
Boost高性能并发无锁队列指南:boost::lockfree::queue
-
大模型手册: 保姆级用CherryStudio知识库
-
用什么工具在Win中查看8G大的log文件?
-
如何在 Windows 10 或 11 上通过命令行安装 Node.js 和 NPM
-
威联通NAS安装阿里云盘WebDAV服务并添加到Infuse
-
Trae IDE 如何与 GitHub 无缝对接?
-
idea插件之maven search(工欲善其事,必先利其器)
-
如何修改图片拍摄日期?快速修改图片拍摄日期的6种方法
-
- 最近发表
- 标签列表
-
- 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)
