昨天的我是个小木匠
图文不符系列?
工作中经常用到MyBatis,刚开始学习的时候,只会使用,却不知道底层原理,最近看了《MyBatis技术内幕》还有Debug跟踪了一下源码,去学习它的底层思想。
MyBatis的官网介绍:
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。
图化两个流程
在Github中将源码拉下来,Debug了binding包下的BindingTest类,简单分析整个流程:
一、数据初始化阶段
二、查询阶段
源码分析
代码位置:org.apache.ibatis.binding.BindingTest
初始化
@BeforeClass
public static void setup() throws Exception {
// 创建一个没有池化的数据源
DataSource dataSource = BaseDataTest.createBlogDataSource();
// 数据源执行两个SQL脚本,第一个是清除数据,第二个是插入数据
BaseDataTest.runScript(dataSource, BaseDataTest.BLOG_DDL);
BaseDataTest.runScript(dataSource, BaseDataTest.BLOG_DATA);
// 创建jdbc事务工厂和环境
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("Production", transactionFactory, dataSource);
// 创建配置(配置巨多...),并且设置可延迟加载和注册类别名、添加两个Mapper资源
Configuration configuration = new Configuration(environment);
configuration.setLazyLoadingEnabled(true);
configuration.getTypeAliasRegistry().registerAlias(Blog.class);
configuration.getTypeAliasRegistry().registerAlias(Post.class);
configuration.getTypeAliasRegistry().registerAlias(Author.class);
configuration.addMapper(BoundBlogMapper.class);
configuration.addMapper(BoundAuthorMapper.class);
// 构建SqlSessionFactory的工厂,参数是configuration,返回的是DefaultSqlSession
sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
}
复制代码
Configuration配置参数
讲下配置巨多的Configuration: org.apache.ibatis.session.Configuration mybatis的所有配置信息都存储到Configuration类中,在初始化的时候进行加载
public class Configuration {
//环境
protected Environment environment;
//---------节点,一般从xml加载-------
protected boolean safeRowBoundsEnabled = false;
protected boolean safeResultHandlerEnabled = true;
protected boolean mapUnderscoreToCamelCase = false;
protected boolean aggressiveLazyLoading = true;
protected boolean multipleResultSetsEnabled = true;
protected boolean useGeneratedKeys = false;
protected boolean useColumnLabel = true;
//默认启用缓存
protected boolean cacheEnabled = true;
protected boolean callSettersOnNulls = false;
······
//类型处理器注册机
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
//类型别名注册机
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
//映射的语句,存在Map里
protected final Map mappedStatements = new StrictMap("Mapped Statements collection");
//缓存,存在Map里
protected final Map caches = new StrictMap("Caches collection");
//结果映射,存在Map里
protected final Map resultMaps = new StrictMap("Result Maps collection");
······
//不完整的SQL语句
protected final Collection incompleteStatements = new LinkedList();
······
// 初始化Configuration时会注册类型别名
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
······
}
}
复制代码
简单介绍它的属性对应的含义:
- Environment:配置了环境ID(决定加载哪种环境-生产OR开发)、事务工厂和数据源
- Settings: 主要是运行时的全局设置
- Registrys: 配置了很多注册器: ①:mapperRegistry,映射注册器,主要用来加载自定义mapper; ②:typeHandlerRegistry,类型处理器,主要用于保存JDBC Type,JAVA Type与处理器的映射关系,各个Handler主要继承BaseTypeHandler,同时用户可以继承自定义实现; ③:typeAliasRegistry,类型别名注册器,但是typeAliasRegister构造方法里面已经有很多基础包装类型,在构建configuration中为何还要加载?? ④:LanguageDriverRegistry,脚本语言注册器,在configuration初始化时,设定XMLLanguageDriver为默认驱动,并添加RawLanguageDriver驱动
创建SqlSessionFactory
在初始化的时候,创建SqlSessionFactory调用的方法是 org.apache.ibatis.session.SqlSessionFactoryBuilder.build(Configuration config)
/*
* Builds {@link SqlSession} instances.
* 构建SqlSessionFactory的工厂.工厂模式
*
*/
/**
* @author Clinton Begin
*/
public class SqlSessionFactoryBuilder {
······
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
/**
* 默认的SqlSessionFactory
*
*/
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
······
}
复制代码
到此,创建完sqlSessionFactory之后,初始化阶段就完成了。
查询阶段
简单查询?:
@Test
public void shouldSelectBlogWithPostsUsingSubSelect() throws Exception {
// 打开会话连接
SqlSession session = sqlSessionFactory.openSession();
try {
// 从会话中拿到mapper资源
BoundBlogMapper mapper = session.getMapper(BoundBlogMapper.class);
// 通过mapper接口进行查询
Blog b = mapper.selectBlogWithPostsUsingSubSelect(1);
// 剩下都是进行单元测试的校验···
assertEquals(1, b.getId());
session.close();
assertNotNull(b.getAuthor());
assertEquals(101, b.getAuthor().getId());
assertEquals("jim", b.getAuthor().getUsername());
assertEquals("********", b.getAuthor().getPassword());
assertEquals(2, b.getPosts().size());
} finally {
session.close();
}
}
复制代码
打开sqlSession会话连接
刚才创建的是DefaultSqlSessionFactory,最终打开会话会调用org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
// 从环境中取出,该方法中判断如果没有配置txFactory,则返回托管txFactory
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//通过事务工厂来产生一个事务,最终生成的是org.apache.ibatis.transaction.jdbc.JdbcTransaction,用来控制commit和rollback
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//生成一个执行器(事务包含在执行器里)
final Executor executor = configuration.newExecutor(tx, execType);
//然后产生一个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();
}
}
/**
* 默认SqlSession实现
*
*/
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
/**
* 是否自动提交
*/
private boolean autoCommit;
private boolean dirty;
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor;
this.dirty = false;
this.autoCommit = autoCommit;
}
public DefaultSqlSession(Configuration configuration, Executor executor) {
this(configuration, executor, false);
}
······
}
复制代码
到此,获取sqlSession连接就结束了
通过mapper接口进行查询
mapperProxy:映射器代理,代理模式
从刚才取出的sqlSession获得mapper资源,接着调用mapper的接口方法,会进入mapperProxy代理方法 org.apache.ibatis.binding.MapperProxy#invoke(有种AOP的切面思想,与Spring类似??)
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 这个判断是,如果这个方法是Object中的通用方法,则不需要执行mapperMethod
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
//从去缓存中找MapperMethod
final MapperMethod mapperMethod = cachedMapperMethod(method);
//执行
return mapperMethod.execute(sqlSession, args);
}
//去缓存中找MapperMethod
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
//找不到才去new
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
复制代码
mapperMethod:映射器方法
经过代理之后,进入org.apache.ibatis.binding.MapperMethod#execute执行
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//有4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法
if (SqlCommandType.INSERT == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
if (method.returnsVoid() && method.hasResultHandler()) {
//如果有结果处理器
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
//如果结果有多条记录
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
//如果结果是map
result = executeForMap(sqlSession, args);
} else {
//我们这次是查询方法,只查询一条记录
//转换参数
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
} else {
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
复制代码
接着进入DefaultSqlSession进行查询 org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)
@Override
public T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
// 去调用selectList,如果得到结果数量大于1,进行报错;不然返回第一条记录
// 特别需要主要的是当没有查询到结果的时候就会返回null。因此一般建议在mapper中编写resultType的时候使用包装类型
// 而不是基本类型,比如推荐使用Integer而不是int。这样就可以避免NPE(空指针异常)
List list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
@Override
public List selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//根据statement id找到对应的MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
//转而用执行器来查询结果,注意这里传入的ResultHandler是null
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();
}
}
private List selectList() throws SQLException {
Executor localExecutor = executor;
//如果executor已经被关闭了,则创建一个新的
if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {
localExecutor = newExecutor();
}
try {
// 从CachingExecutor.query中的delegate代理又调用一次Executor.query
return localExecutor. query(mappedStatement, parameterObject, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey, boundSql);
} finally {
if (localExecutor != executor) {
localExecutor.close(false);
}
}
}
复制代码
使用执行器进行查询 org.apache.ibatis.executor.BaseExecutor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)
@Override
public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
//如果已经关闭,报错
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//先清局部缓存,再查询.但仅查询堆栈为0,才清。为了处理递归调用
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List list;
try {
//加一,这样递归调用到上面的时候就不会再清局部缓存了
queryStack++;
//先根据cachekey从localCache去查
list = resultHandler == null ? (List) localCache.getObject(key) : null;
if (list != null) {
//若查到localCache缓存,处理localOutputParameterCache
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//从数据库查
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
//清空堆栈
queryStack--;
}
if (queryStack == 0) {
//延迟加载队列中所有元素
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
//清空延迟加载队列
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
//如果是STATEMENT,清本地缓存
clearLocalCache();
}
}
return list;
}
复制代码
查询到Object结果后,进入结果抽取器进行解析 org.apache.ibatis.executor.ResultExtractor#extractObjectFromList
public Object extractObjectFromList(List {
Object value = null;
if (targetType != null && targetType.isAssignableFrom(list.getClass())) {
//1.如果targetType是list,直接返回list
value = list;
} else if (targetType != null && objectFactory.isCollection(targetType)) {
//2.如果targetType是Collection,返回包装好的list
value = objectFactory.create(targetType);
MetaObject metaObject = configuration.newMetaObject(value);
metaObject.addAll(list);
} else if (targetType != null && targetType.isArray()) {
//3.如果targetType是数组,则数组转list
Class> arrayComponentType = targetType.getComponentType();
Object array = Array.newInstance(arrayComponentType, list.size());
if (arrayComponentType.isPrimitive()) {
for (int i = 0; i < list.size(); i++) {
Array.set(array, i, list.get(i));
}
value = array;
} else {
value = list.toArray((Object[])array);
}
} else {
//4.最后返回list的第0个元素
if (list != null && list.size() > 1) {
throw new ExecutorException("Statement returned more than one row, where no more than one was expected.");
} else if (list != null && list.size() == 1) {
value = list.get(0);
}
}
return value;
}
复制代码
返回结果Object后,经过PropertyTokenizer(属性分词器)、BeanWrapper(Bean包装器)进行metaResultObject结果设置: org.apache.ibatis.reflection.wrapper.BeanWrapper#set
@Override
public void set(PropertyTokenizer prop, Object value) {
//如果有index,说明是集合,那就要解析集合,调用的是BaseWrapper.resolveCollection 和 setCollectionValue
if (prop.getIndex() != null) {
Object collection = resolveCollection(prop, object);
setCollectionValue(prop, collection, value);
} else {
//否则,setBeanProperty
setBeanProperty(prop, object, value);
}
}
private void setBeanProperty(PropertyTokenizer prop, Object object, Object value) {
try {
//得到setter方法,然后调用(通过反射org.apache.ibatis.reflection.Reflector#getSetInvoker)
Invoker method = metaClass.getSetInvoker(prop.getName());
Object[] params = {value};
try {
// method执行org.apache.ibatis.reflection.invoker.MethodInvoker#invoke
method.invoke(object, params);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
} catch (Throwable t) {
throw new ReflectionException("Could not set property '" + prop.getName() + "' of '" + object.getClass() + "' with value '" + value + "' Cause: " + t.toString(), t);
}
}
复制代码
调用到这个地方,其实已经从blog表中查询到一条blog记录,然后由于mapper文件设定的sql语句是:
<resultMap id="blogWithPosts" type="Blog">
<id property="id" column="id"/>
<result property="title" column="title"/>
<association property="author" column="author_id"
select="selectAuthorWithInlineParams"/>
<collection property="posts" column="id" select="selectPostsForBlog"/>
resultMap>
<select id="selectBlogWithPostsUsingSubSelect" parameterType="int" resultMap="blogWithPosts">
select * from Blog where id = #{id}
select>
<select id="selectAuthorWithInlineParams"
parameterType="int"
resultType="org.apache.ibatis.domain.blog.Author">
select * from author where id = #{id}
select>
<select id="selectPostsForBlog" parameterType="int" resultType="Post">
select * from Post where blog_id = #{blog_id}
select>
复制代码
官网解释节点含义:
- constructor :用于在实例化类时,注入结果到构造方法中
- idArg :ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
- arg : 将被注入到构造方法的一个普通结果
- id : 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能
- result : 注入到字段或 JavaBean 属性的普通结果
- association : 一个复杂类型的关联;许多结果将包装成这种类型 嵌套结果映射 : 关联可以指定为一个 resultMap 元素,或者引用一个
- collection : 一个复杂类型的集合 嵌套结果映射 : 集合可以指定为一个 resultMap 元素,或者引用一个
- discriminator : 使用结果值来决定使用哪个 resultMap
- case : 基于某些值的结果映射 嵌套结果映射 : 一个 case 也是一个映射它本身的结果,因此可以包含很多相 同的元素,或者它可以参照一个外部的 resultMap。
可以看出,这条查询语句涉及到三个表,所以第一次查询到blog表中的一条记录后,循环调用查询流程,进行一个author查询和posts集合查询
将所有查询结果汇总: org.apache.ibatis.executor.loader.ResultLoaderMap#loadAll
public boolean load(String property) throws SQLException {
//先删除key,防止第二次又去查数据库就不对了
LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
if (pair != null) {
//去数据库查
pair.load();
return true;
}
return false;
}
public void loadAll() throws SQLException {
final Set methodNameSet = loaderMap.keySet();
String[] methodNames = methodNameSet.toArray(new String[methodNameSet.size()]);
// 方法列表
for (String methodName : methodNames) {
load(methodName);
}
}
复制代码
最后通过Javassist进行延迟加载: org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory.EnhancedResultObjectProxyImpl#invoke
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
final String methodName = method.getName();
try {
synchronized (lazyLoader) {
if (WRITE_REPLACE_METHOD.equals(methodName)) {
Object original = null;
if (constructorArgTypes.isEmpty()) {
original = objectFactory.create(type);
} else {
original = objectFactory.create(type, constructorArgTypes, constructorArgs);
}
PropertyCopier.copyBeanProperties(type, enhanced, original);
if (lazyLoader.size() > 0) {
return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
} else {
return original;
}
} else {
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
lazyLoader.loadAll();
} else if (PropertyNamer.isProperty(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
if (lazyLoader.hasLoader(property)) {
lazyLoader.load(property);
}
}
}
}
}
return methodProxy.invoke(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
复制代码
好吧,这个太底层了,这个我没看懂,有了解的小伙伴请跟我说说~~??
结果返回之后,就要进行会话连接中断之类的就不用说了。好了,整个调用链路都清晰了,只能说,发明这个持久化框架的Clinton Begin大神??!
挖坑
先挖个坑,下一篇要学MyBatis的分页组件(虽然GitHub已经有,原则是不要重复造轮子,但是只学习了调用原理,还是多去深入学习一下吧~)
参考资料: ①:Mybatis源码分析(一)- Configuration配置文件详解 ②:mybatis源码中文注释 ③:《MyBatis技术内幕》--徐郡明