这里我们简要分析一下Mybatis的执行流程,内在的其他特性细节会有单独的文章进行分析;
贴一段简单的mybatis(3.4.5)使用时的测试代码
(配置文件省略了,如果要搭建环境推荐一个清晰的博客: https://www.cnblogs.com/xdp-gacl/p/4261895.html )
配置文件
mybatis.cfg.xml
<?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="application.properties"></properties>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 配置mybatis运行环境 -->
<environments default="cybatis">
<environment id="cybatis">
<!-- type="JDBC" 代表使用JDBC的提交和回滚来管理事务 -->
<transactionManager type="JDBC" />
<!-- mybatis提供了3种数据源类型,分别是:POOLED,UNPOOLED,JNDI(POOLED 表示支持JDBC数据源连接池) -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC"/>
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="./mapper/user.xml"/>
<mapper resource="./mapper/user-mapper.xml"/>
</mappers>
</configuration>
user.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="com.uu.bean.User" >
<resultMap id="User" type="com.uu.bean.User" >
<result column="id" property="id" jdbcType="BIGINT" />
<result column="roleId" property="roleId" jdbcType="BIGINT" />
<result column="userName" property="userName" jdbcType="VARCHAR" />
</resultMap>
<cache flushInterval="60000" size="1024" readOnly="false"/>
<select id="byroleId" resultType="com.uu.bean.User">
select * from user where roleId in
<foreach collection="collection" open="(" separator="," close=")" item="val">
${val}
</foreach>
</select>
</mapper>
user-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="mapper.UserMapper" >
<select id="getById" resultType="com.uu.bean.User" parameterType="int">
select * from user where id = #{id}
</select>
</mapper>
User.java
package com.uu.bean;
public class User implements java.io.Serializable{
private int id;
private int roleId;
private String userName;
//get set方法可以不写,Mybatis有自己的办法将值写入
}
测试示例代码
package test;
import java.io.IOException;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.uu.UserMapper;
public class T {
//通过namespace的方式执行sql
@Test
public void test() {
SqlSession sqlSession = sessionFactory.openSession();
Set a = new HashSet();
a.add(2);
a.add(3);
List<User> rs = sqlSession.selectList("com.uu.bean.User.byroleId",a);
for(User i:rs){
System.out.println(i);
}
}
//通过接口类的方式执行sql
@Test
public void test() {
SqlSessionFactory sqlSessionFactory = getSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
userMapper.deleteOne();
System.out.println("success");
sqlSession.commit();
}
// Mybatis 通过SqlSessionFactory获取SqlSession, 然后才能通过SqlSession与数据库进行交互
private static SqlSessionFactory getSessionFactory() {
SqlSessionFactory sessionFactory = null;
String resource = "mybatis-config.xml";
try {
sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader(resource));
} catch (IOException e) {
e.printStackTrace();
}
return sessionFactory;
}
}
SqlSessionFactory
代码入口是 SqlSessionFactoryBuilder
1、Resources.getResourceAsReader(resource) 是将配置文件解析封装成一个 java.io.Reader 对象,这个可以不用关注;
2、new SqlSessionFactoryBuilder 这一行代码也没有复杂的操作,只是返回一个 org.apache.ibatis.session.SqlSessionFactoryBuilder 对象;
3、构建SqlSessionFactory的逻辑在build里面,这个build方法里会创建一个XMLConfigBuilder对象,在执行 XMLConfigBuilder的构造方法时,会自动做DTD校验,如果配置不符合dtd的话,就会异常终止掉;
通过解析配置文件,并实例化 SqlSessionFactory对象
解析配置文件
配置文件在程序启动的时候会做一个全量的解析,这个过程被委托给下面流程
org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.Reader, java.lang.String, java.util.Properties)
↓
org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
↓
org.apache.ibatis.session.SqlSessionFactoryBuilder#build(org.apache.ibatis.session.Configuration)
返回值就是一个 SqlSessionFactory对象,核心是 XMLConfigBuilder#parse方法;这里封装了解析配置文件的模板流程
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
//解析properties路径
propertiesElement(root.evalNode("properties"));
//解析全局配置
Properties settings = settingsAsProperties(root.evalNode("settings"));
//虚拟文件系统相关(在通过package方式扫描获得mapper接口类时会使用该类)
loadCustomVfs(settings);
//日志打印相关
loadCustomLogImpl(settings);
//java类与resulttype,parametertype别名映射
typeAliasesElement(root.evalNode("typeAliases"));
//解析插件配置
pluginElement(root.evalNode("plugins"));
//返回值java对象实例化工厂
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
//解析env相关配置,eg.数据源相关配置
environmentsElement(root.evalNode("environments"));
//多数据库类型配置(更具当前的数据库种类和sql标记的databaseId属性执行不同的sql)
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//注册数据库类型与java类型映射关系处理器
typeHandlerElement(root.evalNode("typeHandlers"));
//解析mapper配置文件(sql文件)
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
settings
这里可以配置二级缓存,日志打印等配置
mappers
这里会解析全部sql语句配置,并将解析好的语句封装成 MappedStatement对象缓存在org.apache.ibatis.session.Configuration#mappedStatements中
配置mapper有3种方式:package、resource、url定位语句的资源路径
解析mapper配置的入口(Mybatis对应的配置文件解析类命名都很有特点的,解析Mapper的类中间会带有Mapper字样的XMLMapperBuilder,之前说的总配置文件解析器则是带Config的XMLConfigBuilder)
org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
代码来到:
org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//解析namespace
builderAssistant.setCurrentNamespace(namespace);
//解析二级缓存信息
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
//解析parameterMap
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//解析resultMap
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql标签
sqlElement(context.evalNodes("/mapper/sql"));
//解析crud标签
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);
}
}
这里可以看出公共标签<sql>会在各种crud标签之前被解析,这样设计为了满足 crud 标签里引用<sql>标签的需求;
我们重点还是看最后的crud标签,这里就需要语句相关的配置文件解析器(XMLStatementBuilder,看 类文件命名很有规律)来处理;
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
//配置文件中有多少个crud标签,那么list的大小就是多少
for (XNode context : list) {
//解析一个语句
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
最终调用的是:
org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
方法,将解析好的语句封装成 MappedStatement 对象,放入内存中
SqlSession 执行语句
SqlSession sqlSession = sqlSessionFactory.openSession();
最终调用的是
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSessionFromDataSource(ExecutorType, TransactionIsolationLevel, boolean)
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);
final Executor executor = configuration.newExecutor(tx, execType);
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();
}
}
这里创建了事务,并实例化了一个 Executor 对象(这里要说一句:如果我们在全局配置文件里设置了二级缓存可用的话,这个Executor将会是一个CachingExecutor对象,在CachingExecutor里将会引用一个SimpleExecutor对象,这就是用到了装饰器设计模式;如果关闭二级缓存的话,这里将会直接是一个 SimpleExecutor对象);
然后实例化一个 DefaultSqlSession 并返回,这应该是简单工厂设计模式;
namespace方式
使用namespace方式调用逻辑比较清晰,我们以selectList api为例子分析一下调用执行流程。
示例:List<User> rs = sqlSession.selectList("com.uu.bean.User.byroleId",param);
在执行过程中插件这个组件是不得不说的,插件的原理简单理解就是在程序里要获得某些特定的对象(目前来看有Executor,StatementHandler,ParameterHandler,ResultSetHandler)时,框架可以根据配置将这些类用代理封装起来(封装的逻辑可以看看org.apache.ibatis.plugin.Plugin#wrap 这个方法),然后在执行某些特定的方法时先执行代理的方法(这些规则都通过插件类上的 @Intercepts 注解来进行封装);
插件代理封装可以看看这里个方法
org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
org.apache.ibatis.session.Configuration#newStatementHandler
org.apache.ibatis.session.Configuration#newParameterHandler
org.apache.ibatis.session.Configuration#newResultSetHandler
代码简易流程跟踪:
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)
↓
org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)(如果开启二级缓存)
↓
org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
↓
org.apache.ibatis.executor.SimpleExecutor#doQuery
↓
org.apache.ibatis.executor.SimpleExecutor#prepareStatement
↓
org.apache.ibatis.executor.statement.SimpleStatementHandler#query
↓
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets
↓
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSet
↓
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleRowValuesForSimpleResultMap(这里将会循环包装么一个数据库返回值)
↓
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getRowValue(org.apache.ibatis.executor.resultset.ResultSetWrapper, org.apache.ibatis.mapping.ResultMap, java.lang.String)(这个方法会通过Mybatis提供的机制将数据库值设置进入pojo,即使pojo没有get set方法)
mapper接口方式
接下是通过Mapper接口的代理类来执行sql方式
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
最终调试调用到
org.apache.ibatis.binding.MapperRegistry.getMapper(Class<T>, SqlSession)
@SuppressWarnings("unchecked")
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);
}
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
看到这里就大概明白了Mapper对象是通过 JDK 提供的动态代理机制实现的。
代理逻辑重点就在MapperProxy 这个类里面了,后续查询逻辑与直接使用namespace方式的应该是一致的,主要区别是中间有个代理类,跟踪代码:
org.apache.ibatis.binding.MapperProxy#invoke
↓
org.apache.ibatis.binding.MapperMethod#execute
↓
以select为例子:
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);
//这里可以看到最终是使用的sqlSession的selectOne方法
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
这下清晰了