Mybatis运行流程源码分析

Mybatis运行流程源码分析

SqlSessionFactory构建

以XML文件配置为例
这一步就是创建XMLParser解析主配置文件以及所有mapper文件的过程,所有的相关信息都被保存在Configuration对象中,最终返回一个DefaultSqlSessionFactory对象。
parseConfig.png
依次解析每一个节点及其子节点

对于每一个mapper文件,都有对应的XMLMapperParser解析
mapperParse.png

在XMLStatementBuiler对象中,解析了每个增删改查标签的所有属性,构建了MappedStatement对象。
mappedStatement.png
我们在mapper中写的每个CRUD标签,都被解析成了MappedStatement对象,该对象的属性较多,可自行源码查看。

configration.png
最终Configration对象包含了所有XML配置中的信息,我们编写的Mapper接口最终是由MapperProxyFactory生产为JD动态代理的MapperProxy对象。

整个XML文件解析不是我们分析的重点,了解大概的流程即可。

openSession获取SqlSession对象

这一步只需要分析DefaultSqlSessionFactory对象了,openSession方法的主要逻辑如下
openSession.png
executeType有三种,SIMPLE、REUSE、BATCH,Excutor对象是非常重要的一个类,顾名思义为SQL执行器。
进入newExecutor方法
newExcutor.png

  1. 根据配置的executorType创建不同的Executor对象,如果启用了二级缓存,会包装成CachingExecutor。
  2. 安装了插件,第一次出现。

最终返回了一个SqlSessiond对象,进入SqlSession类中查看增删改查方法,发现其最终调用了excutor成员的方法。

getMapper获取MapperProxy

  1. SqlSession的getMapper方法调用其成员Configuration的getMapper
  2. Configuration的getMapper方法调用MapperRegistry的方法,MapperRegistry中的knownMappers以map方式存储了所有mapper接口,key为对应Mapper接口的Class对象,值为前面提到的MapperProxyFactory对象。
  3. 通过JDK动态代理生成一个MapperProxy对象,代理对应接口的方法。
    getMapper.png

MapperProxyFactory类的源码非常简单,感兴趣可自行查看。

Mapper方法执行流程

动态代理invoke方法

调用Mapper的方法后,直接进入JDK动态代理的invoke方法

try {
  if (Object.class.equals(method.getDeclaringClass())) {
	/* Object类的方法,如equals、toString等 */
	return method.invoke(this, args);
  } else {
	/* 找到匹配的MapperMethod,调用其invoke方法 */
	return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
  }
} catch (Throwable t) {
  throw ExceptionUtil.unwrapThrowable(t);
}

执行mapperMethod

判断该方法的sql语句类型及返回数据类型,然后执行SqlSession封装的对应方法。其中,convertArgsToSqlCommandParam解析方法参数
mapperMethod的invoke.png

参数解析

final int paramCount = names.size();
if (args == null || paramCount == 0) {
  return null;
} else if (!hasParamAnnotation && paramCount == 1) {
  Object value = args[names.firstKey()];
  return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
} else {
  final Map<String, Object> param = new ParamMap<>();
  int i = 0;
  for (Map.Entry<Integer, String> entry : names.entrySet()) {
	param.put(entry.getValue(), args[entry.getKey()]);
	// add generic param names (param1, param2, ...)
	final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
	// ensure not to overwrite parameter named with @Param
	if (!names.containsValue(genericParamName)) {
	  param.put(genericParamName, args[entry.getKey()]);
	}
	i++;
  }
  return param;
}

参数有多个或者包含@Param注解后会被封装成map,之后再调用wrapIfCollection方法,否则直接调用

public static Object wrapToMapIfCollection(Object object, String actualParamName) {
	if (object instanceof Collection) {
	  ParamMap<Object> map = new ParamMap<>();
	  map.put("collection", object);
	  if (object instanceof List) {
		map.put("list", object);
	  }
	  Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
	  return map;
	} else if (object != null && object.getClass().isArray()) {
	  ParamMap<Object> map = new ParamMap<>();
	  map.put("array", object);
	  Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
	  return map;
	}
	return object;
}

可以看到,collection,array都是直接写死放入map的

执行sqlSession封装的CRUD方法

通常我们使用都是获取Mapper代理对象后调用其方法,Mybatis会解析调用的方法的全限定、和参数信息,再调用sqlSession的方法。
我们也可以直接调用sqlSession的方法,本质上没有区别,只是前者方式下程序员的工作量更小了。

根据方法的全限定名获取MappedStatement对象,再由excutor执行

获取statement.png

依据sql语句、参数等创建缓存的key

创建缓存key.png

  • ms:MappedStatement对象,此时s所有#{param}使用?占位符替代
  • parameterObject:参数信息
  • rowBounds:逻辑分页
  • resultHandler:实现ResultHandler接口,返回结果的钩子
  • key: 缓存key中包含了mapper方法信息、sql语句、参数、逻辑分页、数据库标识,开发环境等,如下图所示
  • boundSql:sql语句相关信息

cacheKey.png

首先查询二级缓存

public <E> List<E> query(MappedStatement ms, Object parameterObject, 
	RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
	  throws SQLException {
	/* statement的二级缓存,在对应的mapper中配置了cache后才有效 */
	Cache cache = ms.getCache();
	if (cache != null) {
	  flushCacheIfRequired(ms);
	  if (ms.isUseCache() && resultHandler == null) {
		ensureNoOutParams(ms, boundSql);
		@SuppressWarnings("unchecked")
		/* 从该mapper对应的cache中获取 */
		List<E> list = (List<E>) tcm.getObject(cache, key);
		if (list == null) {
		  list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
	          /* 并未真正进如缓存,事务提交之后才进入缓存 */
		  tcm.putObject(cache, key, list); // issue #578 and #116
		}
		return list;
	  }
	}
	/* 二级缓存无效,走Executor的方法 */
	return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

二级缓存由TransactionalCacheManager管理,put时,会调用TransactionalCache的put方法将待缓存对象放到entriesToAddOnCommit中,是一个Map对象,当该会话关闭或事务提交时,会调用TransactionalCache的flushPendingEntries真正缓存到delegate对象中,这才是真正的缓存区。
具体可查Mybatis缓存

一级缓存与数据库查询

二级缓存未命中,进入以下代码

/* 清除缓存 */
if (queryStack == 0 && ms.isFlushCacheRequired()) {
  clearLocalCache();
}
List<E> list;
try {
  queryStack++;
  /* 尝试一级缓存 */
  list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
  if (list != null) {
	handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
  } else {
	/* 走数据库 */
	list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }
} finally {
  queryStack--;
}

默认一级缓存的实现是PerpetualCache,简单的哈希表

从数据库查询

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
	List<E> list;
	/* 标志正在执行sql查询,占位符 */
	localCache.putObject(key, EXECUTION_PLACEHOLDER);
	try {
	  /* 查数据库 */
	  list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
	} finally {
	  localCache.removeObject(key);
	}
        /* 结果放入缓存  */
	localCache.putObject(key, list);
	if (ms.getStatementType() == StatementType.CALLABLE) {
	  localOutputParameterCache.putObject(key, parameter);
	}
	return list;
}

可以看到,每个sqlSession会有一个自己的localCache,缓存使用哈希表实现,这就解释了为什么一级缓存只在会话中有效。

原生jdbc查询

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    /* jdbc Statement */
    Statement stmt = null;
    try {
      /* 全局config */
      Configuration configuration = ms.getConfiguration();
      /* StatementHandler接口,分阶段对statement处理 */
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      /* 原生jdbc statement预编译 */
      stmt = prepareStatement(handler, ms.getStatementLog());
      /* 送去执行 */
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
}
newStatementHandler方法
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
 }
  • 在 RoutingStatementHandler方法中,判断statement的type类型,XML配置中的statementType,根据配置可以返回
    • SimpleStatementHandler,对应XML中的statement
    • PreparedStatementHandler,对应XML中PREPARED,默认
    • CallableStatementHandler,对应CALLABLE,存储过程
  • 在这里,再次出现了pluginChain,安装插件,第二次出现
    上述StatementHandler三者全部继承自BaseStatementHandler,在BaseStatementHandler的构造方法中,初始化了两个成员

baseStatementHandler.png
在这两个成员的构造函数中,有以下代码

resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);

parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);

至此,pluginAll出现四次

prepareStatement方法
private Statement prepareStatement(StatementHandler handler, Log statementLog) 
throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
 }
  1. 获取连接,根据statement的参数配置,可能获取不同连接,例如,不同数据库
  2. 准备阶段,设置fetchSize、timeOut等
  3. 填充参数,依赖Parameterhandler完成在这个阶段会使用到typeHandler、JdbcType完成参数类型映射

该部分就是对原生jdbc操作令人难受的部分的操作。
至此,等待数据库返回结果,整个查询流程结束了。
statement到此准备完毕了,可以送去执行了。

送到数据库执行

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
}

执行的返回结果由ResultSetHandler处理后返回。

总结

待续。。。
关于四次pluginAll方法,插件原理,请Mybatis插件原理

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值