- 三哥
内容来自【自学星球】
欢迎大家来了解我的星球,和星主(也就是我)一起学习 Java ,深入 Java 体系中的所有技术。我给自己定的时间是一年,无论结果如何,必定能给星球中的各位带来点东西。
想要了解更多,欢迎访问👉:自学星球
--------------SSM系列源码文章及视频导航--------------
创作不易,望三连支持!
SSM源码解析视频
👉点我
Spring
- Spring 中注入 Bean 的各种骚操作做
- Spring 中Bean的生命周期及后置处理器使用
- Spring 中容器启动分析之refresh方法执行之前
- Spring refresh 方法分析之一
- Spring refresh 方法之二 invokeBeanFactoryPostProcessors 方法解析
- Spring refresh 方法分析之三
- Spring refresh 方法之四 finishBeanFactoryInitialization 分析
- Spring AOP源码分析一
- Spring AOP源码分析二
- Spring 事务源码分析
SpringMVC
MyBatis
- MyBatis 源码分析之 SqlSessionFactory 创建
- MyBatis 源码分析之 SqlSession 创建
- MyBatis 源码分析之 Mapper 接口代理对象生成及方法执行
- MyBatis 源码分析之 Select 语句执行(上)
- MyBatis 源码分析之 Select 语句执行(下)
- MyBatis 源码分析一二级缓存
---------------------【End】--------------------
一、Select 语句执行
接上回分析:
result = sqlSession.selectOne(command.getName(), param);
org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
// 调用 selectList 获取结果
List<T> list = this.<T>selectList(statement, parameter);
if (list.size() == 1) {
// 返回结果
return list.get(0);
} else if (list.size() > 1) {
// 如果查询结果大于1则抛出异常
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
该方法会调用 selectList 获取结果,然后对结果进行判断,结果大于 1 则报错反之则返回结果。
下面我们来看看 selectList 方法的实现。
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)
public <E> List<E> selectList(String statement, Object parameter) {
// 调用重载方法
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 通过MappedStatement的Id获取 MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// 调用 Executor 实现类中的 query 方法
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();
}
}
在从 configuration 中获取 MappedStatement 之后就直接交给 Executor 的 query 方法执行了。
Executor 的创建可以回顾,4.2 节,下面我们来看看 query 方法的源码。
org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取 BoundSql
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建 CacheKey
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 调用重载方法
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
该方法不是真正的查询方法,但查询之前做了两个很重要的步骤:
- 获取 SQL
- 创建缓存 key
下面我们分析获取 BoundSql 。
1.1 获取 BoundSql
// 获取 BoundSql
BoundSql boundSql = ms.getBoundSql(parameterObject);
调用了MappedStatement的getBoundSql方法,并将运行时参数传入其中,我们大概的猜一下,这里是不是拼接SQL语句呢,并将运行时参数设置到SQL语句中?
我们都知道 SQL 是配置在映射文件中的,但由于映射文件中的 SQL 可能会包含占位符 #{},以及动态 SQL 标签,比如 、 等。因此,我们并不能直接使用映射文件中配置的 SQL。MyBatis 会将映射文件中的 SQL 解析成一组 SQL 片段。我们需要对这一组片段进行解析,从每个片段对象中获取相应的内容。然后将这些内容组合起来即可得到一个完成的 SQL 语句,这个完整的 SQL 以及其他的一些信息最终会存储在 BoundSql 对象中。下面我们来看一下 BoundSql 类的成员变量信息,如下:
public class BoundSql {
// 一个完整的 SQL 语句,可能会包含问号 ? 占位符
private final String sql;
// 参数映射列表,SQL 中的每个 #{xxx} 占位符都会被解析成相应的 ParameterMapping 对象
private final List<ParameterMapping> parameterMappings;
// 运行时参数,即用户传入的参数,比如 Article 对象,或是其他的参数
private final Object parameterObject;
// 附加参数集合,用于存储一些额外的信息,比如 datebaseId 等
private final Map<String, Object> additionalParameters;
// additionalParameters 的元信息对象
private final MetaObject metaParameters;
}
接下来我们接着MappedStatement 的 getBoundSql 方法,源码如下:
org.apache.ibatis.mapping.MappedStatement#getBoundSql
public BoundSql getBoundSql(Object parameterObject) {
// 获取BoundSql对象,BoundSql对象是对动态sql的解析
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 获取参数映射
List<ParameterMapping> 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;
}
MappedStatement 的 getBoundSql 在内部调用了 SqlSource 实现类的 getBoundSql 方法,并把 method 运行时参数传进去,SqlSource 是一个接口,它有如下几个实现类:
- DynamicSqlSource
- RawSqlSource
- StaticSqlSource
- ProviderSqlSource
- VelocitySqlSource
当 SQL 配置中包含 ${}(不是 #{})占位符,或者包含 、 等标签时,会被认为是动态 SQL,此时使用 DynamicSqlSource 存储 SQL 片段。否则,使用 RawSqlSource 存储 SQL 配置信息。我们来看看 DynamicSqlSource的 getBoundSql 。
org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql
public BoundSql getBoundSql(Object parameterObject) {
// 创建 DynamicContext
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 解析 SQL 片段,并将解析结果存储到 DynamicContext 中,
// 这里会将${}替换成method对应的运行时参数,也会解析<if><where>等SqlNode
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
/*
* 构建 StaticSqlSource,在此过程中将 sql 语句中的占位符 #{} 替换为问号 ?,
* 并为每个占位符构建相应的 ParameterMapping
*/
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 调用 StaticSqlSource 的 getBoundSql 获取 BoundSql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 将 DynamicContext 的 ContextMap 中的内容拷贝到 BoundSql 中
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
总结一下该方法的执行步骤:
- 创建 DynamicContext
- 解析 SQL 片段,并将解析结果存储到 DynamicContext 中
- 解析 SQL 语句,并构建 StaticSqlSource
- 调用 StaticSqlSource 的 getBoundSql 获取 BoundSql
- 将 DynamicContext 的 ContextMap 中的内容拷贝到 BoundSql
1.1.1 DynamicContext
DynamicContext 是 SQL 语句构建的上下文,每个 SQL 片段解析完成后,都会将解析结果存入 DynamicContext 中。待所有的 SQL 片段解析完毕后,一条完整的 SQL 语句就会出现在 DynamicContext 对象中。
public class DynamicContext {
public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";
//bindings 则用于存储一些额外的信息,比如运行时参数
private final ContextMap bindings;
//sqlBuilder 变量用于存放 SQL 片段的解析结果
private final StringBuilder sqlBuilder = new StringBuilder();
public DynamicContext(Configuration configuration, Object parameterObject) {
// 创建 ContextMap,并将运行时参数放入ContextMap中
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
// 存放运行时参数 parameterObject 以及 databaseId
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
public void bind(String name, Object value) {
bindings.put(name, value);
}
//拼接Sql片段
public void appendSql(String sql) {
sqlBuilder.append(sql);
sqlBuilder.append(" ");
}
//得到sql字符串
public String getSql() {
return sqlBuilder.toString().trim();
}
//继承HashMap
static class ContextMap extends HashMap<String, Object> {
private MetaObject parameterMetaObject;
public ContextMap(MetaObject parameterMetaObject) {
this.parameterMetaObject = parameterMetaObject;
}
@Override
public Object get(Object key) {
String strKey = (String) key;
// 检查是否包含 strKey,若包含则直接返回
if (super.containsKey(strKey)) {
return super.get(strKey);
}
if (parameterMetaObject != null) {
// issue #61 do not modify the context when reading
// 从运行时参数中查找结果,这里会在${name}解析时,通过name获取运行时参数值,替换掉${name}字符串
return parameterMetaObject.getValue(strKey);
}
return null;
}
}
// 省略部分代码
}
1.1.2 解析 SQL 片段
接着我们来看看解析SQL片段的逻辑
rootSqlNode.apply(context);
对于一个包含了 ${} 占位符,或 、 等标签的 SQL,在解析的过程中,会被分解成多个片段。每个片段都有对应的类型,每种类型的片段都有不同的解析逻辑。在源码中,片段这个概念等价于 sql 节点,即 SqlNode。
SqlNode 是一个接口,其有很多种实现,类基础图如下:
-
StaticTextSqlNode 用于存储静态文本
-
TextSqlNode 用于存储带有 ${} 占位符的文本
-
IfSqlNode 则用于存储 节点的内容
-
MixedSqlNode 内部维护了一个 SqlNode 集合,用于存储各种各样的 SqlNode
接下来,我将会对 MixedSqlNode 、StaticTextSqlNode、TextSqlNode、IfSqlNode、WhereSqlNode 以及 TrimSqlNode 等进行分析。
MixedSqlNode
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
// 遍历 SqlNode 集合
for (SqlNode sqlNode : contents) {
// 调用 salNode 对象本身的 apply 方法解析 sql
sqlNode.apply(context);
}
return true;
}
}
MixedSqlNode 可以看做是 SqlNode 实现类对象的容器,凡是实现了 SqlNode 接口的类都可以存储到 MixedSqlNode 中,包括它自己。MixedSqlNode 解析方法 apply 逻辑比较简单,即遍历 SqlNode 集合,并调用其他 SqlNode实现类对象的 apply 方法解析 sql。
StaticTextSqlNode
public class StaticTextSqlNode implements SqlNode {
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
@Override
public boolean apply(DynamicContext context) {
//直接拼接当前sql片段的文本到DynamicContext的sqlBuilder中
context.appendSql(text);
return true;
}
}
StaticTextSqlNode 用于存储静态文本,直接将其存储的 SQL 的文本值拼接到 DynamicContext 的sqlBuilder中即可。
TextSqlNode
public class TextSqlNode implements SqlNode {
private final String text;
private final Pattern injectionFilter;
@Override
public boolean apply(DynamicContext context) {
// 创建 ${} 占位符解析器
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
// 解析 ${} 占位符,通过ONGL 从用户传入的参数中获取结果,替换text中的${} 占位符
// 并将解析结果的文本拼接到DynamicContext的sqlBuilder中
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
// 创建占位符解析器
return new GenericTokenParser("${", "}", handler);
}
private static class BindingTokenParser implements TokenHandler {
private DynamicContext context;
private Pattern injectionFilter;
public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
this.context = context;
this.injectionFilter = injectionFilter;
}
@Override
public String handleToken(String content) {
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
// 通过 ONGL 从用户传入的参数中获取结果
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
// 通过正则表达式检测 srtValue 有效性
checkInjection(srtValue);
return srtValue;
}
}
}
GenericTokenParser 是一个通用的标记解析器,用于解析形如 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-89pa28b1-1668829789564)(https://math.jianshu.com/math?formula=%7Bname%7D%EF%BC%8C%23%7Bid%7D%20%E7%AD%89%E6%A0%87%E8%AE%B0%E3%80%82%E6%AD%A4%E6%97%B6%E6%98%AF%E8%A7%A3%E6%9E%90)]{name}的形式,从运行时参数的Map中获取到key为name的值,直接用运行时参数替换掉 ${name}字符串,将替换后的text字符串拼接到DynamicContext的sqlBuilder中
举个例子吧,比喻我们有如下SQL
SELECT * FROM user WHERE name = '${name}' and id= ${id}
假如我们传的参数 Map中name值为 chenhao,id为1,那么该 SQL 最终会被解析成如下的结果:
SELECT * FROM user WHERE name = 'chenhao'; DROP TABLE user;#'
由于传入的参数没有经过转义,最终导致了一条 SQL 被恶意参数拼接成了两条 SQL。这就是为什么我们不应该在 SQL 语句中是用 ${} 占位符,风险太大。
IfSqlNode
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator;
private final String test;
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
@Override
public boolean apply(DynamicContext context) {
// 通过 ONGL 评估 test 表达式的结果
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 若 test 表达式中的条件成立,则调用其子节点节点的 apply 方法进行解析
// 如果是静态SQL节点,则会直接拼接到DynamicContext中
contents.apply(context);
return true;
}
return false;
}
}
IfSqlNode 对应的是 节点,首先是通过 ONGL 检测 test 表达式是否为 true,如果为 true,则调用其子节点的 apply 方法继续进行解析。如果子节点是静态SQL节点,则子节点的文本值会直接拼接到DynamicContext中
好了,其他的 SqlNode 我就不一一分析了,大家有兴趣的可以去看看
1.1.3 解析 #{} 占位符
经过前面的解析,我们已经能从 DynamicContext 获取到完整的 SQL 语句了。但这并不意味着解析过程就结束了,因为当前的 SQL 语句中还有一种占位符没有处理,即 #{}。与 ${} 占位符的处理方式不同,MyBatis 并不会直接将 #{} 占位符替换为相应的参数值,而是将其替换成?。其解析是在如下代码中实现的
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
org.apache.ibatis.builder.SqlSourceBuilder#parse
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 创建 #{} 占位符处理器
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// 创建 #{} 占位符解析器
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
// 解析 #{} 占位符,并返回解析结果字符串
String sql = parser.parse(originalSql);
// 封装解析结果到 StaticSqlSource 中,并返回,因为所有的动态参数都已经解析了,可以封装成一个静态的SqlSource
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
该方法通过创建占位符的处理器和解析器来对 #{} 进行处理,处理方法在解析器中的 parse 进行处理,最后将解析出来的 ParameterMappings 最为参数传进 StaticSqlSource 对象进行返回。
下面来看看 parse 方法
org.apache.ibatis.parsing.GenericTokenParser#parse
public String parse(String text) {
// 忽略代码
// 如果碰到 #{} 字样的字符串 则会调用 下面方法进行解析
handler.handleToken(expression.toString());
// 忽略代码
// 返回解析完成的 sql
return builder.toString();
}
下面来看看 handleToken 方法。
org.apache.ibatis.builder.SqlSourceBuilder.ParameterMappingTokenHandler#handleToken
public String handleToken(String content) {
// 解析
parameterMappings.add(buildParameterMapping(content));
// 返回 ? 号替换原来的字符串
return "?";
}
org.apache.ibatis.builder.SqlSourceBuilder.ParameterMappingTokenHandler#buildParameterMapping
/*
* 将#{xxx} 占位符中的内容解析成 Map。
* #{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
* 上面占位符中的内容最终会被解析成如下的结果:
* {
* "property": "age",
* "typeHandler": "MyTypeHandler",
* "jdbcType": "NUMERIC",
* "javaType": "int"
* }
*/
private ParameterMapping buildParameterMapping(String content) {
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");
Class<?> propertyType;
// metaParameters 为 DynamicContext 成员变量 bindings 的元信息对象
if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
propertyType = metaParameters.getGetterType(property);
/*
* parameterType 是运行时参数的类型。如果用户传入的是单个参数,比如 Employe 对象,此时
* parameterType 为 Employe.class。如果用户传入的多个参数,比如 [id = 1, author = "chenhao"],
* MyBatis 会使用 ParamMap 封装这些参数,此时 parameterType 为 ParamMap.class。
*/
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
propertyType = Object.class;
} else {
/*
* 代码逻辑走到此分支中,表明 parameterType 是一个自定义的类,
* 比如 Employe,此时为该类创建一个元信息对象
*/
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
// 检测参数对象有没有与 property 想对应的 getter 方法
if (metaClass.hasGetter(property)) {
// 获取成员变量的类型
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
}
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
// 将 propertyType 赋值给 javaType
Class<?> javaType = propertyType;
String typeHandlerAlias = null;
// 遍历 propertiesMap
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
// 如果用户明确配置了 javaType,则以用户的配置为准
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
// 解析 jdbcType
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
// Do Nothing
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + parameterProperties);
}
}
if (typeHandlerAlias != null) {
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
// 构建 ParameterMapping 对象
return builder.build();
}
至此,SQL 中的 #{name, …} 占位符被替换成了问号 ? ,#{name, …} 也被解析成了一个 ParameterMapping 对象。
下面来看看 StaticSqlSource 的创建过程。如下:
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
public class StaticSqlSource implements SqlSource {
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Configuration configuration;
public StaticSqlSource(Configuration configuration, String sql) {
this(configuration, sql, null);
}
public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.configuration = configuration;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 创建 BoundSql 对象
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}
最后我们就可以通过调用下面方法获取到 SQL
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
下面我们回到 query 方法。createCacheKey 方法和缓存相关,这里就先不做分析,后面再说,接着我们分析 query 方法。
1.2 query
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.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 从 MappedStatement 中获取缓存
Cache cache = ms.getCache();
// 若映射文件中未配置缓存或参照缓存,此时 cache = null
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 若缓存未命中,则调用被装饰类的 query 方法,也就是SimpleExecutor的query方法
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 调用被装饰类的 query 方法,这里的delegate我们知道应该是SimpleExecutor
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
上面的代码涉及到了二级缓存,若二级缓存为空,或未命中,则调用被装饰类的 query 方法。被装饰类为SimpleExecutor,而SimpleExecutor继承BaseExecutor,那我们来看看 BaseExecutor 的query方法。
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)
public <E> List<E> 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.");
}
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--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
从一级缓存中查找查询结果。若缓存未命中,再向数据库进行查询。至此我们明白了一级二级缓存的大概思路,先从二级缓存中查找,若未命中二级缓存,再从一级缓存中查找,若未命中一级缓存,再从数据库查询数据,那我们来看看是怎么从数据库查询的
org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 向缓存中存储一个占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 调用 doQuery 进行查询
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;
}
调用了doQuery方法进行查询,最后将查询结果放入一级缓存。
org.apache.ibatis.executor.SimpleExecutor#doQuery
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 创建 StatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 创建 Statement
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行查询操作
return handler.<E>query(stmt, resultHandler);
} finally {
// 关闭 Statement
closeStatement(stmt);
}
}
该方法执行分为三步:
- 创建StatementHandler
- 创建 Statement
- 执行 query
1.2.1 创建 StatementHandler
org.apache.ibatis.session.Configuration#newStatementHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 创建具有路由功能的 StatementHandler
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// 应用插件到 StatementHandler 上
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
我们看看RoutingStatementHandler的构造方法
org.apache.ibatis.executor.statement.RoutingStatementHandler#RoutingStatementHandler
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 根据 StatementType 创建不同的 StatementHandler
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
RoutingStatementHandler 的构造方法会根据 MappedStatement 中的 statementType 变量创建不同的 StatementHandler 实现类。那statementType 是什么呢?我们还要回顾一下MappedStatement 的创建过程
public final class MappedStatement {
public static class Builder {
private MappedStatement mappedStatement = new MappedStatement();
public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
mappedStatement.configuration = configuration;
mappedStatement.id = id;
mappedStatement.sqlSource = sqlSource;
mappedStatement.statementType = StatementType.PREPARED;
mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<ParameterMapping>()).build();
mappedStatement.resultMaps = new ArrayList<ResultMap>();
mappedStatement.sqlCommandType = sqlCommandType;
mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
String logId = id;
if (configuration.getLogPrefix() != null) {
logId = configuration.getLogPrefix() + id;
}
mappedStatement.statementLog = LogFactory.getLog(logId);
mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
}
}
}
我们看到 statementType 的默认类型为PREPARED,这里将会创建PreparedStatementHandler,接着往下分析。
1.2.2 创建 Statement
stmt = prepareStatement(handler, ms.getStatementLog());
org.apache.ibatis.executor.SimpleExecutor#prepareStatement
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 获取数据库连接
Connection connection = getConnection(statementLog);
// 创建 Statement,
stmt = handler.prepare(connection, transaction.getTimeout());
// 为 Statement 设置参数
handler.parameterize(stmt);
return stmt;
}
在上面的代码中我们终于看到了和 jdbc 相关的内容了,上述方法分三个步骤:
- 获取数据库连接
- 创建 PreparedStatement
- 为 PreparedStatement 设置运行时参数
1)获取数据库连接
org.apache.ibatis.executor.BaseExecutor#getConnection
protected Connection getConnection(Log statementLog) throws SQLException {
//通过transaction来获取Connection
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
该方法通过 Executor 中的 transaction 属性来获取 Connection 。
由 MyBatis 配置文件中的 配置可知,MyBatis 会创建一个 JdbcTransactionFactory 类型实例来创建 JdbcTransaction 实例赋值给 transaction 属性。
那,先来看看 Transaction 接口。
public interface Transaction {
//获取数据库连接
Connection getConnection() throws SQLException;
//提交事务
void commit() throws SQLException;
//回滚事务
void rollback() throws SQLException;
//关闭事务
void close() throws SQLException;
//获取超时时间
Integer getTimeout() throws SQLException;
}
接着我们看看其实现类JdbcTransaction
public class JdbcTransaction implements Transaction {
private static final Log log = LogFactory.getLog(JdbcTransaction.class);
//数据库连接
protected Connection connection;
//数据源信息
protected DataSource dataSource;
//隔离级别
protected TransactionIsolationLevel level;
//是否为自动提交
protected boolean autoCommmit;
public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
dataSource = ds;
level = desiredLevel;
autoCommmit = desiredAutoCommit;
}
public JdbcTransaction(Connection connection) {
this.connection = connection;
}
@Override
public Connection getConnection() throws SQLException {
//如果事务中不存在connection,则获取一个connection并放入connection属性中
//第一次肯定为空
if (connection == null) {
openConnection();
}
//如果事务中已经存在connection,则直接返回这个connection
return connection;
}
/**
* commit()功能
* @throws SQLException
*/
@Override
public void commit() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Committing JDBC Connection [" + connection + "]");
}
//使用connection的commit()
connection.commit();
}
}
/**
* rollback()功能
* @throws SQLException
*/
@Override
public void rollback() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Rolling back JDBC Connection [" + connection + "]");
}
//使用connection的rollback()
connection.rollback();
}
}
/**
* close()功能
* @throws SQLException
*/
@Override
public void close() throws SQLException {
if (connection != null) {
resetAutoCommit();
if (log.isDebugEnabled()) {
log.debug("Closing JDBC Connection [" + connection + "]");
}
//使用connection的close()
connection.close();
}
}
protected void setDesiredAutoCommit(boolean desiredAutoCommit) {
try {
if (connection.getAutoCommit() != desiredAutoCommit) {
if (log.isDebugEnabled()) {
log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + connection + "]");
}
connection.setAutoCommit(desiredAutoCommit);
}
} catch (SQLException e) {
// Only a very poorly implemented driver would fail here,
// and there's not much we can do about that.
throw new TransactionException("Error configuring AutoCommit. "
+ "Your driver may not support getAutoCommit() or setAutoCommit(). "
+ "Requested setting: " + desiredAutoCommit + ". Cause: " + e, e);
}
}
protected void resetAutoCommit() {
try {
if (!connection.getAutoCommit()) {
// MyBatis does not call commit/rollback on a connection if just selects were performed.
// Some databases start transactions with select statements
// and they mandate a commit/rollback before closing the connection.
// A workaround is setting the autocommit to true before closing the connection.
// Sybase throws an exception here.
if (log.isDebugEnabled()) {
log.debug("Resetting autocommit to true on JDBC Connection [" + connection + "]");
}
//通过connection设置事务是否自动提交
connection.setAutoCommit(true);
}
} catch (SQLException e) {
if (log.isDebugEnabled()) {
log.debug("Error resetting autocommit to true "
+ "before closing the connection. Cause: " + e);
}
}
}
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
//通过dataSource来获取connection,并设置到transaction的connection属性中
connection = dataSource.getConnection();
if (level != null) {
//通过connection设置事务的隔离级别
connection.setTransactionIsolation(level.getLevel());
}
//设置事务是否自动提交
setDesiredAutoCommit(autoCommmit);
}
@Override
public Integer getTimeout() throws SQLException {
return null;
}
}
我们看到 JdbcTransaction 中有一个 Connection 属性和 dataSource 属性,使用 connection 来进行提交、回滚、关闭等操作,也就是说 JdbcTransaction 其实只是在 jdbc 的 connection 上面封装了一下,实际使用的其实还是jdbc的事务。
现在,我们来看看 getConnection() 方法。
org.apache.ibatis.transaction.jdbc.JdbcTransaction#getConnection
public Connection getConnection() throws SQLException {
//如果事务中不存在connection,则获取一个connection并放入connection属性中
//第一次肯定为空
if (connection == null) {
openConnection();
}
//如果事务中已经存在connection,则直接返回这个connection
return connection;
}
org.apache.ibatis.transaction.jdbc.JdbcTransaction#openConnection
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
//通过dataSource来获取connection,并设置到transaction的connection属性中
connection = dataSource.getConnection();
if (level != null) {
//通过connection设置事务的隔离级别
connection.setTransactionIsolation(level.getLevel());
}
//设置事务是否自动提交
setDesiredAutoCommit(autoCommmit);
}
先是判断当前事务中是否存在connection,如果存在,则直接返回connection,如果不存在则通过dataSource来获取connection。
这里我们明白了一点,如果当前事务没有关闭,也就是没有释放connection,那么在同一个Transaction中使用的是同一个connection。
我们再来想想,transaction是SimpleExecutor中的属性,SimpleExecutor又是SqlSession中的属性,那我们可以这样说,同一个SqlSession中只有一个SimpleExecutor,SimpleExecutor中有一个Transaction,Transaction有一个connection。
我们再来看看如下例子:
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//创建一个SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
EmployeeMapper employeeMapper = sqlSession.getMapper(Employee.class);
UserMapper userMapper = sqlSession.getMapper(User.class);
List<Employee> allEmployee = employeeMapper.getAll();
List<User> allUser = userMapper.getAll();
Employee employee = employeeMapper.getOne();
} finally {
sqlSession.close();
}
}
我们可以看到一个 sqlSession 可以获取多个 Mapper 代理对象,这些 Mapper 代理对象中的 Connection 的值肯定是同一个连接,直到调用 close() 。所以我们的 sqlSession 是线程不安全的,如果所有的业务都使用一个 sqlSession,那 Connection 也是同一个,如果一个业务执行完了就将其关闭,那其他还没执行完的业务则会出现问题。
我们回归到源码:
connection = dataSource.getConnection();
最终还是调用 dataSource 来获取连接,那我们是不是要来看看dataSource呢?
我们还是从前面的配置文件来看,这里有UNPOOLED和POOLED两种DataSource。
- UNPOOLED:非池化的 DataSource,将会创将new UnpooledDataSource()实例
- POOLED:池化的 DataSource,将会new pooledDataSource()实例
UnpooledDataSource 和 PooledDataSource 都实现 DataSource 接口,那我们先来看看DataSource接口。
public interface DataSource extends CommonDataSource, Wrapper {
//获取数据库连接
Connection getConnection() throws SQLException;
//获取数据库连接
Connection getConnection(String username, String password) throws SQLException;
}
很简单,只有一个获取数据库连接的接口,那我们来看看其实现类
UnpooledDataSource
UnpooledDataSource,从名称上即可知道,该种数据源不具有池化特性。该种数据源每次会返回一个新的数据库连接,而非复用旧的连接。其核心的方法有三个,分别如下:
- initializeDriver - 初始化数据库驱动
- doGetConnection - 获取数据连接
- configureConnection - 配置数据库连接
PooledDataSource
PooledDataSource 内部实现了连接池功能,用于复用数据库连接。因此,从效率上来说,PooledDataSource 要高于 UnpooledDataSource。但是最终获取Connection还是通过UnpooledDataSource,只不过PooledDataSource 提供一个存储Connection的功能。
在这里,我只是简单的提了一下 UnpooledDataSource、PooledDataSource 这两个类,并没有深入其中的源码,不过其源码的大致执行流程还是非常容易看懂的,如果大家感兴趣可以自己去看看。
2)创建PreparedStatement
下面我们回到 doQuery 方法。
org.apache.ibatis.executor.SimpleExecutor#doQuery
stmt = handler.prepare(connection, transaction.getTimeout());
org.apache.ibatis.executor.statement.BaseStatementHandler#prepare
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
// 创建 Statement
statement = instantiateStatement(connection);
// 设置超时和 FetchSize
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
org.apache.ibatis.executor.statement.PreparedStatementHandler#instantiateStatement
protected Statement instantiateStatement(Connection connection) throws SQLException {
//获取sql字符串,比如"select * from user where id= ?"
String sql = boundSql.getSql();
// 根据条件调用不同的 prepareStatement 方法创建 PreparedStatement
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
//通过connection获取Statement,将sql语句传进去
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() != null) {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
} else {
return connection.prepareStatement(sql);
}
}
看到没和jdbc的形式一模一样,通过 connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS) 方法的调用,我们的 Statement 对象就生成完毕了。
3)为 PreparedStatement 设置运行时参数
下面我们回到 doQuery 方法。
org.apache.ibatis.executor.SimpleExecutor#doQuery
handler.parameterize(stmt);
org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize
public void parameterize(Statement statement) throws SQLException {
// 通过参数处理器 ParameterHandler 设置运行时参数到 PreparedStatement 中
parameterHandler.setParameters((PreparedStatement) statement);
}
org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
/*
* 从 BoundSql 中获取 ParameterMapping 列表,每个 ParameterMapping 与原始 SQL 中的 #{xxx} 占位符一一对应
*/
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
// 获取属性名
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
// 为用户传入的参数 parameterObject 创建元信息对象
MetaObject metaObject = configuration.newMetaObject(parameterObject);
// 从用户传入的参数中获取 propertyName 对应的值
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
// 由类型处理器 typeHandler 向 ParameterHandler 设置参数
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
该方法的本意我相信大家都清楚,就是给 SQL 中的 ? 赋值,那我们来看看 setParameters 方法是怎么做的。
首先从 boundSql 中获取 parameterMappings 集合,然后遍历获取 parameterMapping 中的 propertyName ,如#{name} 中的name,然后从运行时参数parameterObject中获取 name 对应的参数值,最后设置到PreparedStatement 中。
我们主要来看是如何设置参数的,也就是 typeHandler.setParameter(ps, i + 1, value, jdbcType) 这句代码。
分析设置参数之前,先来看看 TypeHandler 接口。该接口是参数设置的抽象定义,具体实现逻辑由其子类实现。如:StringTypeHandler、IntegerTypeHandler、BooleanTypeHandler等。
下面我们以 StringTypeHandler 为例子,看看其如何执行的。
org.apache.ibatis.type.StringTypeHandler#setNonNullParameter
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, parameter);
}
setString 方法就不进去看了,已经涉及到很底层的源码了,它的本质就是字符串替换,但替换前做了很多安全性的检查。
1.2.3 执行 query
回到 doQuery 方法,来看看最后一步查询。
org.apache.ibatis.executor.SimpleExecutor#doQuery
return handler.<E>query(stmt, resultHandler);
org.apache.ibatis.executor.statement.PreparedStatementHandler#query
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
//直接执行ServerPreparedStatement的execute方法
ps.execute();
//进行resultSet自动映射
return resultSetHandler.<E> handleResultSets(ps);
}
该方法主要有两个步骤:
- 执行 SQL
- 解析返回结果
对于执行 SQL 非常简单,直接调用 PreparedStatement 对象的 execute 方法即可完成 SQL 的执行。
下面我们看看执行后的结果集是如何处理的。
好了,今天的内容到这里就结束了,我是 【J3】关注我,我们下期见
。
-
由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。
-
如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。
-
感谢您的阅读,十分欢迎并感谢您的关注。