*************************************优雅的分割线 **********************************
分享一波:程序员赚外快-必看的巅峰干货
如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程
请关注微信公众号:HB荷包
一个能让你学习技术和赚钱方法的公众号,持续更新
前言
接上一篇博客,解析核心配置文件的流程还剩两块。Mybatis初始化1.2 —— 解析别名、插件、对象工厂、反射工具箱、环境
本想着只是两个模块,随便写写就完事,没想到内容还不少,加上最近几天事情比较多,就没怎么更新,几天抽空编写剩下两块代码。
解析sql片段
sql节点配置在Mapper.xml文件中,用于配置一些常用的sql片段。
/**
* 解析sql节点。
* sql节点用于定义一些常用的sql片段
* @param list
*/
private void sqlElement(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
sqlElement(list, configuration.getDatabaseId());
}
sqlElement(list, null);
}
/**
* 解析sql节点
* @param list sql节点集合
* @param requiredDatabaseId 当前配置的databaseId
*/
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// 获取databaseId和id属性
String databaseId = context.getStringAttribute("databaseId");
// 这里的id指定的是命名空间
String id = context.getStringAttribute("id");
// 启用当前的命名空间
id = builderAssistant.applyCurrentNamespace(id, false);
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
// 如果该节点指定的databaseId是当前配置中的,就启用该节点的sql片段
sqlFragments.put(id, context);
}
}
}
这里面,SQLFragments用于存放sql片段。在存放sql片段之前,会先调用databaseIdMatchesCurrent方法去校验该片段的databaseId是否为当前启用的databaseId
/**
* 判断databaseId是否是当前启用的
* @param id 命名空间id
* @param databaseId 待匹配的databaseId
* @param requiredDatabaseId 当前启用的databaseId
* @return
*/
private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
if (requiredDatabaseId != null) {
return requiredDatabaseId.equals(databaseId);
}
if (databaseId != null) {
return false;
}
if (!this.sqlFragments.containsKey(id)) {
return true;
}
// skip this fragment if there is a previous one with a not null databaseId
XNode context = this.sqlFragments.get(id);
return context.getStringAttribute("databaseId") == null;
}
解析sql片段的步骤就这么简单,下面是解析sql节点的代码。
解析sql节点
在XxxMapper.xml中存在诸多的sql节点,大体分为select、insert、delete、update节点(此外还有selectKey节点等,后面会进行介绍)。每一个sql节点最终会被解析成MappedStatement。
/**
-
表示映射文件中的sql节点
-
select、update、insert、delete节点
-
该节点中包含了id、返回值、sql等属性
-
@author Clinton Begin
*/
public final class MappedStatement {/**
- 包含命名空间的节点id
/
private String resource;
private Configuration configuration;
/* - 节点id
/
private String id;
private Integer fetchSize;
private Integer timeout;
/* - STATEMENT 表示简单的sql,不包含动态的
- PREPARED 表示预编译sql,包含#{}
- CALLABLE 调用存储过程
*/
private StatementType statementType;
private ResultSetType resultSetType;
/**
- 节点或者注解中编写的sql
/
private SqlSource sqlSource;
private Cache cache;
private ParameterMap parameterMap;
private List resultMaps;
private boolean flushCacheRequired;
private boolean useCache;
private boolean resultOrdered;
/* - sql的类型。select、update、insert、delete
*/
private SqlCommandType sqlCommandType;
private KeyGenerator keyGenerator;
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;
}
- 包含命名空间的节点id
处理sql节点
/**
* 处理sql节点
* 这里的Statement单词后面会经常遇到
* 一个MappedStatement表示一条sql语句
* @param list
*/
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
/**
* 启用当前databaseId的sql语句节点
* @param list
* @param requiredDatabaseId
*/
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 解析sql节点
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
在parseStatementNode方法中,只会启用当前databaseId的sql节点(如果没配置就全部启用)
/**
* 解析sql节点
*/
public void parseStatementNode() {
// 当前节点id
String id = context.getStringAttribute("id");
// 获取数据库id
String databaseId = context.getStringAttribute("databaseId");
// 启用的数据库和sql节点配置的不同
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 获取当前节点的名称
String nodeName = context.getNode().getNodeName();
// 获取到sql的类型。select|update|delete|insert
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 下面是解析include和selectKey节点
......
}
在该方法中,会依次处理include节点、selectKey节点、最后获取到当前sql节点的各个属性,去创建MappedStatement对象,并添加到Configuration中。
/**
* 解析sql节点
*/
public void parseStatementNode() {
// 在上面已经进行了注释
......
// 解析sql前先处理include节点。
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 获取parameterType属性
String parameterType = context.getStringAttribute("parameterType");
// 直接拿到parameterType对应的Class
Class<?> parameterTypeClass = resolveClass(parameterType);
// 获取到lang属性
String lang = context.getStringAttribute("lang");
// 获取对应的动态sql语言驱动器。
LanguageDriver langDriver = getLanguageDriver(lang);
// 解析selectKey节点
processSelectKeyNodes(id, parameterTypeClass, langDriver);
}
解析parameterType和lang属性比较简单,这里只看解析include和selectKey
解析include节点
/**
* 启用include节点
*
* @param source
*/
public void applyIncludes(Node source) {
Properties variablesContext = new Properties();
Properties configurationVariables = configuration.getVariables();
Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);
applyIncludes(source, variablesContext, false);
}
在applyIncludes方法中,会调用它的重载方法,递归去处理所有的include节点。include节点中,可能会存在${}占位符,在这步,也会将该占位符给替换成实际意义的字符串。接着,include节点会被处理成sql节点,并将sql节点中的sql语句取出放到节点之前,最后删除sql节点。最终select等节点会被解析成带有动态sql的节点。
/**
* 递归去处理所有的include节点.
*
* @param source include节点
* @param variablesContext 当前所有的配置
*/
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
if (source.getNodeName().equals("include")) {
// 获取到refid并从配置中拿到sql片段
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
// 解析include节点下的Properties节点,并替换value对应的占位符,将name和value键值对形式存放到variableContext
Properties toIncludeContext = getVariablesContext(source, variablesContext);
// 递归处理,在sql节点中可能会使用到include节点
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
// 将include节点替换成sql节点
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
// 如果还有子节点,就添加到sql节点前面
// 在上面的代码中,sql节点已经不可能再有子节点了
// 这里的子节点就是文本节点(具体的sql语句)
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
// 删除sql节点
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
if (included && !variablesContext.isEmpty()) {
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
// 获取所有的子节点
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
// 解析include节点
applyIncludes(children.item(i), variablesContext, included);
}
} else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
&& !variablesContext.isEmpty()) {
// 使用之前解析到的Properties对象替换对应的占位符
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
第一行代码的含义是根据include节点的refid属性去获取到对应的sql片段,代码比较简单
/**
* 根据refid查找sql片段
* @param refid
* @param variables
* @return
*/
private Node findSqlFragment(String refid, Properties variables) {
// 替换占位符
refid = PropertyParser.parse(refid, variables);
// 将refid前面拼接命名空间
refid = builderAssistant.applyCurrentNamespace(refid, true);
try {
// 从Configuration中查找对应的sql片段
XNode nodeToInclude = configuration.getSqlFragments().get(refid);
return nodeToInclude.getNode().cloneNode(true);
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e);
}
}
到这里,include节点就会被替换成有实际意义的sql语句。
解析selectKey节点
当数据表中主键设计为自增,可能会存在业务需要在插入后获取到主键,这时候就需要使用selectKey节点。processSelectKeyNodes方法用于解析selectKey节点。该方法会先获取到该sql节点所有的selectKey节点,遍历去解析,解析完毕后删除selectKey节点。
/**
* 解析selectKey节点
* selectKey节点可以解决insert时主键自增问题
* 如果需要在插入数据后获取到主键,就需要使用selectKey节点
*
* @param id sql节点的id
* @param parameterTypeClass 参数类型
* @param langDriver 动态sql语言驱动器
*/
private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
// 获取全部的selectKey节点
List<XNode> selectKeyNodes = context.evalNodes("selectKey");
if (configuration.getDatabaseId() != null) {
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
}
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
removeSelectKeyNodes(selectKeyNodes);
}
删除selectKey节点的代码比较简单,这里就不贴了,重点看parseSelectKeyNodes方法。
该方法负责遍历获取到的所有selectKey节点,只启用当前databaseId对应的节点(这里的逻辑和sql片段那里一样,如果开发者没有配置databaseId,就全部启用)
/**
* 解析selectKey节点
*
* @param parentId 父节点id(指sql节点的id)
* @param list 所有的selectKey节点
* @param parameterTypeClass 参数类型
* @param langDriver 动态sql驱动
* @param skRequiredDatabaseId 数据源id
*/
private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
// 遍历selectKey节点
for (XNode nodeToHandle : list) {
// 拼接id 修改为形如 findById!selectKey形式
String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
// 获得当前节点的databaseId属性
String databaseId = nodeToHandle.getStringAttribute("databaseId");
// 只解析databaseId是当前启用databaseId的节点
if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
}
}
}
在for循环中,会逐个调用parseSelectKeyNode方法去解析selectKey节点。代码看似复杂其实很简单,最终selectKey节点也会被解析成MappedStatement对象
/**
* 解析selectKey节点
*
* @param id 节点id
* @param nodeToHandle selectKey节点
* @param parameterTypeClass 参数类型
* @param langDriver 动态sql驱动
* @param databaseId 数据库id
*/
private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
// 获取 resultType 属性
String resultType = nodeToHandle.getStringAttribute("resultType");
// 解析返回值类型
Class<?> resultTypeClass = resolveClass(resultType);
// 解析statementType(sql类型,简单sql、动态sql、存储过程)
StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 获取keyProperty和keyColumn属性
String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
// 是在之前还是之后去获取主键
boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
// 设置MappedStatement对象需要的一系列属性默认值
boolean useCache = false;
boolean resultOrdered = false;
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
Integer fetchSize = null;
Integer timeout = null;
boolean flushCache = false;
String parameterMap = null;
String resultMap = null;
ResultSetType resultSetTypeEnum = null;
// 生成sqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
// selectKey节点只能配置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);
// 启用当前命名空间(给id前面加上命名空间)
id = builderAssistant.applyCurrentNamespace(id, false);
// 从Configuration中拿到上面的MappedStatement
MappedStatement keyStatement = configuration.getMappedStatement(id, false);
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}
至此,selectKey节点已经被解析完毕并删除掉了,其余代码就是负责解析其他属性并将该sql节点创建为MappedStatement对象。
KeyGenerator keyGenerator;
// 拼接id。形如findById!selectKey
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
// 给这个id前面追加当前的命名空间
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
// 优先取配置的useGeneratorKeys。如果为空就判断当前配置是否允许jdbc自动生成主键,并且当前是insert语句
// 判断如果为真就创建Jdbc3KeyGenerator,如果为假就创建NoKeyGenerator
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 获取当前sql节点的一堆属性,去创建MappedStatement。
// 这里创建的MappedStatement就代表一个sql节点
// 也是后面编写mybatis拦截器时可以拦截的一处
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
结语
在看本博客时,可能会觉得比较吃力,这里建议结合代码去阅读。事实上这三篇博客的阅读和编写的过程中,对应的mybatis代码都比较容易,结合代码阅读起来并没有多大难度。最后贴一下我的码云地址(别问为什么是github,卡的一批)
mybatis源码中文注释
*************************************优雅的分割线 **********************************
分享一波:程序员赚外快-必看的巅峰干货
如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程
请关注微信公众号:HB荷包
一个能让你学习技术和赚钱方法的公众号,持续更新