mybatis之解析statement标签过程分析
顶顶顶顶
写在前面
在这篇文章中分析了解析mapper xml的<mappers>
中所有子标签的过程,其中的statement相关的标签解析过程比较复杂,因此单独在这篇文章中分析。
想要系统学习的,可以参考这篇文章,重要!!!。
1:入口
源码如下:
org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
private void configurationElement(XNode context) {
try {
...snip...
// <202108021631>
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
...snip...
}
}
<202108021631>
处源码如下:
org.apache.ibatis.builder.xml.XMLMapperBuilder#buildStatementFromContext(java.util.List<org.apache.ibatis.parsing.XNode>)
private void buildStatementFromContext(List<XNode> list) {
// 如果有全局databseId
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
// <202108021633>
buildStatementFromContext(list, null);
}
<202108021633>
处源码如下:
org.apache.ibatis.builder.xml.XMLMapperBuilder#buildStatementFromContext(java.util.List<org.apache.ibatis.parsing.XNode>, java.lang.String)
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
// 循环处理每个statement
for (XNode context : list) {
// 创建XMLStatementBuilder类
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// <202108021754>
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
// 发生异常,添加到不完备的statement集合中
// protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
configuration.addIncompleteStatement(statementParser);
}
}
}
<202108021754>
处是使用XMLStatementBuilder类解析statement,详细参考2:解析statement
。
2:解析statement
源码如下:
org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
public void parseStatementNode() {
// 获取id属性,和namespace组成该statement的唯一标识
String id = context.getStringAttribute("id");
// 获取databaseId,一般我们不设置,用于指定数据库厂商,可以默认为mysql
String databaseId = context.getStringAttribute("databaseId");
// 判断数据库厂商是否符合要求
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) return;
// 下面是解析各种各样的属性,具体可以先不用看,等用到了再细看
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
// 根据用户配置的lang属性获取对应的LanguageDriver
// <202108051511>
LanguageDriver langDriver = getLanguageDriver(lang);
// 获取resultType的class类型
Class<?> resultTypeClass = resolveClass(resultType);
// 获取resultSet的值,配置的是JDBC的ResultSet的类型,一般不用,暂时忽略
String resultSetType = context.getStringAttribute("resultSetType");
// <202108051554>
// 获取配置的statementType属性值,并通过枚举类valueOf方法,通过字面量(区分大小写)获取对应
// 枚举对象
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
// 获取节点名称,可能insert|select|delete|update
String nodeName = context.getNode().getNodeName();
// 转换为SqlCommandType枚举类,定义如下:
/*
public enum SqlCommandType {
UNKNOWN, INSERT, UPDATE, DELETE, SELECT;
}
*/
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 是否为select标记
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 配置的是否刷新缓存,默认值是只要不是select则为true(!isSelect)
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
// 是否使用缓存的标记,默认值是只要是select则使用缓存(isSelect)
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 创建用于替换<include/>标签的XMLIncludeTransformer对象
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
// <202108051720>
// 应用节点内部的<include/>标签
includeParser.applyIncludes(context.getNode());
// 解析<selectKey/>标签,该标签用于设置在非数据库自动生成id情况下生成主键值
// <202108091400>
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 创建SqlSource,该接口用于封装statement内配置的信息
// <202108161409>
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 解析statement中的resultSet属性,其实只在<select/>标签中有该属性
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
// 组装KeyGenerator的key,在前面解析<selectKey/>过程已经使用该key存储到configuration中
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
// 添加命名空间前缀,如果需要的话
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
// if为true的话,说明配置了<selectKey>标签
if (configuration.hasKeyGenerator(keyStatementId)) {
// 直接从configuration中获取已经解析完毕的KeyGenerator
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
// 如果是useGeneratedKeys属性配置为true,没有配置的话取"configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)"
// 的布尔结果值,最终为true的则使用内置的Jdbc3KeyGenerator,否则使用NoKeyGenerator
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}
// 将当前的statement构建为MappedStatement并添加到configuration中
// <202108091718>
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
<202108051511>
处是根据lang属性获取对应的LanguageDriver对象,具体参考2.1:解析lang属性
。<202108051554>
处是获取statementType属性,该属性用来设置底层使用的statement的类型,如果值STATEMENT,则对应的就是com.mysql.jdbc.Statement
,如果是PREPARED,则对应的就是com.mysql.jdbc.PreparedStatement
,如果是CALLABLE,则对应的就是com.mysql.jdbc.CallableStatement(存储过程调用)
,对应的枚举类如下:
org.apache.ibatis.mapping.StatementType
public enum StatementType {
STATEMENT, PREPARED, CALLABLE
}
默认值是PREPARED
,使用预编译的API PreparedStatement,该API可以有效防止sql注入问题,此时参数的格式使用#{}
,最终会使用?
方式执行sql语句,如果是Statement则使用${}
,最终会直接获取参数进行拼接,会存在sql注入问题。假设我们设置了statementType="STATEMENT"但是却使用了
#{}`则会报错,如下配置:
<select id="fetchById" resultMap="myResultMap" lang="MyLanguageDriver" statementType="STATEMENT">
SELECT * FROM test_language_driver t WHERE t.`id`=#{id}
</select>
执行的话,会报如下错:
错误的原因是因为在mysql中直接执行了带有?的sql语句,肯定是会报语法错误的。<202108051720>
处源码是解析节点内的<include/>
标签转换为对应的<sql/>
信息,具体参考2.2:解析include标签
。<202108091400>
处是解析selectKey标签,具体参考2.3:解析selectKey标签
。<202108091718>
处是构建org.apache.ibatis.mapping.MappedStatement
,具体参考2.4:构建MappedStatement
。<202108161409>
处是解析sql语句信息,包含动态sql语句,具体参考这篇文章。
2.1:解析lang属性
源码如下:
org.apache.ibatis.builder.xml.XMLStatementBuilder#getLanguageDriver
private LanguageDriver getLanguageDriver(String lang) {
Class<?> langClass = null;
// 解析lang属性对应的Class类型
if (lang != null) {
langClass = resolveClass(lang);
}
// <202108051528>
return builderAssistant.getLanguageDriver(langClass);
}
<202108051528>
处源码如下:
org.apache.ibatis.builder.MapperBuilderAssistant#getLanguageDriver
public LanguageDriver getLanguageDriver(Class<?> langClass) {
// 获取langClass的值,不为空则注册到LanguageDriverRegistry中,否则从其中获取默认的
if (langClass != null) {
configuration.getLanguageRegistry().register(langClass);
} else {
// 获取默认的,默认的为rg.apache.ibatis.scripting.xmltags.XMLLanguageDriver
langClass = configuration.getLanguageRegistry().getDefaultDriverClass();
}
// 根据langClass从LanguageDriverRegitry中获取对应的实例对象
// 存储在map中:org.apache.ibatis.scripting.LanguageDriverRegistry#LANGUAGE_DRIVER_MAP
// private final Map<Class<?>, LanguageDriver> LANGUAGE_DRIVER_MAP = new HashMap<Class<?>, LanguageDriver>();
return configuration.getLanguageRegistry().getDriver(langClass);
}
2.2:解析include标签
源码如下:
org.apache.ibatis.builder.xml.XMLIncludeTransformer#applyIncludes
public void applyIncludes(Node source) {
// 如果当前节点是include节点
if(source.getNodeName().equals("include")) {
// <202108051746>
// 获取引入的sql节点信息
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"));
// 因为引入的sql节点内部可能也使用了inlucde,所以这里递归调用
applyIncludes(toInclude);
// 这个if先忽略,暂时不知道用处
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
// 替换include节点为sql节点,toInlcude是sql节点,source是inlucde节点
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
// 移除include自身,因为已经转化为对应的sql了
toInclude.getParentNode().removeChild(toInclude);
} // 如果当前节点类型是Node.ELEMENT_NODE
else if (source.getNodeType() == Node.ELEMENT_NODE) {
// 获取子节点,并递归调用
NodeList children = source.getChildNodes();
for (int i=0; i<children.getLength(); i++) {
applyIncludes(children.item(i));
}
}
}
<202108051746>
处是获取引用的sql节点信息,源码如下:
org.apache.ibatis.builder.xml.XMLIncludeTransformer#findSqlFragment
private Node findSqlFragment(String refid) {
// 可能是变量,所以这里进行替换,比如在全局config配置文件中通过`<properties>-><prooerty>`
// 定义,然后通过${xxx}进行引用
refid = PropertyParser.parse(refid, configuration.getVariables());
// 添加命名空间前缀,namespace+refid,以便在多个命名空间中保证唯一性
refid = builderAssistant.applyCurrentNamespace(refid, true);
try {
// 获取引用的sql节点信息
XNode nodeToInclude = configuration.getSqlFragments().get(refid);
// 克隆结果,防止影响老的
Node result = nodeToInclude.getNode().cloneNode(true);
return result;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e);
}
}
2.3:解析selectKey标签
源码如下:
org.apache.ibatis.builder.xml.XMLStatementBuilder#processSelectKeyNodes
/*
id:statement 的id值,即id属性的值
parameterTypeClass:
langDriver:用于处理statement的sql语句和参数相关信息的语言驱动类
*/
private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
// 提示:该处为了方便调试,可以增加“selectKeyNodes.size() > 0”条件断点
// 获取配置的所有<selectKey/>标签节点集合
List<XNode> selectKeyNodes = context.evalNodes("selectKey");
if (configuration.getDatabaseId() != null) {
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
}
// 解析<selectKey/>标签
// <202108091410>
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
// 移除<selectKey/>节点,因为已经解析完毕没有用了,所以移除
removeSelectKeyNodes(selectKeyNodes);
}
<202108091410>
处是解析<selectKey/>
节点集合,源码如下:
org.apache.ibatis.builder.xml.XMLStatementBuilder#parseSelectKeyNodes
private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
// 遍历所有的<selectKey/>节点
for (XNode nodeToHandle : list) {
// parentId+SELECT_KEY_SUFFIX,如insertByQueryParamter!selectKey,作为id值
// 最终selectKey会映射为MappedStatment对象,注意这里可能配置有多个的情况,不同的
// databaseId每个配置一个,但是只有符合要求的那个datbaseId才会被解析
String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
// 获取selectKey配置的databaseId属性
String databaseId = nodeToHandle.getStringAttribute("databaseId");
// 判断配置的databaseId和当前要求的是否匹配
if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
// 解析单个的<selectKey/>节点
// <202108091458>
parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
}
}
}
<202108091458>
处源码如下:
org.apache.ibatis.builder.xml.XMLStatementBuilder#parseSelectKeyNode
private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
// 获取resultType属性值
String resultType = nodeToHandle.getStringAttribute("resultType");
// 解析resultType属性值为对应的class类型
Class<?> resultTypeClass = resolveClass(resultType);
// 获取statementType属性值,默认为PREPARED,即用JDBC的PreparedStatement接口API
StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 获取keyProperty属性值
String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
// 获取keyColumn属性值
String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
// 获取order属性值
boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
// 各种属性的默认值,用于创建MappedStatement对象
boolean useCache = false;
boolean resultOrdered = false;
KeyGenerator keyGenerator = new NoKeyGenerator();
Integer fetchSize = null;
Integer timeout = null;
boolean flushCache = false;
String parameterMap = null;
String resultMap = null;
ResultSetType resultSetTypeEnum = null;
// 创建SqlSource对象,这是一个用于封装statement内部定义信息的接口,可以理解为封装sql语句信息
SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
// statement的类型,默认是select
SqlCommandType sqlCommandType = SqlCommandType.SELECT;
// 创建MappedStatement,并添加到configuration中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
// 添加命名空间前缀,如果需要的话,最终${namespace}.${id}
id = builderAssistant.applyCurrentNamespace(id, false);
// 通过当前id获取创建好的MappedStatement,即<selectKey/>标签信息
MappedStatement keyStatement = configuration.getMappedStatement(id, false);
// 创建KeyGenerator 并添加到configuration中
/*
public void addKeyGenerator(String id, KeyGenerator keyGenerator) {
// protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");
keyGenerators.put(id, keyGenerator);
}
*/
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}
2.4:构建MappedStatement
源码如下:
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
// 如果此时cache-ref还没有解析,则异常
if (unresolvedCacheRef) throw new IncompleteElementException("Cache-ref not yet resolved");
// 添加命名空间前缀,如果需要的话,格式${namespace}+${id}
id = applyCurrentNamespace(id, false);
// SqlCommandType为statement类型的枚举类,源码如下:
/*
public enum SqlCommandType {
UNKNOWN, INSERT, UPDATE, DELETE, SELECT;
}
*/
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 创建MappedStatement的构造器对象
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
// 设置构造器的各种属性
statementBuilder.resource(resource);
statementBuilder.fetchSize(fetchSize);
statementBuilder.statementType(statementType);
statementBuilder.keyGenerator(keyGenerator);
statementBuilder.keyProperty(keyProperty);
statementBuilder.keyColumn(keyColumn);
statementBuilder.databaseId(databaseId);
statementBuilder.lang(lang);
statementBuilder.resultOrdered(resultOrdered);
statementBuilder.resulSets(resultSets);
setStatementTimeout(timeout, statementBuilder);
// 获取ParameterMap并设置到MappedStatement的构造器中
setStatementParameterMap(parameterMap, parameterType, statementBuilder);
// 获取ResultMap并设置的MappedStatement的构造器中
setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
// 设置cache-ref到MappedStatment中
setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);
// 通过构造器构造MappedStatement
// <202108091754>
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
<202108091754>
处的MappedStatement对象是用来封装<insert/>,<delete/>,<update/>,<select/>
标签的,另外就是,<selectKey/>
标签也会解析成MappedStatement,因此一共是5个标签。

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐
所有评论(0)