本文探讨在需要了解一个开源项目时, 如何快速的理清开源项目的代码逻辑!
以下是个人认为行之有效的方法:
先跑起来
自顶向下拆解
深入细节
延伸改进
本文以 Mybatis 为例来进行演示!
先 "跑起来"
程序界有个老传统, 学习新技术时都是从Hello World开始的! 无论是学习新语言时, 打印Hello World; 还是学习新框架时编写个 demo! 那为什么这里的跑起来要打个引号呢?
实际上, 当你想要阅读一个开源项目的源码时, 绝大部分情况下, 你已经能够使用这个开源项目了! 所以这里的 "跑起来" 就不是写个Hello World, 也不是能跑起来的程序了! 而是能__在你的脑子里跑起来__! 什么意思?
Mybatis 你会用了吧? 那么请问 Mybatis 是如何执行的呢? 仔细想想, 你能否用完整的语句把它描述出来?
这里是 Mybatis 的官方入门 http://www.mybatis.org/mybatis-3/zh/getting-started.html 文章! 你是如何看这篇文章的? 读一遍就行了吗? 还是跟着文章跑一遍就够了吗? 从这篇文章里你能获得多少信息?
我们来理一下:
安装
如何在项目中引入 Mybatis?
Mybatis 的 groupId 是什么? artifactId 又是什么? 目前最新版本是多少?
从 XML 中构建 SqlSessionFactory
SqlSessionFactoryBuilder 可以通过 xml 或者 Configuration 来构建 SqlSessionFactory, 那是如何构建的呢?
xml 配置了哪些信息? 既然使用了 xml, 那肯定有 xml 解析, 用什么方式解析的?
xml 里的标签都是什么意思: configuration,environments,transactionManager,dataSource,mappers. 以及这些标签的属性分别是什么意思?
SqlSessionFactory 的作用是什么?
不使用 XML 构建 SqlSessionFactory
BlogDataSourceFactory,DataSource,TransactionFactory,Environment,Configuration 这些类的作用是什么?
*Mapper 的作用是什么?
为什么提供基于 XML 和 Java 的两种配置方式? 这两种配置方式的优缺点是什么?
从 SqlSessionFactory 中获取 SqlSession
SqlSession 的作用是什么?
selectOne 和 getMapper 的执行方式有什么区别?
探究已映射的 SQL 语句
*Mapper.xml 的配置是什么?
命名空间, id 的作用是什么?
*Mapper.xml 是如何和 * Mapper.java 进行匹配的?
匹配规则是什么?
基于注解的映射配置如何使用?
为什么提供基于 XML 和基于注解的两种映射配置? 有什么优劣?
作用域 (Scope) 和生命周期
SqlSessionFactoryBuilder 应该在哪个作用域使用? 为什么?
SqlSessionFactory 应该在哪个作用域使用? 为什么?
SqlSession 应该在哪个作用域使用? 为什么?
Mapper 实例应该在哪个作用域使用? 为什么?
回答出了上面这些问题! 你也就基本能在脑子里把 Mybatis跑起来了! 之后, 你才能正真的开始阅读源码!
当你能把一个开源项目跑起来后, 实际上你就有了对开源项目最初步的了解了! 就像书的索引一样! 基于这个索引, 我们一步步的进行拆解, 来细化出下一层的结构和流程, 期间可能需要深入技术细节, 考量实现, 考虑是否有更好的实现方案! 也就是说后面的三步并不是线性的, 而是__不断交替执行__的一个过程! 最终就形成一个完整的源码执行流程!
自顶向下拆解
继续通过 Mybatis 来演示(限于篇幅, 我只演示一个大概流程)! 我们现在已经有了一个大概的流程了:
SqlSessionFactoryBuilder 通过 xml 或者 Configuration 构建出 SqlSessionFactory
可以从 SqlSessionFactory 中获取 SqlSession
SqlSession 则是真正执行 sql 的类
虽说每个点都可以往下细化, 但是也分个轻重缓急!
我们是先了解怎么构建 SqlSessionFactory 呢?
还是了解如何获取 SqlSession 呢?
还是了解 SqlSession 如何执行 sql 的呢?
很明显, SqlSession 去执行 sql 才是 Mybatis 的核心! 我们先从这个点入手!
首先, 你当然得先下载 Mybatis 的源码了(请自行下载)!
我们直接去看 SqlSession! 它是个接口, 里面有一堆执行 sql 的方法!
这里只列出了一部分方法:
- public interface SqlSession extends Closeable {
- <T> T selectOne(String statement);
- <E> List<E> selectList(String statement);
- <K, V> Map<K, V> selectMap(String statement, String mapKey);
- <T> Cursor<T> selectCursor(String statement);
- void select(String statement, Object parameter, ResultHandler handler);
- int insert(String statement);
- int update(String statement);
- int delete(String statement);
- void commit();
- void rollback();
- List<BatchResult> flushStatements();
- <T> T getMapper(Class<T> type);
...
}
SqlSession 就是通过这些方法来执行 sql 的! 我们直接看我们常用的, 也是 Mybatis 推荐的用法, 就是基于 Mapper 的执行! 也就是说SqlSession 通过 Mapper 来执行具体的 sql! 上面的流程也就细化成了:
SqlSessionFactoryBuilder 通过 xml 或者 Configuration 构建出 SqlSessionFactory
可以从 SqlSessionFactory 中获取 SqlSession
SqlSession 则是真正执行 sql 的类
SqlSession 获取对应的 Mapper 实例
Mapper 实例来执行相应的 sql
那 SqlSession 是如何获取 Mapper 的呢? Mapper 又是如何执行 sql 的呢?
深入细节
我们来看 SqlSession 的实现! SqlSession 有两个实现类 SqlSessionManager 和 DefaultSqlSession! 通过 IDE 的引用功能可以查看两个类的使用情况. 你会发现 SqlSessionManager 实际并没有使用! 而 DefaultSqlSession 是通过 DefaultSqlSessionFactory 构建的! 所以我们来看 DefaultSqlSession 是如何构建 Mapper 的!
- @Override
- public <T> T getMapper(Class<T> type) {
- return configuration.<T>getMapper(type, this);
- }
它直接委托给了 Configuration 的 getMapper 方法!
- public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
- return mapperRegistry.getMapper(type, sqlSession);
- }
Configuration 又委托给了 MapperRegistry 类的 getMapper 方法!
- public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
- final MapperProxyFactory<T> mapperProxyFactory =
- (MapperProxyFactory<T>) knownMappers.get(type);
- if (mapperProxyFactory == null) {
- throw new BindingException("Type" + type
- + "is not known to the MapperRegistry.");
- }
- try {
- return mapperProxyFactory.newInstance(sqlSession);
- } catch (Exception e) {
- throw new BindingException("Error getting mapper instance. Cause:" + e, e);
- }
- }
在 MapperRegistry 类的 getMapper 中:
通过 type 从 knownMappers 中获取对应的 MapperProxyFactory 实例
如果不存在则抛出异常
如果存在则调用 mapperProxyFactory.newInstance(sqlSession)创建对应的 Mapper
在这里 knowMappers 是什么? MapperProxyFactory 又是什么? mapperProxyFactory.newInstance(sqlSession)具体做了什么?
其实很简单, knowMappers 是个 Map, 里面包含了 class 与对应的 MapperProxyFactory 的对应关系! MapperProxyFactory 通过 newInstance 来构建对应的 Mapper(实际上是 Mapper 的代理)!
快接近真相了, 看 mapperProxyFactory.newInstance(sqlSession)里的代码:
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession,
- mapperInterface, methodCache);
- return newInstance(mapperProxy);
- }
- protected T newInstance(MapperProxy<T> mapperProxy) {
- return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),
- new Class[] { mapperInterface },
- mapperProxy);
- }
这里干了什么?
通过 sqlSession,mapperInterface 和 methodCache 构建了一个 MapperProxy 对象
然后通过 Java 的动态代理, 来生成了 Mapper 的代理类
将 Mapper 方法的执行都委托给了 MapperProxy 去执行
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- try {
- if (Object.class.equals(method.getDeclaringClass())) {
- return method.invoke(this, args);
- } else if (isDefaultMethod(method)) {
- return invokeDefaultMethod(proxy, method, args);
- }
- } catch (Throwable t) {
- throw ExceptionUtil.unwrapThrowable(t);
- }
- final MapperMethod mapperMethod = cachedMapperMethod(method);
- return mapperMethod.execute(sqlSession, args);
- }
如果是 Object 里的方法则直接执行
否则执行 MapperMethod 的 execute 方法
- public Object execute(SqlSession sqlSession, Object[] args) {
- Object result;
- switch (command.getType()) {
- case INSERT:
- {
- Object param = method.convertArgsToSqlCommandParam(args);
- result = rowCountResult(sqlSession.insert(command.getName(), param));
- break;
- }
- case UPDATE:
- {
- Object param = method.convertArgsToSqlCommandParam(args);
- result = rowCountResult(sqlSession.update(command.getName(), param));
- break;
- }
- case DELETE:
- {
- Object param = method.convertArgsToSqlCommandParam(args);
- result = rowCountResult(sqlSession.delete(command.getName(), param));
- break;
- }
- case SELECT:
- if (method.returnsVoid() && method.hasResultHandler()) {
- executeWithResultHandler(sqlSession, args);
- result = null;
- } else if (method.returnsMany()) {
- result = executeForMany(sqlSession, args);
- } else if (method.returnsMap()) {
- result = executeForMap(sqlSession, args);
- } else if (method.returnsCursor()) {
- result = executeForCursor(sqlSession, args);
- } else {
- Object param = method.convertArgsToSqlCommandParam(args);
- result = sqlSession.selectOne(command.getName(), param);
- if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
- result = OptionalUtil.ofNullable(result);
- }
- }
- break;
- case FLUSH:
- result = sqlSession.flushStatements();
- break;
- default:
- throw new BindingException("Unknown execution method for:" + command.getName());
- }
- return result;
- }
最终实际还是委托给了 sqlSession 去执行具体的 sql! 后面具体怎么实现的就自行查看吧!
延伸改进
现在我们的流程大概是这样的一个过程:
SqlSessionFactoryBuilder 通过 xml 或者 Configuration 构建出 SqlSessionFactory
可以从 SqlSessionFactory 中获取 SqlSession
SqlSession 则是真正执行 sql 的类
SqlSession 获取对应的 Mapper 实例
DefaultSqlSession.getMapper
Configuration.getMapper
MapperRegistry.getMapper
mapperProxyFactory.newInstance(sqlSession)
通过 sqlSession,mapperInterface 和 methodCache 构建了一个 MapperProxy 对象
然后通过 Java 的动态代理, 来生成了 Mapper 的代理类
Mapper 实例来执行相应的 sql
将 Mapper 方法的执行都委托给了 MapperProxy 去执行
如果是 Object 里的方法则直接执行
否则执行 MapperMethod 的 execute 方法
最终还是委托给 sqlSession 去执行 sql
现在我们大概知道了:
为什么 Mapper 是个接口了
Mybatis 基于这个接口做了什么
那么,
什么是动态代理(基础哦)?
为什么使用动态代理来处理?
基于动态代理有什么优点? 又有什么缺点?
除了动态代理, 还有其它什么实现方式吗? 比如说 cglib?
如果是其它语言的话, 有没有什么好的实现方式呢?
......
这个问题列表可以很长, 可以按个人需要去思考并尝试回答! 可能最终这些问题已经和开源项目本身没有什么关系了! 但是你思考后的收获要比看源码本身要多得多!
再循环
一轮结束后, 可以再次进行:
自顶向下拆解
深入细节
延伸改进
不断的拆解 ->深入 ->改进, 最终你能__通过一个开源项目, 学习到远比开源项目本身多得多的知识__!
最重要的是, 你的流程是完整的. 无论是最初的大致流程:
SqlSessionFactoryBuilder 通过 xml 或者 Configuration 构建出 SqlSessionFactory
可以从 SqlSessionFactory 中获取 SqlSession
SqlSession 则是真正执行 sql 的类
还是到最终深入的细枝末节, 都是个完整的流程!
这样的好处是, 你的时间能自由控制:
你是要花个半天时间, 了解大致流程
还是花个几天理解细节流程
还是花个几周, 几个月来深入思考, 不断延伸
你都可以从之前的流程中快速进行下去!
而不像 debug 那样的方式, 需要一下子花费很长的时间去一步步的理流程, 费时费力, 收效很小, 而且如果中断了就很难继续了!
总结
本文通过梳理 Mybatis 源码的一个简单流程, 来讲述一个个人认为比较好的阅读源码的方式, 并阐述此方法与传统 debug 方式相比的优势.
公众号: ivaneye
来源: https://www.cnblogs.com/ivaneye/p/8732453.html