停更了一个多月,博主一直在忙于技术的学习与工作的繁忙之间,其实更多的是迷茫于技术中,但是想想还是要把Mybatis系列继续更新下去。博主也给自己在20年立了几个flag:深入学习Java、研究研究c++、第三个就是健身咯,只要是因为回首2019年,感觉自己在技术学习的道路上是在没走多少,因此想着这次必须要对自己狠一点了。
先不多说了,回顾一下前一篇文章,由于时隔太久,笔者都有点记不清了。在前一篇文章中简单的说了一下Mybatis获取Mapper代理对象的流程,当获取到mapper的代理对象后,便是SQLSession的执行流程了,这个稍微熟悉Mybatis的都是了解的。但我们学习Mybatis的时候,只是了解它的一个执行流程,对其内部的代码原理却并不了解,那么接下来就带大家来看看Mybatis中BoundSql获取流程。
在执行完SQLSession的流程后,便会进入SQL语句的执行过程了,但是此时我们的SQL还并不是一句可执行的语句,它需要经过参数绑定后才能进行真正的执行,上一篇文章我们说到了关于getBoundSQL的代码入口,如下:
@Override
public List selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 根据传入的statementId,获取MappedStatement对象
MappedStatement ms = configuration.getMappedStatement(statement);
// 调用执行器的查询方法
// RowBounds是用来逻辑分页(按照条件将数据从数据库查询到内存中,在内存中进行分页)
// wrapCollection(parameter)是用来装饰集合或者数组参数
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();
}
}
从上面的代码我们似乎并没有看到getBoundSQL代码的字样,但是在前面我们说了,getBoundSQL是在query执行的前做的,那我们来看看上面的query方法,当你在按住Ctr后点击鼠标时,编辑器会提示你这个方法它有两个实现一个是BaseExecutor类中,一个在CachingExecutor类中,但是不管是这两个里面的哪个类,当你最后跟到的代码还是在MappedStatement这个类中。那我这里就以BaseExecutor中的代码为例了,代码如下:
@Override
public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
在上面的代码中,看到了getBoundSql这个方法,但是这里并不是getBoundSql的核心代码,那继续看下去吧。代码如下:
public BoundSql getBoundSql(Object parameterObject) {
//
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
// check for nested result maps in parameter mappings (issue #30)
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}
return boundSql;
}
上面代码中的逻辑,首先是通过parameterObject参数去获取boundSql对象,然后再通过boundSql去获取parameterMappings这个集合,若所获取到的集合null或为空,那么则new一个BoundSql对象,不管是获取到的还是创建出来的,最后都要进行一个遍历的过程。在遍历的过程中,需要通过rmId来到configuration对象中获取ResultMap对象,如果能获取到便进行这样一个运算hasNestedResultMaps |= rm.hasNestedResultMaps(),关于|=运算符,感兴趣的朋友可以自己去俩复习一下。其实就是哪rm.hasNestedResultMaps()写的二进制去和hasNestedResultMaps 的二进制进行一个运算。
由于我们此时要说的重点是getBoundSql,那么我们继续跟进去看看,此时在你按住Ctr后点击鼠标时,编辑器会提示你这个方法它在以下几个类中有实现:DynamicSqlSource、ProviderSqlSource、RawSqlSource、StaticSqlSource、VelocitySqlSource,在这这几个类中ProviderSqlSource已经被弃用,但是他们都是去实现了SqlSource这个类,StaticSqlSource中的getBoundSql归根结底还是new一个BoundSql对象,DynamicSqlSource和VelocitySqlSource类中的实现稍微复杂点,这里我们就以DynamicSqlSource这个类的实现为例,代码如下:
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
// 创建SQL信息解析器
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
// 获取入参类型
Class> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 执行解析:将带有#{}的SQL语句进行解析,然后封装到StaticSqlSource中
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 将解析后的SQL语句还有入参绑定到一起(封装到一个对象中,此时还没有将参数替换到SQL占位符?)
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
}
在上面的代码中首先通过configuration对象和传入的参数parameterObject来创建一个DynamicContext对象,然后把这个上下文对象交给rootSqlNode,其实就是一个SqlNode节点,这个节点类主要实现的就是SQL语句中一些类似if、foreach等标签的节点使用。当把上下文对象交给rootSqlNode后,又通过configuration对象来创建一个SqlSourceBuilder对象,这个对象是个SQL信息解析器,主要是用于带有#{}的SQL语句进行解析,然后封装到StaticSqlSource中,最后通过sqlSource来获取BoundSql,然后遍历BoundSql,来进行参数赋值,最后返回boundSql对象。
其实上面那段代码中,其主要的核心代码是parse方法,接下来我们继续看看看这个方法。
public SqlSource parse(String originalSql, Class> parameterType, Map additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType,
additionalParameters);
// 创建分词解析器
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
// 解析#{}
String sql = parser.parse(originalSql);
// 将解析之后的SQL信息,封装到StaticSqlSource对象中
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
代码很简单,大体步骤就是:
ParameterMappingTokenHandler其实是一个内部类,它继承了BaseBuilder,并且实现了TokenHandler,我们简单看下它的构造函数:
public ParameterMappingTokenHandler(Configuration configuration, Class> parameterType,
Map additionalParameters) {
super(configuration);
this.parameterType = parameterType;
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
这个构造函数里调了父类的方法,然后通过configuration创建了一个newMetaObject。
GenericTokenParser这个类主要就是分词解析器,在它的构造函数里指定了待分析的openToken和closeToken,并指定处理 器。
public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}
关于解析#{}代码比较复杂,我们直接看代码:
/**
* 解析${}和#{}
* @param text
* @return
*/
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token
int start = text.indexOf(openToken, 0);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
offset = end + closeToken.length();
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}
这段代码主要就是解析${}和#{}的,具体细节童鞋们可以自己debug时细细品味,其实说到这里本篇文章也将要结束了,本篇文章主要讲了getBoundSql和执行解析。顺便补一下RawSqlSource的类实现。
public class RawSqlSource implements SqlSource {
private final SqlSource sqlSource;
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class> parameterType) {
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
public RawSqlSource(Configuration configuration, String sql, Class> parameterType) {
// 解析SQL语句
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
// 获取入参类型
Class> clazz = parameterType == null ? Object.class : parameterType;
// 开始解析
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap());
}
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
rootSqlNode.apply(context);
return context.getSql();
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
}
其实其原理大同小异,只是因为不同的需求实现方式有些诧异。
接下来和大家简单总结一下这个代码执行流程: