玖叶教程网

前端编程开发入门

Mybatis获取SQL(2) - 处理动态SQL

1.XMLScriptBuilder处理动态SQL

StatementHandler在执行数据库语句时,SQL语句保存在BoundSql中,而BoundSql通过SqlSource得到

SqlSource会通过XMLScriptBuilder来创建,在XMLScriptBuilder创建SqlSource之前会对动态SQL标签进行处理,处理方法是parseDynamicTags

在parseDynamicTags方法中,获取Statement节点的子节点

如果节点是ELEMENT_NODE类型,通过节点名称获取NodeHandler并处理节点,否则设置为TextSqlNode

public class XMLScriptBuilder extends BaseBuilder {
    protected MixedSqlNode parseDynamicTags(XNode node) {
        List<SqlNode> contents = new ArrayList();
        NodeList children = node.getNode().getChildNodes();
		//遍历子节点
        for(int i = 0; i < children.getLength(); ++i) {
            XNode child = node.newXNode(children.item(i));
            String nodeName;
			//不是CDATA_SECTION_NODE和TEXT_NODE类型
            if (child.getNode().getNodeType() != 4 && child.getNode().getNodeType() != 3) {
				//ELEMENT_NODE节点类型处理
                if (child.getNode().getNodeType() == 1) {
                    nodeName = child.getNode().getNodeName();
					//通过Node名称获取NodeHandler
                    XMLScriptBuilder.NodeHandler handler = (XMLScriptBuilder.NodeHandler)this.nodeHandlerMap.get(nodeName);
                    if (handler == null) {
                        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
                    }
					//使用NodeHander处理Node
                    handler.handleNode(child, contents);
                    this.isDynamic = true;
                }
            } else {
				//TextSqlNode处理
                nodeName = child.getStringBody("");
                TextSqlNode textSqlNode = new TextSqlNode(nodeName);
                if (textSqlNode.isDynamic()) {
                    contents.add(textSqlNode);
                    this.isDynamic = true;
                } else {
                    contents.add(new StaticTextSqlNode(nodeName));
                }
            }
        }
        return new MixedSqlNode(contents);
    }
	...
}

Node类型定义如下

nodeHandlerMap中保存了节点名和对应的NodeHandler的关系,在构造方法中调用initNodeHandlerMap进行初始化

public class XMLScriptBuilder extends BaseBuilder {
    private final Map<String, XMLScriptBuilder.NodeHandler> nodeHandlerMap;

    private void initNodeHandlerMap() {
        this.nodeHandlerMap.put("trim", new XMLScriptBuilder.TrimHandler());
        this.nodeHandlerMap.put("where", new XMLScriptBuilder.WhereHandler());
        this.nodeHandlerMap.put("set", new XMLScriptBuilder.SetHandler());
        this.nodeHandlerMap.put("foreach", new XMLScriptBuilder.ForEachHandler());
        this.nodeHandlerMap.put("if", new XMLScriptBuilder.IfHandler());
        this.nodeHandlerMap.put("choose", new XMLScriptBuilder.ChooseHandler());
        this.nodeHandlerMap.put("when", new XMLScriptBuilder.IfHandler());
        this.nodeHandlerMap.put("otherwise", new XMLScriptBuilder.OtherwiseHandler());
        this.nodeHandlerMap.put("bind", new XMLScriptBuilder.BindHandler());
    }
	...
}

2.SqlNode处理

各个节点解析后都会生成SqlNode节点对象,SqlNode会保存到List<SqlNode>中 ,这个集合在所有的节点解析完后会生成MixedSqlNode对象去生成DynamicSqlSource对象

SqlNode是一个接口,只定义了一个apply方法

DynamicSqlSource生成BoundSql的时候,就会调用MixedSqlNode的apply方法,传入的参数context中会附带parameterObject对象,这个是执行数据库操作的参数

而在MixedSqlNode的apply方法中,会遍历所有的SqlNode,回调apply方法,这些SqlNode就是之前NodeHandler解析的节点集合

3.TrimHandler

对于trim节点,对调用TrimHandler的handleNode方法,方法中首先会解析prefix、prefixOverrdes、suffix、suffixOverridex属性,然后创建一个TrimSqlNode对象

public class XMLScriptBuilder extends BaseBuilder {
    private class TrimHandler implements XMLScriptBuilder.NodeHandler {
        public TrimHandler() {
        }

        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
            MixedSqlNode mixedSqlNode = XMLScriptBuilder.this.parseDynamicTags(nodeToHandle);
            String prefix = nodeToHandle.getStringAttribute("prefix");
            String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
            String suffix = nodeToHandle.getStringAttribute("suffix");
            String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
            TrimSqlNode trim = new TrimSqlNode(XMLScriptBuilder.this.configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
            targetContents.add(trim);
        }
    }
	...
}

TrimSqlNode构造方法会对属性进行赋值,然后在回调的apply方法中调用FilteredDynamicContext的applyAll方法处理

FilteredDynamicContext的applyAll方法中,调用applyPrefix和applySuffix方法将sql解析后放入到DynamicContext的sql属性中

Trim标签的作用在包含的内容上添加前缀和后缀,也可以用来移除动态sql中最前面和最后面多余的字符

public class TrimSqlNode implements SqlNode {
    private class FilteredDynamicContext extends DynamicContext {
 		public void applyAll() {
            this.sqlBuffer = new StringBuilder(this.sqlBuffer.toString().trim());
            String trimmedUppercaseSql = this.sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
            if (trimmedUppercaseSql.length() > 0) {
                this.applyPrefix(this.sqlBuffer, trimmedUppercaseSql);
                this.applySuffix(this.sqlBuffer, trimmedUppercaseSql);
            }
            this.delegate.appendSql(this.sqlBuffer.toString());
        }

		//前缀处理
        private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
            if (!this.prefixApplied) {
                this.prefixApplied = true;
				//prefixesToOverride处理
                if (TrimSqlNode.this.prefixesToOverride != null) {
					//遍历prefixesToOverride中的字符,如果sql以这些字符开头,则删除开头的这几个字符
                    Iterator var3 = TrimSqlNode.this.prefixesToOverride.iterator();
                    while(var3.hasNext()) {
                        String toRemove = (String)var3.next();
                        if (trimmedUppercaseSql.startsWith(toRemove)) {
                            sql.delete(0, toRemove.trim().length());
                            break;
                        }
                    }
                }
				//将prefix添加到sql最前面
                if (TrimSqlNode.this.prefix != null) {
                    sql.insert(0, " ");
                    sql.insert(0, TrimSqlNode.this.prefix);
                }
            }
        }	

		//applySuffix后缀处理
        private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
            if (!this.suffixApplied) {
                this.suffixApplied = true;
				//suffixesToOverride 处理
                if (TrimSqlNode.this.suffixesToOverride != null) {
					//如果sql是以suffixesToOverride中的字符结果,删除该字符
                    label33: {
                        Iterator var3 = TrimSqlNode.this.suffixesToOverride.iterator();
                        String toRemove;
                        do {
                            if (!var3.hasNext()) {
                                break label33;
                            }
                            toRemove = (String)var3.next();
                        } while(!trimmedUppercaseSql.endsWith(toRemove) && !trimmedUppercaseSql.endsWith(toRemove.trim()));

                        int start = sql.length() - toRemove.trim().length();
                        int end = sql.length();
                        sql.delete(start, end);
                    }
                }

				//将suffix的字符添加到sql最后
                if (TrimSqlNode.this.suffix != null) {
                    sql.append(" ");
                    sql.append(TrimSqlNode.this.suffix);
                }
            }
        }
		...
	}
}

4.WhereHandler

对于where节点,会调用WhereHandler的handleNode方法,方法中创建一个WhereSqlNode对象

public class XMLScriptBuilder extends BaseBuilder {
    private class WhereHandler implements XMLScriptBuilder.NodeHandler {
        public WhereHandler() {
        }

        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
            MixedSqlNode mixedSqlNode = XMLScriptBuilder.this.parseDynamicTags(nodeToHandle);
            WhereSqlNode where = new WhereSqlNode(XMLScriptBuilder.this.configuration, mixedSqlNode);
            targetContents.add(where);
        }
    }
	...
}

WhereSqlNode实际上是继承TrimSqlNode,其中prefix固定为WHERE,prefixesToOverride为AND和OR,根据TrimSqlNode的解析,可以知道where节点就是在包含的内容前面加上WHERE,并删除最前面的AND或者OR字符

public class WhereSqlNode extends 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, (String)null, (List)null);
    }
}

5.SetHandler

对于set节点,会调用SetHandler的handleNode方法,方法中创建一个SetSqlNode对象

public class XMLScriptBuilder extends BaseBuilder {
    private class SetHandler implements XMLScriptBuilder.NodeHandler {
        public SetHandler() {
        }

        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
            MixedSqlNode mixedSqlNode = XMLScriptBuilder.this.parseDynamicTags(nodeToHandle);
            SetSqlNode set = new SetSqlNode(XMLScriptBuilder.this.configuration, mixedSqlNode);
            targetContents.add(set);
        }
    }
	...
}

SetSqlNode也是继承TrimSqlNode,其中prefix固定为SET,prefixesToOverride和suffixesToOverride为“,”,根据TrimSqlNode的解析,可以知道set节点就是在包含的内容前面加上SET,并删除最前和最后面的“,”字符

public class SetSqlNode extends TrimSqlNode {
    private static final List<String> COMMA = Collections.singletonList(",");

    public SetSqlNode(Configuration configuration, SqlNode contents) {
        super(configuration, contents, "SET", COMMA, (String)null, COMMA);
    }
}

6.ForEachHandler

对于foreach节点,会调用ForEachHandler的handleNode方法,方法中首先会解析collection、item、index、open、close、separator属性,然后创建一个forEachSqlNode对象

public class XMLScriptBuilder extends BaseBuilder {
    private class ForEachHandler implements XMLScriptBuilder.NodeHandler {
        public ForEachHandler() {
        }

        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
            MixedSqlNode mixedSqlNode = XMLScriptBuilder.this.parseDynamicTags(nodeToHandle);
            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 = new ForEachSqlNode(XMLScriptBuilder.this.configuration, mixedSqlNode, collection, index, item, open, close, separator);
            targetContents.add(forEachSqlNode);
        }
    }
	...
}

ForEachSqlNode的apply方法

public class ForEachSqlNode implements SqlNode {
   public boolean apply(DynamicContext context) {
        Map<String, Object> bindings = context.getBindings();
        Iterable<?> iterable = this.evaluator.evaluateIterable(this.collectionExpression, bindings);
        if (!iterable.iterator().hasNext()) {
            return true;
        } else {
            boolean first = true;
			//拼接open字符串
            this.applyOpen(context);
            int i = 0;

			//遍历collection集合
            for(Iterator var6 = iterable.iterator(); var6.hasNext(); ++i) {
                Object o = var6.next();
                ForEachSqlNode.PrefixedContext context;
				//根据separator创建PrefixedContext
                if (!first && this.separator != null) {
                    context = new ForEachSqlNode.PrefixedContext(context, this.separator);
                } else {
                    context = new ForEachSqlNode.PrefixedContext(context, "");
                }
				//重新设置参数
                int uniqueNumber = context.getUniqueNumber();
                if (o instanceof Entry) {
                    Entry<Object, Object> mapEntry = (Entry)o;
                    this.applyIndex(context, mapEntry.getKey(), uniqueNumber);
                    this.applyItem(context, mapEntry.getValue(), uniqueNumber);
                } else {
                    this.applyIndex(context, i, uniqueNumber);
                    this.applyItem(context, o, uniqueNumber);
                }

                this.contents.apply(new ForEachSqlNode.FilteredDynamicContext(this.configuration, context, this.index, this.item, uniqueNumber));
                if (first) {
                    first = !((ForEachSqlNode.PrefixedContext)context).isPrefixApplied();
                }
                context = context;
            }
			//拼接close字符串
            this.applyClose(context);
			//移除参数
            context.getBindings().remove(this.item);
            context.getBindings().remove(this.index);
            return true;
        }
    }
	...
}

7.IfHandler

对于if节点,会调用IfHandler的handleNode方法,方法中首先会解析test属性,然后创建一个IfSqlNode对象

public class XMLScriptBuilder extends BaseBuilder {
    private class IfHandler implements XMLScriptBuilder.NodeHandler {
        public IfHandler() {
        }

        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
            MixedSqlNode mixedSqlNode = XMLScriptBuilder.this.parseDynamicTags(nodeToHandle);
            String test = nodeToHandle.getStringAttribute("test");
            IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
            targetContents.add(ifSqlNode);
        }
    }
	...
}

IfSqlNode的apply方法中,如果test中的条件成立,则将节点内的sql进行拼接

8.ChooseHandler

对于choose节点,会调用ChooseHandler的handleNode方法,方法中会对when节点和otherwise节点分别进行解析,对应使用IfHandler和OtherwiseHandler来处理

9.OtherwiseHandler

对于otherwise节点,会调用OtherwiseHandler的handleNode方法,方法中直接将sql添加到targetContents中

public class XMLScriptBuilder extends BaseBuilder {
    private class OtherwiseHandler implements XMLScriptBuilder.NodeHandler {
        public OtherwiseHandler() {
        }

        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
            MixedSqlNode mixedSqlNode = XMLScriptBuilder.this.parseDynamicTags(nodeToHandle);
            targetContents.add(mixedSqlNode);
        }
    }
	...
}

10.BindHandler

对于bind节点,会调用BindHandler的handleNode方法,方法中首先会解析name、value属性,然后创建一个VarDeclSqlNode对象

public class XMLScriptBuilder extends BaseBuilder {
    private class BindHandler implements XMLScriptBuilder.NodeHandler {
        public BindHandler() {
        }

        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
            String name = nodeToHandle.getStringAttribute("name");
            String expression = nodeToHandle.getStringAttribute("value");
            VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
            targetContents.add(node);
        }
    }
	...
}

VarDeclSqlNode的apply方法中,会将bind属性的name和value进行绑定,在参数传递时使用,bind标签可以用于参数的和字符串的拼接形成新的参数

public class VarDeclSqlNode implements SqlNode {
    private final String name;
    private final String expression;

    public VarDeclSqlNode(String var, String exp) {
        this.name = var;
        this.expression = exp;
    }

    public boolean apply(DynamicContext context) {
        Object value = OgnlCache.getValue(this.expression, context.getBindings());
        context.bind(this.name, value);
        return true;
    }
}

11.TextSqlNode处理

其他的非标签的sql,会创建一个TextSqlNode,首先会通过isDynamic检查如果返回true,则通过TextSqlNode进行处理

否则创建StaticTextSqlNode对象,StaticTextSqlNode中不会对sql进行处理,这里详细看TextSqlNode的处理

isDynamic方法中如果字符串中包含了${},会返回true,则会使用TextSqlNode的apply方法进行处理

apply方法中创建BindingTokenParser和GenericTokenParser对象,然后通过GenericTokenParser的parse方法处理字符串,处理后拼接到最终的sql上

GenericTokenParser中会将openToken和closeToken之间的字符去掉,替代为handler的handleToken方法返回的值

public class TextSqlNode implements SqlNode {
    public boolean apply(DynamicContext context) {
        GenericTokenParser parser = this.createParser(new TextSqlNode.BindingTokenParser(context, this.injectionFilter));
        context.appendSql(parser.parse(this.text));
        return true;
    }

    private GenericTokenParser createParser(TokenHandler handler) {
        return new GenericTokenParser("${", "}", handler);
    }
	...
}

这里handler是BindingTokenParser的实例,handleToken方法中会获取${}中字符串对应的参数的值,然后直接拼接到sql中

现在可以了解到Mybatis中$和#参数的区别,$是直接进行拼接,会有sql注入的风险,#是替换为?,然后通过PrepareStatement预编译传递参数

public class TextSqlNode implements SqlNode {
    private static class BindingTokenParser implements TokenHandler {
        private DynamicContext context;
        private Pattern injectionFilter;

        public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
            this.context = context;
            this.injectionFilter = injectionFilter;
        }

        public String handleToken(String content) {
            Object parameter = this.context.getBindings().get("_parameter");
            if (parameter == null) {
                this.context.getBindings().put("value", (Object)null);
            } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
                this.context.getBindings().put("value", parameter);
            }

            Object value = OgnlCache.getValue(content, this.context.getBindings());
            String srtValue = value == null ? "" : String.valueOf(value);
            this.checkInjection(srtValue);
            return srtValue;
        }

        private void checkInjection(String value) {
            if (this.injectionFilter != null && !this.injectionFilter.matcher(value).matches()) {
                throw new ScriptingException("Invalid input. Please conform to regex" + this.injectionFilter.pattern());
            }
        }
    }
}

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言