*************************************优雅的分割线 **********************************
分享一波:程序员赚外快-必看的巅峰干货
如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程
请关注微信公众号: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 databaseIdXNode 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() {// 当前节点idString id = context.getStringAttribute("id");// 获取数据库idString databaseId = context.getStringAttribute("databaseId");// 启用的数据库和sql节点配置的不同if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}// 获取当前节点的名称String nodeName = context.getNode().getNodeName();// 获取到sql的类型。select|update|delete|insertSqlCommandType 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对应的ClassClass<?> 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键值对形式存放到variableContextProperties 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;// 生成sqlSourceSqlSource 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中拿到上面的MappedStatementMappedStatement keyStatement = configuration.getMappedStatement(id, false);configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}
至此,selectKey节点已经被解析完毕并删除掉了,其余代码就是负责解析其他属性并将该sql节点创建为MappedStatement对象。
KeyGenerator keyGenerator;// 拼接id。形如findById!selectKeyString 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,如果为假就创建NoKeyGeneratorkeyGenerator = 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荷包
一个能让你学习技术和赚钱方法的公众号,持续更新