这个系列的文章的开篇《当咱们说起看源码时,咱们是在看什么》在去年十月份就开始了,明天开始填这个系列的坑。MyBatis是我接触的第一个ORM框架,也是我目前最为相熟的ORM框架,对它始终停留在用的阶段,明天尝试来看MyBatis的外部结构。如果还不会MyBatis的,能够先去看下《伪装是小白之重学MyBatis(一)》。
那该如何看源码呢?
我是把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.ConsoleAppenderlog4j.appender.CONSOLE.Encoding=UTF-8log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayoutlog4j.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->Configurationprivate 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这个过程中大抵做了什么:
@Overridepublic SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);}
留神这个getDefaultExecutorType, 这个事实是MyBatis分层中核心层的SQL执行器,咱们接着往下看openSessionFromDataSource:
// level 隔离级别, autoCommit 是否主动提交// ExecutorType 是一个枚举值: SIMPLE、REUSE、BATCHprivate 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办法如下图所示:
@Overridepublic 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: 参数处理器,
总结一下
在MyBatis中咱们写在xml文件中的SQL语句到底是怎么被执行这个问题到当初曾经有了答案:
- xml中的查问语句、属性会被事后的加载进入Configuration中,Configuration中有MappedStatements,这是一个Map,key是标签的id。
- 咱们在执行对应的Mapper的时候,首先要执行获取Session,在这个过程中会通过MyBatis的拦截器,咱们能够抉择在这个过程对MyBatis进行加强
- 调用接口对应的办法时, 事实上调用的时代理类的办法,代理类会先进行参数解决,依据办法签名获取MappedStatement,再转换,交给JDBC来解决。
到当初咱们曾经对MyBatis曾经有的执行流程曾经有一个大抵的理解了,可能一些办法没有看太细,因为讲那些细节也对宏观执行流程没有太大的帮忙。
参考资料
- MyBatis视频教程(高级篇) 视频 颜群 https://www.bilibili.com/vide...
- 玩转 MyBatis:深度解析与定制 https://juejin.cn/book/694491...