该系列文档是本人在学习 Mybatis 的源码过程当中总结下来的,可能对读者不太友好,请结合个人源码注释(Mybatis源码分析 GitHub 地址、Mybatis-Spring 源码分析 GitHub 地址、Spring-Boot-Starter 源码分析 GitHub 地址)进行阅读html
MyBatis 版本:3.5.2java
MyBatis-Spring 版本:2.0.3node
MyBatis-Spring-Boot-Starter 版本:2.1.4git
在MyBatis初始化过程当中,大体会有如下几个步骤:github
建立Configuration
全局配置对象,会往TypeAliasRegistry
别名注册中心添加Mybatis须要用到的相关类,并设置默认的语言驱动类为XMLLanguageDriver
spring
加载mybatis-config.xml
配置文件、Mapper接口中的注解信息和XML映射文件,解析后的配置信息会造成相应的对象并保存到Configuration全局配置对象中sql
构建DefaultSqlSessionFactory
对象,经过它能够建立DefaultSqlSession
对象,MyBatis中SqlSession
的默认实现类数据库
由于整个初始化过程涉及到的代码比较多,因此拆分红了四个模块依次对MyBatis的初始化进行分析:express
因为在MyBatis的初始化过程当中去解析Mapper接口与XML映射文件涉及到的篇幅比较多,XML映射文件的解析过程也比较复杂,因此才分红了后面三个模块,逐步分析,这样便于理解apache
在前面的MyBatis初始化相关文档中已经大体讲完了MyBatis初始化的整个流程,其中遗漏了一部分,就是在解析<select /> <insert /> <update /> <delete />
节点的过程当中,是如何解析SQL语句,如何实现动态SQL语句,最终会生成一个org.apache.ibatis.mapping.SqlSource
对象的,对于这烦琐且易出错的过程,咱们来看看MyBatis如何实现的?
咱们回顾org.apache.ibatis.builder.xml.XMLStatementBuilder
的parseStatementNode()
解析 Statement 节点时,经过下面的方法建立对应的SqlSource
对象
// 建立对应的 SqlSource 对象,保存了该节点下 SQL 相关信息 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
langDriver
是从Configuration全局配置对象中获取的默认实现类,对应的也就是XMLLanguageDriver
,在Configuration初始化的时候设置的
public Configuration() { languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class); languageRegistry.register(RawLanguageDriver.class); }
主要包路径:org.apache.ibatis.scripting、org.apache.ibatis.builder、org.apache.ibatis.mapping
主要涉及到的类:
org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
:语言驱动接口的默认实现,建立ParameterHandler参数处理器对象和SqlSource资源对象
org.apache.ibatis.scripting.xmltags.XMLScriptBuilder
:继承 BaseBuilder 抽象类,负责将SQL脚本(XML或者注解中定义的SQL语句)解析成SqlSource(DynamicSqlSource或者RawSqlSource)资源对象
org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.NodeHandler
:定义在XMLScriptBuilder
内部的一个接口,用于处理MyBatis自定义标签(<if /> <foreach />
等),生成对应的SqlNode对象,不一样的实现类处理不一样的标签
org.apache.ibatis.scripting.xmltags.DynamicContext
:解析动态SQL语句时的上下文,用于解析SQL时,记录动态SQL处理后的SQL语句,内部提供ContextMap对象保存上下文的参数
org.apache.ibatis.scripting.xmltags.SqlNode
:SQL Node接口,每一个XML Node会解析成对应的SQL Node对象,经过上下文能够对动态SQL进行逻辑处理,生成须要的结果
org.apache.ibatis.scripting.xmltags.OgnlCache
:用于处理Ognl表达式
语言驱动接口的实现类以下图所示:
org.apache.ibatis.scripting.LanguageDriver
:语言驱动接口,代码以下:
public interface LanguageDriver { /** * Creates a {@link ParameterHandler} that passes the actual parameters to the the JDBC statement. * 建立 ParameterHandler 对象 * * @param mappedStatement The mapped statement that is being executed * @param parameterObject The input parameter object (can be null) * @param boundSql The resulting SQL once the dynamic language has been executed. * @return 参数处理器 * @author Frank D. Martinez [mnesarco] * @see DefaultParameterHandler */ ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql); /** * Creates an {@link SqlSource} that will hold the statement read from a mapper xml file. * It is called during startup, when the mapped statement is read from a class or an xml file. * 建立 SqlSource 对象,从 Mapper XML 配置的 Statement 标签中,即 <select /> 等。 * * @param configuration The MyBatis configuration * @param script XNode parsed from a XML file * @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null. * @return SQL 资源 */ SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType); /** * Creates an {@link SqlSource} that will hold the statement read from an annotation. * It is called during startup, when the mapped statement is read from a class or an xml file. * 建立 SqlSource 对象,从方法注解配置,即 @Select 等。 * * @param configuration The MyBatis configuration * @param script The content of the annotation * @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null. * @return SQL 资源 */ SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType); }
定义了三个方法:
createParameterHandler
:获取 ParameterHandler 参数处理器对象
createSqlSource
:建立 SqlSource 对象,解析 Mapper XML 配置的 Statement 标签中,即 <select /> <update /> <delete /> <insert />
createSqlSource
:建立 SqlSource 对象,从方法注解配置,即 @Select 等
org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
:语言驱动接口的默认实现,代码以下:
public class XMLLanguageDriver implements LanguageDriver { @Override public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { // 建立 DefaultParameterHandler 对象 return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql); } /** * 用于解析 XML 映射文件中的 SQL * * @param configuration The MyBatis configuration * @param script XNode parsed from a XML file * @param parameterType input parameter type got from a mapper method or * specified in the parameterType xml attribute. Can be * null. * @return SQL 资源 */ @Override public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { // 建立 XMLScriptBuilder 对象,执行解析 XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); return builder.parseScriptNode(); } /** * 用于解析注解中的 SQL * * @param configuration The MyBatis configuration * @param script The content of the annotation * @param parameterType input parameter type got from a mapper method or * specified in the parameterType xml attribute. Can be * null. * @return SQL 资源 */ @Override public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) { // issue #3 // <1> 若是是 <script> 开头,表示是在注解中使用的动态 SQL if (script.startsWith("<script>")) { // <1.1> 建立 XPathParser 对象,解析出 <script /> 节点 XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver()); return createSqlSource(configuration, parser.evalNode("/script"), parameterType); } else { // issue #127 // <2.1> 变量替换 script = PropertyParser.parse(script, configuration.getVariables()); // <2.2> 建立 TextSqlNode 对象 TextSqlNode textSqlNode = new TextSqlNode(script); if (textSqlNode.isDynamic()) { // <2.3.1> 若是是动态 SQL ,则建立 DynamicSqlSource 对象 return new DynamicSqlSource(configuration, textSqlNode); } else { // <2.3.2> 若是非动态 SQL ,则建立 RawSqlSource 对象 return new RawSqlSource(configuration, script, parameterType); } } } }
实现了LanguageDriver接口:
建立 DefaultParameterHandler
默认参数处理器并返回
解析 XML 映射文件中的 SQL,经过建立 XMLScriptBuilder
对象,调用其 parseScriptNode()
方法解析
解析注解定义的 SQL
<script>
开头,表示是在注解中使用的动态 SQL,将其转换成 XNode 而后调用上述方法,不了解的能够看看MyBatis三种动态SQL配置方式org.apache.ibatis.scripting.defaults.RawLanguageDriver
:继承了XMLLanguageDriver,在的基础上增长了是否为静态SQL语句的校验,也就是判断建立的 SqlSource 是否为 RawSqlSource 静态 SQL 资源
org.apache.ibatis.scripting.xmltags.XMLScriptBuilder
:继承 BaseBuilder 抽象类,负责将 SQL 脚本(XML或者注解中定义的 SQL )解析成 SqlSource 对象
public class XMLScriptBuilder extends BaseBuilder { /** * 当前 SQL 的 XNode 对象 */ private final XNode context; /** * 是否为动态 SQL */ private boolean isDynamic; /** * SQL 的 Java 入参类型 */ private final Class<?> parameterType; /** * NodeNodeHandler 的映射 */ private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>(); public XMLScriptBuilder(Configuration configuration, XNode context) { this(configuration, context, null); } public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) { super(configuration); this.context = context; this.parameterType = parameterType; initNodeHandlerMap(); } private void initNodeHandlerMap() { nodeHandlerMap.put("trim", new TrimHandler()); nodeHandlerMap.put("where", new WhereHandler()); nodeHandlerMap.put("set", new SetHandler()); nodeHandlerMap.put("foreach", new ForEachHandler()); nodeHandlerMap.put("if", new IfHandler()); nodeHandlerMap.put("choose", new ChooseHandler()); nodeHandlerMap.put("when", new IfHandler()); nodeHandlerMap.put("otherwise", new OtherwiseHandler()); nodeHandlerMap.put("bind", new BindHandler()); } }
在构造函数中会初始化 NodeHandler 处理器,分别用于处理不一样的MyBatis自定义的XML标签,例如<if /> <where /> <foreach />
等标签
parseScriptNode()
方法将 SQL 脚本(XML或者注解中定义的 SQL )解析成 SqlSource
对象,代码以下:
public SqlSource parseScriptNode() { // 解析 XML 或者注解中定义的 SQL MixedSqlNode rootSqlNode = parseDynamicTags(context); SqlSource sqlSource; if (isDynamic) { // 动态语句,使用了 ${} 也算 sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; }
parseDynamicTags(XNode node)
方法,将解析 SQL 成 MixedSqlNode
对象,主要是将一整个 SQL 解析成一系列的 SqlNode 对象<if />
等)或者使用了${}
,则封装成DynamicSqlSource
对象RawSqlSource
对象parseDynamicTags()
将 SQL 脚本(XML或者注解中定义的 SQL )解析成MixedSqlNode
对象,代码以下:
protected MixedSqlNode parseDynamicTags(XNode node) { // <1> 建立 SqlNode 数组 List<SqlNode> contents = new ArrayList<>(); /* * <2> 遍历 SQL 节点中全部子节点 * 这里会对该节点内的全部内容进行处理而后返回 NodeList 对象 * 1. 文本内容会被解析成 '<#text></#text>' 节点,就算一个换行符也会解析成这个 * 2. <![CDATA[ content ]]> 会被解析成 '<#cdata-section>content</#cdata-section>' 节点 * 3. 其余动态<if /> <where /> */ NodeList children = node.getNode().getChildNodes(); for (int i = 0; i < children.getLength(); i++) { // 当前子节点 XNode child = node.newXNode(children.item(i)); // <2.1> 若是类型是 Node.CDATA_SECTION_NODE 或者 Node.TEXT_NODE 时 if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE // <![CDATA[ ]]>节点 || child.getNode().getNodeType() == Node.TEXT_NODE) { // 纯文本 // <2.1.1> 得到内容 String data = child.getStringBody(""); // <2.1.2> 建立 TextSqlNode 对象 TextSqlNode textSqlNode = new TextSqlNode(data); if (textSqlNode.isDynamic()) { // <2.1.2.1> 若是是动态的 TextSqlNode 对象,也就是使用了 '${}' // 添加到 contents 中 contents.add(textSqlNode); // 标记为动态 SQL isDynamic = true; } else { // <2.1.2.2> 若是是非动态的 TextSqlNode 对象,没有使用 '${}' // <2.1.2> 建立 StaticTextSqlNode 添加到 contents 中 contents.add(new StaticTextSqlNode(data)); } } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628 <2.2> 若是类型是 Node.ELEMENT_NODE // <2.2.1> 根据子节点的标签,得到对应的 NodeHandler 对象 String nodeName = child.getNode().getNodeName(); NodeHandler handler = nodeHandlerMap.get(nodeName); if (handler == null) { // 得到不到,说明是未知的标签,抛出 BuilderException 异常 throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement."); } // <2.2.2> 执行 NodeHandler 处理 handler.handleNode(child, contents); // <2.2.3> 标记为动态 SQL isDynamic = true; } } // <3> 建立 MixedSqlNode 对象 return new MixedSqlNode(contents); }
<1>
建立 SqlNode 数组 contents
,用于保存解析 SQL 后的一些列 SqlNode 对象
<2>
获取定义的 SQL 节点中全部子节点,返回一个 NodeList 对象,这个对象中包含了该 SQL 节点内的全部信息,而后逐个遍历子节点
1. 其中文本内容会被解析成`<#text></#text>`节点,就算一个换行符也会解析成这个 2. `<![CDATA[ ]]>` 会被解析成 `<#cdata-section></#cdata-section>` 节点 3. 还有其余MyBatis自定义的标签`<if /> <where />`等等
<2.1>
若是子节点是<#text />
或者<#cdata-section />
类型
<2.1.1>
获取子节点的文本内容
<2.1.2>
建立 TextSqlNode 对象
<2.1.2.1>
调用 TextSqlNode
的 isDynamic() 方法,点击去该进去看看就知道了,若是文本中使用了${}
,则标记为动态 SQL 语句,将其添加至 contents
数组中
<2.1.2.2>
不然就是静态文本内容,建立对应的 StaticTextSqlNode
对象,将其添加至 contents
数组中
<2.2>
若是类型是 Node.ELEMENT_NODE
时,也就是 MyBatis 的自定义标签
<2.2.1>
根据子节点的标签名称,得到对应的 NodeHandler
对象
<2.2.2>
执行NodeHandler
的handleNode
方法处理该节点,建立不通类型的 SqlNode 并添加到 contents
数组中,如何处理的在下面讲述
<2.2.3>
标记为动态 SQL 语句
<3>
最后将建立 contents
封装成 MixedSqlNode
对象
XMLScriptBuilder
的内部接口,用于处理MyBatis自定义标签,接口实现类以下图所示:
代码以下:
private interface NodeHandler { /** * 处理 Node * * @param nodeToHandle 要处理的 XNode 节点 * @param targetContents 目标的 SqlNode 数组。实际上,被处理的 XNode 节点会建立成对应的 SqlNode 对象,添加到 targetContents 中 */ void handleNode(XNode nodeToHandle, List<SqlNode> targetContents); }
这些 NodeHandler 实现类都定义在 XMLScriptBuilder 内部,用于处理不一样标签,咱们逐个来看
实现了NodeHandler接口,<bind />
标签的处理器,代码以下:
/** * <bind />元素容许你在 OGNL 表达式(SQL语句)之外建立一个变量,并将其绑定到当前的上下文 */ private class BindHandler implements NodeHandler { public BindHandler() { // Prevent Synthetic Access } @Override public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) { // 解析 name、value 属性 final String name = nodeToHandle.getStringAttribute("name"); final String expression = nodeToHandle.getStringAttribute("value"); // 建立 VarDeclSqlNode 对象 final VarDeclSqlNode node = new VarDeclSqlNode(name, expression); targetContents.add(node); } }
获取<bind />
标签的name和value属性
根据这些属性建立一个 VarDeclSqlNode
对象
添加到targetContents
集合中
例如这样配置:
<select id="selectBlogsLike" resultType="Blog"> <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" /> SELECT * FROM BLOG WHERE title LIKE #{pattern} </select>
实现了NodeHandler接口,<trim />
标签的处理器,代码以下:
private class TrimHandler implements NodeHandler { public TrimHandler() { // Prevent Synthetic Access } @Override public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) { // <1> 解析内部的 SQL 节点,成 MixedSqlNode 对象 MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle); // <2> 得到 prefix、prefixOverrides、"suffix"、suffixOverrides 属性 String prefix = nodeToHandle.getStringAttribute("prefix"); String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides"); String suffix = nodeToHandle.getStringAttribute("suffix"); String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides"); // <3> 建立 TrimSqlNode 对象 TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides); targetContents.add(trim); } }
继续调用parseDynamicTags
方法解析<if />
标签内部的子标签节点,嵌套解析,生成MixedSqlNode对象
得到 prefix
、prefixOverrides
、suffix
、suffixOverrides
属性
根据上面获取到的属性建立TrimSqlNode
对象
添加到targetContents
集合中
实现了NodeHandler接口,<where />
标签的处理器,代码以下:
private class WhereHandler implements NodeHandler { public WhereHandler() { // Prevent Synthetic Access } @Override public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) { // 解析内部的 SQL 节点,成 MixedSqlNode 对象 MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle); // 建立 WhereSqlNode 对象 WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode); targetContents.add(where); } }
parseDynamicTags
方法解析<where />
标签内部的子标签节点,嵌套解析,生成MixedSqlNode对象WhereSqlNode
对象,该对象继承了TrimSqlNode
,自定义前缀(WHERE)和须要删除的前缀(AND、OR等)targetContents
集合中实现了NodeHandler接口,<set />
标签的处理器,代码以下:
private class SetHandler implements NodeHandler { public SetHandler() { // Prevent Synthetic Access } @Override public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) { // 解析内部的 SQL 节点,成 MixedSqlNode 对象 MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle); SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode); targetContents.add(set); } }
parseDynamicTags
方法解析<set />
标签内部的子标签节点,嵌套解析,生成MixedSqlNode对象SetSqlNode
对象,该对象继承了TrimSqlNode
,自定义前缀(SET)和须要删除的前缀和后缀(,)targetContents
集合中实现了NodeHandler接口,<foreach />
标签的处理器,代码以下:
private class ForEachHandler implements NodeHandler { public ForEachHandler() { // Prevent Synthetic Access } @Override public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) { // 解析内部的 SQL 节点,成 MixedSqlNode 对象 MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle); // 得到 collection、item、index、open、close、separator 属性 String collection = nodeToHandle.getStringAttribute("collection"); String item = nodeToHandle.getStringAttribute("item"); String index = nodeToHandle.getStringAttribute("index"); String open = nodeToHandle.getStringAttribute("open"); String close = nodeToHandle.getStringAttribute("close"); String separator = nodeToHandle.getStringAttribute("separator"); // 建立 ForEachSqlNode 对象 ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator); targetContents.add(forEachSqlNode); } }
parseDynamicTags
方法解析<foreach />
标签内部的子标签节点,嵌套解析,生成MixedSqlNode对象ForEachSqlNode
对象targetContents
集合中实现了NodeHandler接口,<if />
标签的处理器,代码以下:
private class IfHandler implements NodeHandler { public IfHandler() { // Prevent Synthetic Access } @Override public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) { // 解析内部的 SQL 节点,成 MixedSqlNode 对象 MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle); // 得到 test 属性 String test = nodeToHandle.getStringAttribute("test"); // 建立 IfSqlNode 对象 IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test); targetContents.add(ifSqlNode); } }
parseDynamicTags
方法解析<if />
标签内部的子标签节点,嵌套解析,生成MixedSqlNode对象IfSqlNode
对象targetContents
集合中实现了NodeHandler接口,<otherwise />
标签的处理器,代码以下:
private class OtherwiseHandler implements NodeHandler { public OtherwiseHandler() { // Prevent Synthetic Access } @Override public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) { // 解析内部的 SQL 节点,成 MixedSqlNode 对象 MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle); targetContents.add(mixedSqlNode); } }
parseDynamicTags
方法解析<otherwise />
标签内部的子标签节点,嵌套解析,生成MixedSqlNode对象targetContents
集合中,须要结合ChooseHandler使用实现了NodeHandler接口,<choose />
标签的处理器,代码以下:
private class ChooseHandler implements NodeHandler { public ChooseHandler() { // Prevent Synthetic Access } @Override public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) { List<SqlNode> whenSqlNodes = new ArrayList<>(); List<SqlNode> otherwiseSqlNodes = new ArrayList<>(); // 解析 `<when />` 和 `<otherwise />` 的节点们 handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes); // 得到 `<otherwise />` 的节点,存在多个会抛出异常 SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes); // 建立 ChooseSqlNode 对象 ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode); targetContents.add(chooseSqlNode); } private void handleWhenOtherwiseNodes(XNode chooseSqlNode, List<SqlNode> ifSqlNodes, List<SqlNode> defaultSqlNodes) { List<XNode> children = chooseSqlNode.getChildren(); for (XNode child : children) { String nodeName = child.getNode().getNodeName(); NodeHandler handler = nodeHandlerMap.get(nodeName); if (handler instanceof IfHandler) { // 处理 `<when />` 标签的状况 handler.handleNode(child, ifSqlNodes); } else if (handler instanceof OtherwiseHandler) { // 处理 `<otherwise />` 标签的状况 handler.handleNode(child, defaultSqlNodes); } } } private SqlNode getDefaultSqlNode(List<SqlNode> defaultSqlNodes) { SqlNode defaultSqlNode = null; if (defaultSqlNodes.size() == 1) { defaultSqlNode = defaultSqlNodes.get(0); } else if (defaultSqlNodes.size() > 1) { throw new BuilderException("Too many default (otherwise) elements in choose statement."); } return defaultSqlNode; } }
先逐步处理<choose />
标签的<when />
和 <otherwise />
子标签们,经过组合 IfHandler 和 OtherwiseHandler 两个处理器,实现对子节点们的解析
若是存在<otherwise />
子标签,则抛出异常
根据这些属性建立ChooseSqlNode
对象
添加到targetContents
集合中
org.apache.ibatis.scripting.xmltags.DynamicContext
:解析动态SQL语句时的上下文,用于解析SQL时,记录动态SQL处理后的SQL语句,内部提供ContextMap对象保存上下文的参数
public class DynamicContext { /** * 入参保存在 ContextMap 中的 Key * * {@link #bindings} */ public static final String PARAMETER_OBJECT_KEY = "_parameter"; /** * 数据库编号保存在 ContextMap 中的 Key * * {@link #bindings} */ public static final String DATABASE_ID_KEY = "_databaseId"; static { // <1.2> 设置 OGNL 的属性访问器 OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor()); } /** * 上下文的参数集合,包含附加参数(经过`<bind />`标签生成的,或者`<foreach />`标签中的集合的元素等等) */ private final ContextMap bindings; /** * 生成后的 SQL */ private final StringJoiner sqlBuilder = new StringJoiner(" "); /** * 惟一编号。在 {@link org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.ForEachHandler} 使用 */ private int uniqueNumber = 0; public DynamicContext(Configuration configuration, Object parameterObject) { // <1> 初始化 bindings 参数 if (parameterObject != null && !(parameterObject instanceof Map)) { // 构建入参的 MetaObject 对象 MetaObject metaObject = configuration.newMetaObject(parameterObject); // 入参类型是否有对应的类型处理器 boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass()); bindings = new ContextMap(metaObject, existsTypeHandler); } else { bindings = new ContextMap(null, false); } // <2> 添加 bindings 的默认值 bindings.put(PARAMETER_OBJECT_KEY, parameterObject); bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId()); } }
类型 | 属性 | 说明 |
---|---|---|
ContextMap | bindings | 上下文的参数集合,包含附加参数(经过<bind /> 标签生成的,或者<foreach /> 标签解析参数保存的),以及几个默认值 |
StringJoiner | sqlBuilder | 保存本次解析后的SQL,每次添加字符串以空格做为分隔符 |
int | uniqueNumber | 惟一编号,在ForEachHandler 处理节点时须要用到,生成惟一数组做为集合中每一个元素的索引(做为后缀) |
bindings
参数,建立 ContextMap 对象
OGNL
库中的类,设置ContextMap对应的访问器是ContextAccessor
类bindings
中添加几个默认值:_parameter
> 入参对象,_databaseId
-> 数据库标识符DynamicContext的内部静态类,继承HashMap,用于保存解析动态SQL语句时的上下文的参数集合,代码以下:
static class ContextMap extends HashMap<String, Object> { private static final long serialVersionUID = 2977601501966151582L; /** * parameter 对应的 MetaObject 对象 */ private final MetaObject parameterMetaObject; /** * 是否有对应的类型处理器 */ private final boolean fallbackParameterObject; public ContextMap(MetaObject parameterMetaObject, boolean fallbackParameterObject) { this.parameterMetaObject = parameterMetaObject; this.fallbackParameterObject = fallbackParameterObject; } @Override public Object get(Object key) { String strKey = (String) key; if (super.containsKey(strKey)) { return super.get(strKey); } if (parameterMetaObject == null) { return null; } if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) { return parameterMetaObject.getOriginalObject(); } else { // issue #61 do not modify the context when reading return parameterMetaObject.getValue(strKey); } } }
重写了 HashMap 的 get(Object key) 方法,增长支持对 parameterMetaObject
属性的访问
DynamicContext的内部静态类,实现 ognl.PropertyAccessor
接口,上下文访问器,代码以下:
static class ContextAccessor implements PropertyAccessor { @Override public Object getProperty(Map context, Object target, Object name) { Map map = (Map) target; // 优先从 ContextMap 中,得到属性 Object result = map.get(name); if (map.containsKey(name) || result != null) { return result; } // <x> 若是没有,则从 PARAMETER_OBJECT_KEY 对应的 Map 中,得到属性 Object parameterObject = map.get(PARAMETER_OBJECT_KEY); if (parameterObject instanceof Map) { return ((Map) parameterObject).get(name); } return null; } @Override public void setProperty(Map context, Object target, Object name, Object value) { Map<Object, Object> map = (Map<Object, Object>) target; map.put(name, value); } @Override public String getSourceAccessor(OgnlContext arg0, Object arg1, Object arg2) { return null; } @Override public String getSourceSetter(OgnlContext arg0, Object arg1, Object arg2) { return null; } }
在DynamicContext的静态代码块中,设置OGNL
的属性访问器,设置了ContextMap.class的属性访问器为ContextAccessor
这里方法的入参中的target
,就是 ContextMap 对象
在重写的getProperty
方法中,先从 ContextMap 里面获取属性值(能够回过去看下ContextMap的get方法)
没有获取到则获取 PARAMETER_OBJECT_KEY
属性的值,若是是 Map 类型,则从这里面获取属性值
回看 DynamicContext 的构造方法,细品一下😄😄😄,先从Map中获取属性值,没有获取到则从parameterObject
入参对象中获取属性值
org.apache.ibatis.scripting.xmltags.SqlNode
:SQL Node接口,每一个XML Node会解析成对应的SQL Node对象,经过上下文能够对动态SQL进行逻辑处理,生成须要的结果
实现类以下图所示:
代码以下:
public interface SqlNode { /** * 应用当前 SQLNode 节点 * * @param context 正在解析 SQL 语句的上下文 * @return 是否应用成功 */ boolean apply(DynamicContext context); }
由于在解析SQL语句的时候咱们须要根据入参来处理不一样的SqlNode,经过其apply(DynamicContext context)
方法应用SqlNode节点,将节点转换成相应的SQL
咱们来看看它的实现类是如何处理相应的SQL Node的
org.apache.ibatis.scripting.xmltags.VarDeclSqlNode
:实现 SqlNode 接口,<bind />
标签对应的 SqlNode 实现类,代码以下:
public class VarDeclSqlNode implements SqlNode { /** * 变量名称 */ private final String name; /** * 表达式 */ private final String expression; public VarDeclSqlNode(String var, String exp) { name = var; expression = exp; } @Override public boolean apply(DynamicContext context) { // 获取该表达式转换后结果 final Object value = OgnlCache.getValue(expression, context.getBindings()); // 将该结果与变量名称设置到解析 SQL 语句的上下文中,这样接下来的解析过程当中能够获取到 name 的值 context.bind(name, value); return true; } }
经过OGNL
表达式expression
从DynamicContext上下文的ContextMap中获取转换后的结果,OgnlCache
在后面讲到
将name
与转换后的结果绑定到DynamicContext上下文中,后续处理其余节点能够获取到
org.apache.ibatis.scripting.xmltags.TrimSqlNode
:实现 SqlNode 接口,<trim/>
标签对应的 SqlNode 实现类
public class TrimSqlNode implements SqlNode { /** * MixedSqlNode,包含该<if />节点内全部信息 */ private final SqlNode contents; /** * 前缀,行首添加 */ private final String prefix; /** * 后缀,行尾添加 */ private final String suffix; /** * 须要删除的前缀,例如这样定义:'AND|OR' * 注意空格,这里是不会去除的 */ private final List<String> prefixesToOverride; /** * 须要删除的后缀,例如咱们这样定义:',|AND' * 注意空格,这里是不会去除的 */ private final List<String> suffixesToOverride; private final Configuration configuration; public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) { this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride)); } protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) { this.contents = contents; this.prefix = prefix; this.prefixesToOverride = prefixesToOverride; this.suffix = suffix; this.suffixesToOverride = suffixesToOverride; this.configuration = configuration; } }
在构造方法中解析<trim />
标签的属性,其中调用了parseOverrides
方法将|
做为分隔符分隔该字符串并所有大写,生成一个数组,相关属性可查看上面的注释
@Override public boolean apply(DynamicContext context) { // <1> 建立 FilteredDynamicContext 对象 FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context); // <2> 先解析 <trim /> 节点中的内容,将生成的 SQL 先存放在 FilteredDynamicContext 中 boolean result = contents.apply(filteredDynamicContext); /* * <3> 执行 FilteredDynamicContext 的应用 * 对上一步解析到的内容进行处理 * 处理完成后再将处理后的 SQL 拼接到 DynamicContext 中 */ filteredDynamicContext.applyAll(); return result; }
经过装饰器模式将context
装饰成FilteredDynamicContext
对象
由于<trim />
标签中定义了内容或者其余标签,都会解析成相应的SqlNode,保存在contents
中(MixedSqlNode)
因此这里须要先应用内部的SqlNode,转换后的SQL会先保存在FilteredDynamicContext中
对FilteredDynamicContext
中的SQL进行处理,也就是添加先后缀,去除先后缀的处理逻辑,而后将处理后的SQL拼接到context
中
TrimSqlNode的私有内部类,继承了DynamicContext类,对<trim />
标签逻辑的实现,代码以下:
private class FilteredDynamicContext extends DynamicContext { /** * 装饰的 DynamicContext 对象 */ private DynamicContext delegate; /** * 是否 prefix 已经被应用 */ private boolean prefixApplied; /** * 是否 suffix 已经被应用 */ private boolean suffixApplied; /** * StringBuilder 对象 * * @see #appendSql(String) */ private StringBuilder sqlBuffer; public FilteredDynamicContext(DynamicContext delegate) { super(configuration, null); this.delegate = delegate; this.prefixApplied = false; this.suffixApplied = false; this.sqlBuffer = new StringBuilder(); } public void applyAll() { // <1> 去除先后多余的空格,生成新的 sqlBuffer 对象 sqlBuffer = new StringBuilder(sqlBuffer.toString().trim()); // <2> 所有大写 String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH); // <3> 应用 TrimSqlNode 的 trim 逻辑 if (trimmedUppercaseSql.length() > 0) { applyPrefix(sqlBuffer, trimmedUppercaseSql); applySuffix(sqlBuffer, trimmedUppercaseSql); } delegate.appendSql(sqlBuffer.toString()); } @Override public Map<String, Object> getBindings() { return delegate.getBindings(); } @Override public void bind(String name, Object value) { delegate.bind(name, value); } @Override public int getUniqueNumber() { return delegate.getUniqueNumber(); } @Override public void appendSql(String sql) { sqlBuffer.append(sql); } @Override public String getSql() { return delegate.getSql(); } private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) { if (!prefixApplied) { prefixApplied = true; // prefixesToOverride 非空,先删除 if (prefixesToOverride != null) { for (String toRemove : prefixesToOverride) { if (trimmedUppercaseSql.startsWith(toRemove)) { sql.delete(0, toRemove.trim().length()); break; } } } // prefix 非空,再添加 if (prefix != null) { sql.insert(0, " "); sql.insert(0, prefix); } } } private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) { if (!suffixApplied) { suffixApplied = true; // suffixesToOverride 非空,先删除 if (suffixesToOverride != null) { for (String toRemove : suffixesToOverride) { if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) { int start = sql.length() - toRemove.trim().length(); int end = sql.length(); sql.delete(start, end); break; } } } // suffix 非空,再添加 if (suffix != null) { sql.append(" "); sql.append(suffix); } } } }
逻辑并不复杂,你们能够看下
org.apache.ibatis.scripting.xmltags.WhereSqlNode
:继承了TrimSqlNode
类,<where />
标签对应的 SqlNode 实现类,代码以下:
public class WhereSqlNode extends TrimSqlNode { /** * 也是经过 TrimSqlNode ,这里定义须要删除的前缀 */ private static List<String> prefixList = Arrays.asList("AND ", "OR ", "AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"); public WhereSqlNode(Configuration configuration, SqlNode contents) { // 设置前缀和须要删除的前缀 super(configuration, contents, "WHERE", prefixList, null, null); } }
基于TrimSqlNode
类,定义了须要添加的前缀为WHERE
和须要删除的前缀AND
和OR
org.apache.ibatis.scripting.xmltags.SetSqlNode
:继承了TrimSqlNode
类,<set />
标签对应的 SqlNode 实现类,代码以下:
public class SetSqlNode extends TrimSqlNode { /** * 也是经过 TrimSqlNode ,这里定义须要删除的前缀 */ private static final List<String> COMMA = Collections.singletonList(","); public SetSqlNode(Configuration configuration,SqlNode contents) { // 设置前缀、须要删除的前缀和后缀 super(configuration, contents, "SET", COMMA, null, COMMA); } }
基于TrimSqlNode
类,定义了须要添加的前缀为SET
、须要删除的前缀,
和须要删除的后缀,
org.apache.ibatis.scripting.xmltags.ForeachNode
:实现 SqlNode 接口,<foreach />
标签对应的 SqlNode 实现类
其中apply(DynamicContext context)
方法的处理逻辑饶了我半天,你们能够仔细看一下
public class ForEachSqlNode implements SqlNode { /** * 集合中元素绑定到上下文中 key 的前缀 */ public static final String ITEM_PREFIX = "__frch_"; /** * 表达式计算器 */ private final ExpressionEvaluator evaluator; /** * 须要遍历的集合类型,支持:list set map array */ private final String collectionExpression; /** * MixedSqlNode,包含该<where />节点内全部信息 */ private final SqlNode contents; /** * 开头 */ private final String open; /** * 结尾 */ private final String close; /** * 每一个元素以什么分隔 */ private final String separator; /** * 集合中每一个元素的值 */ private final String item; /** * 集合中每一个元素的索引 */ private final String index; private final Configuration configuration; public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) { this.evaluator = new ExpressionEvaluator(); this.collectionExpression = collectionExpression; this.contents = contents; this.open = open; this.close = close; this.separator = separator; this.index = index; this.item = item; this.configuration = configuration; } }
对每一个属性进行赋值,参考每一个属性上面的注释
@Override public boolean apply(DynamicContext context) { // 获取入参 Map<String, Object> bindings = context.getBindings(); /* * <1> 得到遍历的集合的 Iterable 对象,用于遍历 * 例如配置了 collection 为如下类型 * list:则从入参中获取到 List 集合类型的属性的值 * array:则从入参中获取到 Array 数组类型的属性的值,会转换成 ArrayList * map:则从入参中获取到 Map 集合类型的属性的值 */ final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings); if (!iterable.iterator().hasNext()) { // 集合中没有元素则无需遍历 return true; } boolean first = true; // <2> 添加 open 到 SQL 中 applyOpen(context); int i = 0; for (Object o : iterable) { // <3> 记录原始的 context 对象,下面经过两个装饰器对他进行操做 DynamicContext oldContext = context; // <4> 生成一个 context 装饰器 if (first || separator == null) { context = new PrefixedContext(context, ""); } else { // 设置其须要添加的前缀为分隔符 context = new PrefixedContext(context, separator); } // <5> 生成一个惟一索引值 int uniqueNumber = context.getUniqueNumber(); // Issue #709 // <6> 绑定到 context 中 if (o instanceof Map.Entry) { @SuppressWarnings("unchecked") Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o; /* * 和下面同理,只不过索引是 Map 的 key */ applyIndex(context, mapEntry.getKey(), uniqueNumber); applyItem(context, mapEntry.getValue(), uniqueNumber); } else { /* * 绑定当前集合中当前元素的索引到当前解析 SQL 语句的上下文中 * * 1. 'index' -> i * * 2. __frch_'index'_uniqueNumber -> i */ applyIndex(context, i, uniqueNumber); /* * 绑定集合中当前元素的值到当前解析 SQL 语句的上下文中 * * 1. 'item' -> o * * 2. __frch_'item'_uniqueNumber -> o * */ applyItem(context, o, uniqueNumber); } /* * 再装饰一下 PrefixedContext -> FilteredDynamicContext * * 前者进行前缀的添加,第一个元素添加后设置为已添加标记,后续不在添加 * 后者将<foreach />标签内的"#{item}"或者"#{index}"替换成上面咱们已经绑定的数据:"#{__frch_'item'_uniqueNumber}"或者"#{__frch_'index'_uniqueNumber}" * * <7> 进行转换,将<foreach />标签内部定义的内容进行转换 */ contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber)); if (first) { // <8> 判断 prefix 是否已经插入 first = !((PrefixedContext) context).isPrefixApplied(); } // <9> 恢复原始的 context 对象,由于目前 context 是装饰器 context = oldContext; i++; } // <10> 添加 close 到 SQL 中 applyClose(context); // <11> 移除 index 和 item 对应的绑定 context.getBindings().remove(item); context.getBindings().remove(index); return true; } private void applyIndex(DynamicContext context, Object o, int i) { if (index != null) { context.bind(index, o); context.bind(itemizeItem(index, i), o); } } private void applyItem(DynamicContext context, Object o, int i) { if (item != null) { context.bind(item, o); context.bind(itemizeItem(item, i), o); } } private void applyOpen(DynamicContext context) { if (open != null) { context.appendSql(open); } } private void applyClose(DynamicContext context) { if (close != null) { context.appendSql(close); } } private static String itemizeItem(String item, int i) { return ITEM_PREFIX + item + "_" + i; }
得到须要遍历的集合 Iterable 对象,调用ExpressionEvaluator
的evaluateIterable(String expression, Object parameterObject)
方法,根据表达式从参数中获取集合对象
OgnlCache
根据Ognl表达式从上下文的ContextMap中获取转换后的结果,OgnlCache
在后面会讲到若是定义了open
属性,则先拼接到SQL中
开始遍历集合 Iterable 对象,先记录context
原始对象为oldContext
,由于接下来须要对其进行两次装饰,而这里会再次进入
建立一个PrefixedContext
对象,装饰context
,主要是对集合中的每一个元素添加separator
分隔符
生成一个惟一索引值,也就是DynamicContext的uniqueNumber++
,这样集合中每一个元素都有一个惟一索引了
将集合中的当前元素绑定到上下文中,会保存如下信息:
applyIndex
:若是配置了index
属性,则将当前元素的索引值绑定到上下文的ContextMap中,保存两个数据:
'index'
-> i,其中'index'
就是咱们在<foreach />
标签中配置的index属性,i
就是当前元素在集合中的索引
__frch_'index'_uniqueNumber
-> i
applyItem
:若是配置了item
属性,则将当前元素绑定到上下文的ContextMap中,保存两个数据:
'item'
-> o,其中'item'
就是咱们在<foreach />
标签中配置的item属性,o
就是当前元素对象__frch_'item'_uniqueNumber
-> o再将PrefixedContext
对象装饰成FilteredDynamicContext
对象
而后应用<foreach />
标签内部的SqlNode节点们
主要是替换咱们在<foreach />
标签中定义的内容,替换成上面第6
步绑定的数据的key值,这样就能够获取到该key对应的value了
例如:将<foreach />
标签内的#{item}或者#{index}替换成第6
步已经绑定的数据的key值#{__frch_'item'_uniqueNumber}
或者#{__frch_'index'_uniqueNumber}
,而后拼接到SQL中
判断是否添加了open
前缀,添加了则遍历时不用再添加前缀
恢复原始的oldContext
对象,由于目前context
是装饰器,而后继续遍历
若是定义了close
属性,则拼接到SQL中
从上下文的ContextMap中移除第6
步绑定的第1条数据
第6
步中,若是是Map类型,i
对应的就是key值,o
对应的就是value值,为何两个方法都须要保存第1条数据?
由于<foreach />
标签中可能还有其余的标签,例如<if />
标签,它的判断条件中可能须要用到当前元素或者索引值,而表达式中使用了'index'
或者'item'
,那么就须要从上下文中获取到对应的值了
那么接下来咱们来看看内部定义的两个类:PrefixedContext和FilteredDynamicContext
ForeachNode的内部类,继承了DynamicContext,用于应用<foreach />
标签时添加分隔符
重写了appendSql方法,逻辑比较简单,判断是否须要添加分隔符,代码以下:
private class PrefixedContext extends DynamicContext { /** * 装饰的 DynamicContext 对象 */ private final DynamicContext delegate; /** * 须要添加的前缀 */ private final String prefix; /** * 是否已经添加 */ private boolean prefixApplied; public PrefixedContext(DynamicContext delegate, String prefix) { super(configuration, null); this.delegate = delegate; this.prefix = prefix; this.prefixApplied = false; } public boolean isPrefixApplied() { return prefixApplied; } @Override public Map<String, Object> getBindings() { return delegate.getBindings(); } @Override public void bind(String name, Object value) { delegate.bind(name, value); } @Override public void appendSql(String sql) { if (!prefixApplied && sql != null && sql.trim().length() > 0) { delegate.appendSql(prefix); prefixApplied = true; } delegate.appendSql(sql); } @Override public String getSql() { return delegate.getSql(); } @Override public int getUniqueNumber() { return delegate.getUniqueNumber(); } }
ForeachNode的私有静态内部类,继承了DynamicContext,用于应用<foreach />
标签时替换内部的#{item}或者#{index},
重写了appendSql方法,代码以下:
private static class FilteredDynamicContext extends DynamicContext { /** * 装饰的对象 */ private final DynamicContext delegate; /** * 集合中当前元素的索引 */ private final int index; /** * <foreach />定义的 index 属性 */ private final String itemIndex; /** * <foreach />定义的 item 属性 */ private final String item; public FilteredDynamicContext(Configuration configuration, DynamicContext delegate, String itemIndex, String item, int i) { super(configuration, null); this.delegate = delegate; this.index = i; this.itemIndex = itemIndex; this.item = item; } @Override public Map<String, Object> getBindings() { return delegate.getBindings(); } @Override public void bind(String name, Object value) { delegate.bind(name, value); } @Override public String getSql() { return delegate.getSql(); } @Override public void appendSql(String sql) { GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> { // 若是在`<foreach />`标签下的内容为经过item获取元素,则替换成`__frch_'item'_uniqueNumber` String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index)); /* * 若是在`<foreach />`标签中定义了index属性,而且标签下的内容为经过index获取元素 * 则替换成`__frch_'index'_uniqueNumber` */ if (itemIndex != null && newContent.equals(content)) { newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index)); } /* * 返回`#{__frch_'item'_uniqueNumber}`或者`#{__frch_'index'_uniqueNumber}` * 由于在前面已经将集合中的元素绑定在上下文的ContextMap中了,因此能够经过上面两个key获取到对应元素的值 * 例如绑定的数据: * 1. __frch_'item'_uniqueNumber = 对应的元素值 * 2. __frch_'index'_uniqueNumber = 对应的元素值的索引 */ return "#{" + newContent + "}"; }); delegate.appendSql(parser.parse(sql)); } @Override public int getUniqueNumber() { return delegate.getUniqueNumber(); } }
建立一个GenericTokenParser对象parser
,用于处理 #{}
建立一个TokenHandler处理器,大体的处理逻辑:
<foreach />
标签下的内容为经过item获取元素,则替换成__frch_'item'_uniqueNumber
<foreach />
标签中定义了index属性,而且标签下的内容为经过index获取元素,则替换成__frch_'index'_uniqueNumber
#{__frch_'item'_uniqueNumber}
或者#{__frch_'index'_uniqueNumber}
,由于在前面已经将集合中的元素绑定在上下文的ContextMap中了,因此能够经过上面两个key获取到对应元素的值调用parser
进行解析,使用第2
建立处理器进行处理,而后将转换后的结果拼接到SQL中
org.apache.ibatis.scripting.xmltags.IfSqlNode
:实现 SqlNode 接口,<if />
标签对应的 SqlNode 实现类,代码以下:
public class IfSqlNode implements SqlNode { /** * 表达式计算器 */ private final ExpressionEvaluator evaluator; /** * 判断条件的表达式 */ private final String test; /** * MixedSqlNode,包含该<if />节点内全部信息 */ 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) { // <1> 判断是否符合条件 if (evaluator.evaluateBoolean(test, context.getBindings())) { // <2> 解析该<if />节点中的内容 contents.apply(context); return true; } // <3> 不符合 return false; } }
调用ExpressionEvaluator
的evaluateBoolean(String expression, Object parameterObject)
方法,根据表达式从参数中获取结果
OgnlCache
根据Ognl表达式从上下文的ContextMap中获取转换后的结果,OgnlCache
在后面会讲到根据第1
步的结果判断是否应用<if />
标签内的SqlNode节点们
org.apache.ibatis.scripting.xmltags.ChooseSqlNode
:实现 SqlNode 接口,<choose />
标签对应的 SqlNode 实现类,代码以下:
public class ChooseSqlNode implements SqlNode { /** * <otherwise /> 标签对应的 SqlNode 节点 */ private final SqlNode defaultSqlNode; /** * <when /> 标签对应的 SqlNode 节点数组 */ private final List<SqlNode> ifSqlNodes; public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) { this.ifSqlNodes = ifSqlNodes; this.defaultSqlNode = defaultSqlNode; } @Override public boolean apply(DynamicContext context) { // <1> 先判断 <when /> 标签中,是否有符合条件的节点。 // 若是有,则进行应用。而且只因应用一个 SqlNode 对象 for (SqlNode sqlNode : ifSqlNodes) { if (sqlNode.apply(context)) { return true; } } // <2> 再判断 <otherwise /> 标签,是否存在 // 若是存在,则进行应用 if (defaultSqlNode != null) { defaultSqlNode.apply(context); return true; } // <3> 返回都失败 return false; } }
<choose />
下的全部<when />
标签所对应的IfSqlNode,有一个应用成功则返回true<when />
都不知足条件,则应用<otherwise />
标签下的内容所对应的SqlNodeorg.apache.ibatis.scripting.xmltags.StaticTextSqlNode
:实现 SqlNode 接口,用于保存静态文本,逻辑比较简单,直接拼接文本,代码以下:
public class StaticTextSqlNode implements SqlNode { /** * 静态内容 */ private final String text; public StaticTextSqlNode(String text) { this.text = text; } @Override public boolean apply(DynamicContext context) { // 直接往正在解析 SQL 语句的上下文的 SQL 中添加该内容 context.appendSql(text); return true; } }
org.apache.ibatis.scripting.xmltags.TextSqlNode
:实现了 SqlNode 接口,用于处理${}
,注入对应的值,代码以下:
public class TextSqlNode implements SqlNode { /** * 动态文本 */ private final String text; /** * 注入时的过滤器 */ private final Pattern injectionFilter; public TextSqlNode(String text) { this(text, null); } public TextSqlNode(String text, Pattern injectionFilter) { this.text = text; this.injectionFilter = injectionFilter; } public boolean isDynamic() { // <1> 建立 DynamicCheckerTokenParser 对象 DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser(); // <2> 建立 GenericTokenParser 对象 GenericTokenParser parser = createParser(checker); // <3> 执行解析,若是存在 '${ }',则 checker 会设置 isDynamic 为true parser.parse(text); // <4> 判断是否为动态文本 return checker.isDynamic(); } @Override public boolean apply(DynamicContext context) { // <1> 建立 BindingTokenParser 对象 // <2> 建立 GenericTokenParser 对象 GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); // <3> 执行解析 // <4> 将解析的结果,添加到 context 中 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) { // 从上下文中获取入参对象,在DynamicContext的构造方法中能够看到为何能够获取到 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); } // 使用 OGNL 表达式,得到对应的值 Object value = OgnlCache.getValue(content, context.getBindings()); String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null" // 使用过滤器进行过滤 checkInjection(srtValue); return srtValue; } private void checkInjection(String value) { if (injectionFilter != null && !injectionFilter.matcher(value).matches()) { throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern()); } } } }
在XML文件中编写SQL语句时,若是使用到了${}
做为变量时,那么会生成TextSqlNode对
象,能够回看XMLScriptBuilder
的parseDynamicTags()
方法
在MyBatis处理SQL语句时就会将
${}
进行替换成对应的参数,存在SQL注入的安全性问题而
#{}
就不同了,MyBatis会将其替换成?
占位符,经过java.sql.PreparedStatement
进行预编译处理,不存在上面的问题
parser
,用于处理${}
,设置的Token处理器为BindingTokenParser
handleToken(String content)
方法
OGNL
相关,感兴趣的小伙伴能够探讨一下😈${}
中内容对应的值,若是为null则设置为空字符串org.apache.ibatis.scripting.xmltags.MixedSqlNode
:实现 SqlNode 接口,用于保存多个SqlNode对象
由于一个SQL语句会被解析成多个SqlNode,且内部还会嵌套多个,因此使用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) { // 逐个应用 contents.forEach(node -> node.apply(context)); return true; } }
org.apache.ibatis.scripting.xmltags.OgnlCache
:用于处理Ognl表达式
在上面SqlNode的apply方法中,使用到的逻辑判断时获取表达式的结果则须要经过OgnlCache来进行解析
对OGNL不了解的小伙伴能够看下这篇文章:Ognl表达式基本原理和使用方法
代码以下:
public final class OgnlCache { /** * OgnlMemberAccess 单例,用于修改某个对象的成员为可访问 */ private static final OgnlMemberAccess MEMBER_ACCESS = new OgnlMemberAccess(); /** * OgnlClassResolver 单例,用于建立 Class 对象 */ private static final OgnlClassResolver CLASS_RESOLVER = new OgnlClassResolver(); /** * 表达式的缓存的映射 * * KEY:表达式 VALUE:表达式的缓存 @see #parseExpression(String) */ private static final Map<String, Object> expressionCache = new ConcurrentHashMap<>(); private OgnlCache() { // Prevent Instantiation of Static Class } public static Object getValue(String expression, Object root) { try { /* * <1> 建立 OgnlContext 对象,设置 OGNL 的成员访问器和类解析器,设置根元素为 root 对象 * 这里是调用 OgnlContext 的s etRoot 方法直接设置根元素,能够经过 'user.id' 获取结果 * 若是是经过 put 方法添加的对象,则取值时须要使用'#',例如 '#user.id' */ Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null); /* * <2> expression 转换成 Ognl 表达式 * <3> 根据 Ognl 表达式获取结果 */ return Ognl.getValue(parseExpression(expression), context, root); } catch (OgnlException e) { throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e); } } /** * 根据表达式构建一个 Ognl 表达式 * * @param expression 表达式,例如<if test="user.id > 0"> </if>,那这里传入的就是 "user.id > 0" * @return Ognl 表达式 * @throws OgnlException 异常 */ private static Object parseExpression(String expression) throws OgnlException { Object node = expressionCache.get(expression); if (node == null) { node = Ognl.parseExpression(expression); expressionCache.put(expression, node); } return node; } }
getValue
方法:根据Ognl表达式从Object中获取结果
建立 OgnlContext 对象,设置 OGNL 的成员访问器和类解析器,设置根元素为 root
对象
将建立 expression
转换成 Ognl 表达式,缓存起来
根据 Ognl 表达式从 root
对象中获取结果
本文讲述的是XML映射文件中的<select /> <insert /> <update /> <delete />
节点内的SQL语句如何被解析的
在XMLLanguageDriver
语言驱动类中,经过XMLScriptBuilder
对该到节点的内容进行解析,建立相应的SqlSource
资源对象
在其解析的过程会根据不一样的NodeHandler
节点处理器对MyBatis自定义的标签(<if /> <foreach />
等)进行处理,生成相应的SqlNode
对象,最后将全部的SqlNode
对象存放在MixedSqlNode
中
解析的过程当中会判断是否为动态的SQL语句,包含了MyBatis自定义的标签或者使用了${}
都是动态的SQL语句,动态的SQL语句建立DynamicSqlSource
对象,不然建立RawSqlSource
对象
那么关于SqlSource
是什么其实这里还不是特别了解,因为其涉及到的篇幅并很多,因此另起一篇文档《MyBatis初始化(四)之SQL初始化(下)》进行分析
参考文章:芋道源码《精尽 MyBatis 源码分析》