### SqlSession简介
Mybatis是一个强大的ORM框架,它通过接口式编程为开发者屏蔽了传统JDBC的诸多不便,以简单的方式提供强大的扩展能力。其中的接口式编程就是指日常使用的Mapper接口,Mybatis借助动态代理实现了sql语句与Mapper的接口的动态绑定,降低复杂度的同时为开发人员提供强大自主权。
经过上一篇文章《Mybatis源码之SQL执行过程》已经知道,Mapper其实是Mybatis暴露给开发人员的扩展能力,在运行时Mybatis把Mapper接口转为动态方法,最终也会转交给SqlSession的CRUD方法执行。所以,SqlSession是Mybatis框架真正意义上的命令执行入口。
SqlSession是Mybatis中最基础的一个接口类,它代表一个与数据库的会话,在其生命周期内完成与数据库的交互,通过它可以:
* 执行基本的SQL命令(select/update/insert/delete):SQL命令具有唯一ID,每个ID对应Configration中的一个MappedStatement。
* 获取自定义的Mapper接口对象(getMapper):使用动态代理技术动态生成Mapper接口实现类,由MapperProxy代理实现。
* 控制管理事务操作(rollback/commit):数据库事务操作中提交与回滚操作。
![image.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/88c786ad607d4c2981a1e761406783de~tplv-k3u1fbpfcp-zoom-1.image)
以上类图为SqlSession等Mybatis接口层的继承体系与关系,由上图可知:
* SqlSessionFactory负责SqlSession的创建工作,其默认实现为DefaultSqlSessionFactory。
* SqlSession有两个实现类:DefaultSqlSession和SqlSessionManager,其中DefaultSqlSession为默认实现;SqlSessionManager除了实现SqlSession接口外,还实现了SqlSessionFactory接口。
接下来,我们通过实例和源码来详细了解下DefaultSqlSession和SqlSessionManager两者的特点与差别,进一步加深对它的理解。
### DefaultSqlSession
DefaultSqlSession是SqlSession的默认实现,它纯粹是一个功能类,完全实现了SqlSession的接口方法,但是它并不具备自主管理能力。在开始之前,我们先了解两个问题:
* DefaultSqlSession是非线程安全的,因此需要避免多线程并发使用,随用随取,及时释放;
* DefaultSqlSession它不具有自动关闭的能力,需要开发者自主调用关闭方法。
下面这个示例代码还是跟之前一样,我们重点关注第7-9行,其实只有两行代码。
~~~java
public static void main(String[] args) throws IOException {
// mybatis配置文件
String path = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(path);
// 获取 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 创建 SqlSession
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
// ……
}
}
复制代码
~~~
第7行:主要完成配置文件解析并保存到Configuration,由SqlSessionFactoryBuilder创建SqlSessionFactory对象,为创建SqlSession做准备。
第9行:采用“try-with-resource”方式调用SqlSessionFactory#openSession方法创建了SqlSession对象。当代码离开try代码块时会自动关闭SqlSession,但是这里的自动关闭并不是SqlSession的能力,相当于手动调用了SqlSession#close方法。看下openSession的实现:
~~~java
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//获取环境配置信息
final Environment environment = configuration.getEnvironment();
//创建事务处理器
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//创建Executor,这里默认使用CachingExecutor
final Executor executor = configuration.newExecutor(tx, execType);
//创建SqlSession,默认实例DefaultSqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
复制代码
~~~
由此可知,openSessionFromDataSource方法完成了SqlSession的创建,它返回了DefaultSqlSession对象。DefaultSqlSession中聚合了Configuration、Executor等对象。
#### selectXxx方法
以selectList为例看下selectXxx方法的执行过程:
~~~java
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//statement为MappedStatement的唯一id,注册在Configuration#mappedStatements
//根据id获取MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
//调用执行器的query方法执行查询命令。
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
复制代码
~~~
SqlSession中selectXxx方法,最终都走到了selectList方法,但是它并没有直接与数据库交互,获取MappedStatement对象后调用了执行器Executor的query方法。
#### insert方法
insert对应sql的insert命令,代码如下:
~~~java
public int insert(String statement) {
return insert(statement, null);
}
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
复制代码
~~~
与select类似,首先获取MappedStatement,然后调用了执行器的update方法。整体流程比较简单,有一点需要说明的是:对于数据库而言,insert/update/delete其实都是更新操作,所以这里虽然是insert方法入口,但是实际调用了Executor#update方法。
#### delete方法/update方法
delete/update方法的执行过程与insert基本是一样的,不再赘述。
#### 小结
DefaultSqlSession是SqlSession的默认实现,提供了数据库的CURD能力,但是它并没有直接与数据库交互,而是调用了Executor的相关方法,是一个名副其实的“甩手掌柜”。
在selectXxx或insert方法中,我们并没有发现它调用close方法,所以它不具备自动关闭的能力,需要我们开发者手动调用,比如示例中的“try-with-resource”方式。
回过头来说下“线程安全”:通过上述示例和源码,我们会发现一旦DefaultSqlSession对象被创建,任何获取到它的线程都可以访问其方法,CURD或者close,多个线程同时操作就会导致不可预知的结果。所以,我们不能把它直接设置为公共实例,应该即用即取,及时释放。
### SqlSessionManager
看完DefaultSqlSession,继续看下SqlSessionManager。由上述类图可知,它实现了SqlSession和SqlSessionFactory两个接口,同时具备SqlSession接口能力及SqlSession对象创建能力。
仔细观察类图会发现,SqlSessionManager内部还聚合了一个SqlSessionFactory和SqlSession对象,什么情况?实现了这两个接口,却又聚合了这两个接口的实例,难道它只是个代理,就是包了一层?源码走走看!
~~~java
public class SqlSessionMgrDemo {
public static void main(String[] args) throws IOException {
// mybatis配置文件
String path = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(path);
// 获取 SqlSessionManager
SqlSessionManager sqlSessionManager = SqlSessionManager.newInstance(inputStream);
// 开启对session的管理:没有这行代码也可以使用
sqlSessionManager.startManagedSession();
// 获取mapper
CompanyDao dao = sqlSessionManager.getMapper(CompanyDao.class);
// 执行查询
CompanyDO companyDO = dao.selectById(1);
System.out.println(companyDO);
}
}
复制代码
~~~
简单说下这段代码的执行流程:
* 4~5行:加载并读取配置文件;
* 7行:创建SqlSessionManager实例;
* 9行:开启对SqlSession的管理,通过它可以实现对SqlSession的线程安全管理,使SqlSessionManager实例可共享(这个与DefaultSqlSession有差别);
* 11~14行:执行查询;
#### 实例化
通过示例代码可知,SqlSessionManager#newInstance创建了SqlSessionManager对象,看下执行过程,我们仔细看看SqlSessionManager到底是个什么东东。
~~~java
// 创建 SqlSessionManager
public static SqlSessionManager newInstance(InputStream inputStream) {
return new SqlSessionManager(new SqlSessionFactoryBuilder().build(inputStream, null, null));
}
// 构造方法,这里入参为DefaultSqlSessionFactory实例
private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
// 通过动态代理,创建SqlSession实例
this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[]{SqlSession.class},
new SqlSessionInterceptor());
}
// SqlSessionFactory实例
private final SqlSessionFactory sqlSessionFactory;
// SqlSession 代理对象
private final SqlSession sqlSessionProxy;
复制代码
~~~
newInstance方法调用了SqlSessionManager的私有构造方法(仅有私有构造方法),构造方法的入参是SqlSessionFactory(与DefaultSqlSession的实现一致),这里是其默认实现DefaultSqlSessionFactory。
构造方法内通过动态代理创建对象sqlSessionProxy,它是SqlSession的动态实现,这里需要重点关注SqlSessionInterceptor这个类。从名称看它是一个拦截器,也就是说当我们调用SqlSession的方法时,会被它拦截,看下SqlSessionInterceptor#invoke方法:
~~~java
private class SqlSessionInterceptor implements InvocationHandler {
public SqlSessionInterceptor() {
// Prevent Synthetic Access
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 判断本地线程是否有sqlSession
final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
// 如果有,直接使用,无需创建
if (sqlSession != null) {
try {
// 调用SqlSession接口方法实现
return method.invoke(sqlSession, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
} else {
// 如果没有,通过openSession创建,openSession方法返回的是DefaultSqlSession实例
//这里使用try-with-resource,使用完成会自动关闭
try (SqlSession autoSqlSession = openSession()) {
try {
// 调用SqlSession接口方法实现
final Object result = method.invoke(autoSqlSession, args);
//事务提交
autoSqlSession.commit();
return result;
} catch (Throwable t) {
//事务回滚
autoSqlSession.rollback();
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
}
}
复制代码
~~~
SqlSessionInterceptor#invoke首先会从localSqlSession获取SqlSession对象,localSqlSession是什么呢?这里只提到get,那set在哪里?从代码里面找下吧:
~~~java
//localSqlSession是一个ThreadLocal对象,用来存储SqlSession
//这是一个线程安全的标志
private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();
//开启对SqlSession的管理,我们示例代码有调用
public void startManagedSession() {
//为localSqlSession赋值,参数为openSession返回值
this.localSqlSession.set(openSession());
}
public SqlSession openSession() {
//与之前一样,它返回的是DefaultSqlSession对象
return sqlSessionFactory.openSession();
}
复制代码
~~~
localSqlSession是一个ThreadLocal对象,用来存储SqlSession,它用来在线程内共享对象,很明显的线程安全处理方式。localSqlSession是在startManagedSession及其重载方法中设置的,通过openSession方法可知,localSqlSession内部存放的是DefaultSqlSession对象。
继续SqlSessionInterceptor#invoke分析,根据localSqlSession中是否存有sqlSession,有两个不同的处理方式:
* 有:如果有,则直接使用,不再新建。这里只能是在同一线程内共享,避免了重复创建对象的额外开销。
* 无:如果没有,就与我们DefaultSqlSession一节中的使用方式一样,使用了“try-with-resource”方式,实现了SqlSession的自动关闭。
**SqlSessionManager持有的SqlSessionFactory引用为DefaultSqlSessionFactory,作用自然是创建SqlSession实例;SqlSessionFactory创建的SqlSession是DefaultSqlSession实例;但是SqlSessionManager内部通过动态代理模式使用SqlSessionInterceptor对DefaultSqlSession进行了拦截,在拦截处理中有两种逻辑:一是使用ThreadLocal对SqlSession进行了线程安全共享;二是实现了对SqlSession的自动关闭。由此可见:SqlSessionManager是DefaultSqlSession的加强版,或者说是一个高级封装。**
#### SqlSession接口实现
SqlSessionManager对SqlSession接口的实现全部转交给其内部的sqlSessionProxy代理对象完成,没有什么处理逻辑,贴几个方法看下:
~~~java
public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
sqlSessionProxy.select(statement, parameter, rowBounds, handler);
}
public int insert(String statement) {
return sqlSessionProxy.insert(statement);
}
public int update(String statement) {
return sqlSessionProxy.update(statement);
}
public int delete(String statement) {
return sqlSessionProxy.delete(statement);
}
复制代码
~~~
sqlSessionProxy其实是一个包装了DefaultSqlSession的SqlSessionInterceptor实例,所有方法会被SqlSessionInterceptor#invoke拦截,最终是由DefaultSqlSession来实现的(就是前面一节说的内容)。
前面“实例化”一节说invoke内有两种逻辑,那实际会走哪种逻辑呢?实际上,这引出了SqlSessionManager的两种使用方式,使用方式不同invoke的执行逻辑也不同。
* 一是:像使用DefaultSqlSession一样,按需创建,此时SqlSessionManager提供了自动关闭功能。
* 二是:启用SqlSession管理功能。此时SqlSessionManager会在同一线程内共享SqlSession,它是线程安全的。
小伙伴儿们可以尝试把示例代码中第9行关闭或打开注释作下调试,一试便知。
#### 小结
SqlSessionManager虽然同时实现了SqlSession和SqlSessionFactory两个接口,但是真正“干活”的还是DefaultSqlSession和DefaultSqlSessionFactory;它使用动态代理对SqlSession的方法进行拦截,提供了SqlSession自动关闭或线程安全的解决方案。整体上看,SqlSessionManager是对安全、智能、高级封装版的DefaultSqlSession。
### SqlSession总结
本文“啰里八嗦”的介绍了DefaultSqlSession和SqlSessionManager两者的区别与联系,重点关注了SqlSessionManager如何解决线程安全和自动关闭两个问题,从源码层面了解到SqlSessionManager其实是对DefaultSqlSession的高级封装。
我们还了解到,所有的SQL命令都会从SqlSession开始,然而SqlSession并没有直接操作数据库,脏活累活都交给了Executor,形象点说SqlSession其实是一个“包工头儿”。具体Executor如何做,且听下文讲解。
作者:码路印记
链接:https://juejin.cn/post/6882696575315886094
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 一.JVM
- 1.1 java代码是怎么运行的
- 1.2 JVM的内存区域
- 1.3 JVM运行时内存
- 1.4 JVM内存分配策略
- 1.5 JVM类加载机制与对象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面试相关文章
- 2.1 可能是把Java内存区域讲得最清楚的一篇文章
- 2.0 GC调优参数
- 2.1GC排查系列
- 2.2 内存泄漏和内存溢出
- 2.2.3 深入理解JVM-hotspot虚拟机对象探秘
- 1.10 并发的可达性分析相关问题
- 二.Java集合架构
- 1.ArrayList深入源码分析
- 2.Vector深入源码分析
- 3.LinkedList深入源码分析
- 4.HashMap深入源码分析
- 5.ConcurrentHashMap深入源码分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的设计模式
- 8.集合架构之面试指南
- 9.TreeSet和TreeMap
- 三.Java基础
- 1.基础概念
- 1.1 Java程序初始化的顺序是怎么样的
- 1.2 Java和C++的区别
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字节与字符的区别以及访问修饰符
- 1.7 深拷贝与浅拷贝
- 1.8 字符串常量池
- 2.面向对象
- 3.关键字
- 4.基本数据类型与运算
- 5.字符串与数组
- 6.异常处理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 数据流(Stream)
- 8.3 Java 8 并发教程:线程和执行器
- 8.4 Java 8 并发教程:同步和锁
- 8.5 Java 8 并发教程:原子变量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、数值、算术和文件
- 8.7 在 Java 8 中避免 Null 检查
- 8.8 使用 Intellij IDEA 解决 Java 8 的数据流问题
- 四.Java 并发编程
- 1.线程的实现/创建
- 2.线程生命周期/状态转换
- 3.线程池
- 4.线程中的协作、中断
- 5.Java锁
- 5.1 乐观锁、悲观锁和自旋锁
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平锁和非公平锁
- 5.3.1 说说ReentrantLock的实现原理,以及ReentrantLock的核心源码是如何实现的?
- 5.5 锁优化和升级
- 6.多线程的上下文切换
- 7.死锁的产生和解决
- 8.J.U.C(java.util.concurrent)
- 0.简化版(快速复习用)
- 9.锁优化
- 10.Java 内存模型(JMM)
- 11.ThreadLocal详解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的实现原理
- 1.DelayQueue的实现原理
- 14.Thread.join()实现原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的实际使用场景
- 五.Java I/O NIO
- 1.I/O模型简述
- 2.Java NIO之缓冲区
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之选择器
- 6.基于 Java NIO 实现简单的 HTTP 服务器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面试题
- 六.Java设计模式
- 1.单例模式
- 2.策略模式
- 3.模板方法
- 4.适配器模式
- 5.简单工厂
- 6.门面模式
- 7.代理模式
- 七.数据结构和算法
- 1.什么是红黑树
- 2.二叉树
- 2.1 二叉树的前序、中序、后序遍历
- 3.排序算法汇总
- 4.java实现链表及链表的重用操作
- 4.1算法题-链表反转
- 5.图的概述
- 6.常见的几道字符串算法题
- 7.几道常见的链表算法题
- 8.leetcode常见算法题1
- 9.LRU缓存策略
- 10.二进制及位运算
- 10.1.二进制和十进制转换
- 10.2.位运算
- 11.常见链表算法题
- 12.算法好文推荐
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事务管理
- 4.SpringMVC 运行流程和手动实现
- 0.Spring 核心技术
- 5.spring如何解决循环依赖问题
- 6.springboot自动装配原理
- 7.Spring中的循环依赖解决机制中,为什么要三级缓存,用二级缓存不够吗
- 8.beanFactory和factoryBean有什么区别
- 九.数据库
- 1.mybatis
- 1.1 MyBatis-# 与 $ 区别以及 sql 预编译
- Mybatis系列1-Configuration
- Mybatis系列2-SQL执行过程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-参数设置揭秘(ParameterHandler)
- Mybatis系列8-缓存机制
- 2.浅谈聚簇索引和非聚簇索引的区别
- 3.mysql 证明为什么用limit时,offset很大会影响性能
- 4.MySQL中的索引
- 5.数据库索引2
- 6.面试题收集
- 7.MySQL行锁、表锁、间隙锁详解
- 8.数据库MVCC详解
- 9.一条SQL查询语句是如何执行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能优化神器 Explain 使用分析
- 12.mysql中,一条update语句执行的过程是怎么样的?期间用到了mysql的哪些log,分别有什么作用
- 十.Redis
- 0.快速复习回顾Redis
- 1.通俗易懂的Redis数据结构基础教程
- 2.分布式锁(一)
- 3.分布式锁(二)
- 4.延时队列
- 5.位图Bitmaps
- 6.Bitmaps(位图)的使用
- 7.Scan
- 8.redis缓存雪崩、缓存击穿、缓存穿透
- 9.Redis为什么是单线程、及高并发快的3大原因详解
- 10.布隆过滤器你值得拥有的开发利器
- 11.Redis哨兵、复制、集群的设计原理与区别
- 12.redis的IO多路复用
- 13.相关redis面试题
- 14.redis集群
- 十一.中间件
- 1.RabbitMQ
- 1.1 RabbitMQ实战,hello world
- 1.2 RabbitMQ 实战,工作队列
- 1.3 RabbitMQ 实战, 发布订阅
- 1.4 RabbitMQ 实战,路由
- 1.5 RabbitMQ 实战,主题
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 实战 – 整合 RabbitMQ 发送邮件
- 1.8 RabbitMQ 的消息持久化与 Spring AMQP 的实现剖析
- 1.9 RabbitMQ必备核心知识
- 2.RocketMQ 的几个简单问题与答案
- 2.Kafka
- 2.1 kafka 基础概念和术语
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志机制
- 2.4 kafka是pull还是push的方式传递消息的?
- 2.5 Kafka的数据处理流程
- 2.6 Kafka的脑裂预防和处理机制
- 2.7 Kafka中partition副本的Leader选举机制
- 2.8 如果Leader挂了的时候,follower没来得及同步,是否会出现数据不一致
- 2.9 kafka的partition副本是否会出现脑裂情况
- 十二.Zookeeper
- 0.什么是Zookeeper(漫画)
- 1.使用docker安装Zookeeper伪集群
- 3.ZooKeeper-Plus
- 4.zk实现分布式锁
- 5.ZooKeeper之Watcher机制
- 6.Zookeeper之选举及数据一致性
- 十三.计算机网络
- 1.进制转换:二进制、八进制、十六进制、十进制之间的转换
- 2.位运算
- 3.计算机网络面试题汇总1
- 十四.Docker
- 100.面试题收集合集
- 1.美团面试常见问题总结
- 2.b站部分面试题
- 3.比心面试题
- 4.腾讯面试题
- 5.哈罗部分面试
- 6.笔记
- 十五.Storm
- 1.Storm和流处理简介
- 2.Storm 核心概念详解
- 3.Storm 单机版本环境搭建
- 4.Storm 集群环境搭建
- 5.Storm 编程模型详解
- 6.Storm 项目三种打包方式对比分析
- 7.Storm 集成 Redis 详解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初识ElasticSearch
- 2.文档基本CRUD、集群健康检查
- 3.shard&replica
- 4.document核心元数据解析及ES的并发控制
- 5.document的批量操作及数据路由原理
- 6.倒排索引
- 十七.分布式相关
- 1.分布式事务解决方案一网打尽
- 2.关于xxx怎么保证高可用的问题
- 3.一致性hash原理与实现
- 4.微服务注册中心 Nacos 比 Eureka的优势
- 5.Raft 协议算法
- 6.为什么微服务架构中需要网关
- 0.CAP与BASE理论
- 十八.Dubbo
- 1.快速掌握Dubbo常规应用
- 2.Dubbo应用进阶
- 3.Dubbo调用模块详解
- 4.Dubbo调用模块源码分析
- 6.Dubbo协议模块