那该如何看源码呢?
我是把MyBatis的源码下载下来, 茫无目的的看?这会不会迷失在源码中呢,我记得我刚到我当前这家公司的时候,看代码就是一个一个方法的看,然后感觉很头疼,也没看懂最后再做什么。后面反思了一下,其实应该关注宏观的流程,就是这个代码实现了什么功能,这些代码都是为了实现这个功能,不必每一个方法都一行一行的看,以方法为单位去看,这个方法从整体上来看做了什么样的事情,先不必过多的去关注内部的实现细节。这样去看对代码大概心里就有数了。同样的在MyBatis这里,这也是我第一个特别仔细研究的代码,所以MyBatis系列的第一篇,我们先从宏观上看其实现,在后面的过程中慢慢补全其细节。本篇的主线是我们在xml中写的增删改查语句究竟是怎么被执行的。
参阅了很多MyBatis源码的资料,MyBatis的整体架构可以分为三层:
- 接口层: SqlSession 是我们平时与MyBatis完成交互的核心接口(包括后续整合SpringFramework用到的SqlSessionTemplte)
- 核心层: SqlSession执行的方法,底层需要经过配置文件的解析、SQL解析,以及执行SQL时的参数映射、SQL执行、结果集映射,另外还有穿插其中的扩展插件。
- 支持层: 核心层的功能实现,是基于底层的各个模块,共同协调完成的。
搭建MyBatis的环境
搭建MyBatis的环境在《假装是小白之重学MyBatis(一)》已经讲过了,这里只简单在讲一下:
- 引入Maven依赖
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.5</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.30</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.30</version> </dependency>
- 然后来一张表
CREATE TABLE `student` ( `id` int(11) NOT NULL COMMENT '唯一标识', `name` varchar(255) , `number` varchar(255) , `money` int(255) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4;
- 来个MyBatis的配置文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--加载配置文件--> <properties resource="jdbc.properties"/> <!--指定默认环境, 一般情况下,我们有三套环境,dev 开发 ,uat 测试 ,prod 生产 --> <environments default="development"> <environment id="development"> <!-- 设置事务管理器的管理方式 --> <transactionManager type="JDBC"/> <!-- 设置数据源连接的关联方式为数据池 --> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/studydatabase?characterEncoding=utf-8"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <!--设置扫描的xml,org/example/mybatis是包的全类名,StudentMapper.xml会讲--> <mappers> <!--设置扫描的xml,org/example/mybatis是包的全类名,这个BlogMapper.xml会讲 <package name = "org.example.mybatis"/> <!-- 包下批量引入 单个注册 --> <mapper resource="org/example/mybatis/StudentMapper.xml"/> </mappers> </mappers> </configuration>
- 来个Student类
public class Student { private Long id; private String name; private String number; private String money; // 省略get set 函数 }
- 来个Mapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace = "org.example.mybatis.StudentMapper"> <select id = "selectStudent" resultType = "org.example.mybatis.Student"> SELECT * FROM STUDENT </select> </mapper>
- 来个接口
public interface StudentMapper { List<Student> selectStudent(); }
- 日志配置文件
log4j.rootCategory=debug, CONSOLE # Set the enterprise logger category to FATAL and its only appender to CONSOLE. log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE # CONSOLE is set to be a ConsoleAppender using a PatternLayout. log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender log4j.appender.CONSOLE.Encoding=UTF-8 log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30
- 开始查询之旅
public class MyBatisDemo { public static void main(String[] args) throws Exception { Reader reader = Resources.getResourceAsReader("conf.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); SqlSession sqlSession = sqlSessionFactory.openSession(); StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); List<Student> studentList = studentMapper.selectStudent(); studentList.forEach(System.out::println); } }
执行之后就可以在控制台看到如下输出了:
让我们从SQL的执行之旅开始谈起
执行过程浅析
上面的执行过程大致可以分成三步:
- 解析配置文件,构建SqlSessionFactory
- 通过SqlSessionFactory 拿到SqlSession,进而获得代理类
- 执行代理类的方法
解析配置文件
解析配置文件通过SqlSessionFactoryBuilder的build方法来执行, build方法有几个重载:
Reader指向了conf文件, environment是环境,properties用于conf向其他properties取值。我们的配置文件是一个xml,所以XmlConfigBuilder最终是对配置文件的封装。这里我们不关注XmlBuilder是如何构建的,我们接着往下看,构建Xml对象之后,调用parse方法,将其转换为MyBatis的Configuration对象:
// parseConfiguration 这个方法用于取xml标签的值并将其设置到Configuration上 public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; }
// 取标签的过程,XML->Configuration private void parseConfiguration(XNode root) { try { // issue #117 read properties first propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); loadCustomLogImpl(settings); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers")); // 获取mapper方法, } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
- Configuration概览
- mapperElement
注意我们本篇的主题是重点看我们写在xml标签中的sql是如何被执行的,所以我们这里重点看parseConfiguration的mapperElement的方法。从名字上我们大致推断,这个方法是加载mapper.xml文件的。我们点进去看一下:
// parent 是mappers标签 private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // 遍历mappers下面的结点 if ("package".equals(child.getName())) { // 如果是package标签则走批量引入 String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { String resource = child.getStringAttribute("resource"); // 我们本次看单个引入的方式 String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); // 加载指定文件夹下的XML XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); // 将mapper 中的标签值映射成MyBatis的对象 } else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); // 我们大致看下parse方法的实现 } else if (resource == null && url == null && mapperClass != null) { Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }
parent参数是mappers标签,我们可以通过调试验证这一点:
public void parse() { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
在介绍的时候西安判断该xml是否已经加载过了, 然后解析mapper标签下的增删改查等标签,我们可以在configurationElement看到这一点。
private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.isEmpty()) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); buildStatementFromContext(context.evalNodes("select|insert|update|delete")); //该方法解析标签 } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } }
private void buildStatementFromContext(List<XNode> list) { if (configuration.getDatabaseId() != null) { // dataBaseId 用于指明该标签在哪个数据库下执行 buildStatementFromContext(list, configuration.getDatabaseId()); } buildStatementFromContext(list, null); }
parseStatementNode方法比较长,最终还是在解析Mapper.xml的select、insert、update、delete的属性, 将解析的属性传递builderAssistant.addMappedStatement()方法中去,该方法参数略多,这来我们上截图:
到此我们基本结束看构建configuration的过程,我们可以认为在这一步,Mybatis的配置文件和Mapper.xml已经基本解析完毕。
获取SqlSession对象
SqlSession是一个接口,有两个主要实现类:
我们在第一步build出来的事实上是DefaultSqlSessionFactory:
public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
这里事实上openSession也是由DefaultSqlSessionFactory来执行的,我们看下在openSession这个过程中大致做了什么:
@Override public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); }
注意这个getDefaultExecutorType, 这个事实是MyBatis分层中核心层的SQL执行器,我们接着往下看openSessionFromDataSource:
// level 隔离级别, autoCommit 是否自动提交 // ExecutorType 是一个枚举值: SIMPLE、REUSE、BATCH 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); // 返回一个执行器,我们看下newExecutor这个方法 final Executor executor = configuration.newExecutor(tx, execType); // 最后构造出来SqlSession 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(); } }
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } // 上面是根据executorType生成对应的执行器 // 如果开启缓存,则将其执行器包装为另一种形式的执行器 if (cacheEnabled) { executor = new CachingExecutor(executor); } // interceptorChain 是一个拦截器链 // 将该执行器加入到拦截器链中增强,这事实上是MyBatis的插件开发。 // 也是装饰器模式的应用,后面会讲。 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
执行增删改查
接着我们来看我们的接口中的方法是如何执行的,
StudentMapper执行selectStudent方法事实上进入的应该是对应代理的对象, 我们进入下一步, 事实上是进入了invoke方法,这个invoke方法事实上重写的InvocationHandler的方法,InvocationHandler是JDK提供的动态代理接口,调用被代理的的方法,事实上是会走到这个invoke方法中,实现如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else { // 该方法会缓存该方法,如果该在缓存里面有,则无需再次产生,里面的methodCache是ConcurrentHashMap // 最终会返回MapperMethod对象调用invoke方法。 // 我这里最终的MethodInvoker是PlainMethodInvoker return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } }
最终的invoke方法如下图所示:
@Override public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable { //这个execute太长,里面根据标签类型,来做下一步的操纵,这里我们放截图 return mapperMethod.execute(sqlSession, args); }
我们接着来跟executeForMany这个方法的执行:
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) { List<E> result; Object param = method.convertArgsToSqlCommandParam(args); // 默认的分页 if (method.hasRowBounds()) { RowBounds rowBounds = method.extractRowBounds(args); result = sqlSession.selectList(command.getName(), param, rowBounds); } else { // 会走DefaultSqlSession的selectList下面 result = sqlSession.selectList(command.getName(), param); } // issue #510 Collections & arrays support // 转换结果 if (!method.getReturnType().isAssignableFrom(result.getClass())) { if (method.getReturnType().isArray()) { return convertToArray(result); } else { return convertToDeclaredCollection(sqlSession.getConfiguration(), result); } } return result; }
public <E> List<E> selectList(String statement, Object parameter) { return this.selectList(statement, parameter, RowBounds.DEFAULT); }
// @Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { // 这个statement是方法引用:org.example.mybatis.StudentMapper.selectStudent // 通过这个key就可以从configuration获取构建的MappedStatement MappedStatement ms = configuration.getMappedStatement(statement); // query里面会判断结果是否在缓存里,我们没有引入缓存 // 最终会走的query中的queryFromDatabase方法。 // queryFromDatabase 里面会调用doQuery方法 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(); } }
我们这里重点来看doQuery方法:
@Override public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { // 这里我们其实已经可以看到MyBatis已经准备在调用JDBC了 // Statement 就位于JDBC中 Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); // 根据参数处理标签中的SQL StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 产生执行SQL的Statement stmt = prepareStatement(handler, ms.getStatementLog()); // 接着调query方法. 最终会走到PreparedStatementHandler的query方法上 return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } }
// 最终执行SQL public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; ps.execute(); return resultSetHandler.handleResultSets(ps); }
PreparedStatement是JDBC,到现在就已经开始调用JDBC执行SQL了。resultSetHandler是对JDBC结果进行处理的处理器。
这里我们把上面遇到的Handler大致梳理一下:
- StatementHandler: 语句处理器
- ResultSetHandler:结果处理器,有结果处理器就有参数处理器
- ParameterHandler: 参数处理器,