写点什么

Mybatis 源码解析:sql 参数处理,原来可以这么简单 -1

  • 2022 年 4 月 15 日
  • 本文字数:7193 字

    阅读完需:约 24 分钟

VariableTokenHandler.handleToken 传入 String 变量 globalId,将其替换成 1 并返回。


public String handleToken(String content) {//variables 里面存放全局的变量,为空直接 returnif (variables != null) {String key = content;//是否存在默认值,默认是 falseif (enableDefaultValue) {final int separatorIndex = content.indexOf(defaultValueSeparator);String defaultValue = null;if (separatorIndex >= 0) {key = content.substring(0, separatorIndex);defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());}if (defaultValue != null) {return variables.getProperty(key, defaultValue);}}//variables 是用来存放全局变量的容器。//这里会从全局变量中找到我们定义的 globalId,然后将对应的值返回,这样我们的 sql 就拼接完成了 if (variables.containsKey(key)) {return variables.getProperty(key);}}return "${" + content + "}";}}


解析器代码,根据传入的标记开始解析,这里传入开始标记{和结束标记}。在这之后还会用来解析 #{}。代码比较长,最好打个断点进去看。


//GenericTokenParser.parsepublic String parse(String text) {if (text == null || text.isEmpty()) {return "";}//查找开始标记,如果不存在返回-1 ,存在返回偏移量 int start = text.indexOf(openToken);if (start == -1) {return text;}char[] src = text.toCharArray();int offset = 0;final StringBuilder builder = new StringBuilder();//这个变量用来存放中间的字符,如 ${id}中的 idStringBuilder expression = null;//如果存在开始标志 while (start > -1) {//这里将从 offset 开始,一直到 start 的字符先放入 builder 中//例如 select * from user where id =if (start > 0 && src[start - 1] == '\') {// this open token is escaped. remove the backslash and continue.builder.append(src, offset, start - offset - 1).append(openToken);offset = start + openToken.length();} else {// found open token. let's search close token.if (expression == null) {expression = new StringBuilder();} else {expression.setLength(0);}builder.append(src, offset, start - offset);//更新偏移量 offset = start + openToken.length();//找到与开始标志对应的结束标志 int end = text.indexOf(closeToken, offset);//取到中间字符 globalIdwhile (end > -1) {if (end > offset && src[end - 1] == '\') {// this close token is escaped. remove the backslash and continue.expression.append(src, offset, end - offset - 1).append(closeToken);offset = end + closeToken.length();end = text.indexOf(closeToken, offset);} else {expression.append(src, offset, end - offset);break;}}if (end == -1) {// close token was not found.builder.append(src, start, src.length - start);offset = src.length;} else {//这里根据不同的处理器会有不同的操作,刚才传入的是 VariableTokenHandlerbuilder.append(handler.handleToken(expression.toString()));offset = end + closeToken.length();}}start = text.indexOf(openToken, offset);}if (offset < src.length) {builder.append(src, offset, src.length - offset);}return builder.toString();}到这里全局变量就解析完成了,那么如果在全局变量中没有找到对 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》开源 应的值该怎么办呢?例如我这里使用的 sql 是 select * from user where id = {globalId},那么根据 VariableTokenHandler 处理器,它会原封不动的进行返回,等待后文的解析。


顺便一提,这一部分的解析实在解析我们的配置文件的时候就发生了,方法入口为 context.evalNodes("select|insert|update|delete"),在解析配置的时候,其他节点也大量使用了 context.evalNodes()方法去解,所以只要当配置 mybatis.xml 文件中的 properties 节点解析完成之后,里面的变量就是能全局使用了,这也是为什么 properties 节点要放在第一个解析。


又由于这个通用解析器只解析 ${XXX}格式的变量,所以全局的变量不能写成 #{xxx}.


入参{id}</select>这个例子,我们没有在全局变量中定义 id,而是在方法中传入这个值。根据上文中的 VariableTokenHandler.handleToken 方法就会返回 ${id},表示这个参数全局变量中没有,是待解析的参数。


这是解析 buildStatementFromContext(context.evalNodes("select|insert|update|delete"));的后续代码,用来解析标签,并创建 mappedStaement,在第二章中也分析过,这里直接 copy 过来.


//XMLStatementBuilder.parseStatementNodepublic void parseStatementNode() {String id = context.getStringAttribute("id");String databaseId = context.getStringAttribute("databaseId");


if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}


String nodeName = context.getNode().getNodeName();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);//是否需要处理嵌套查询结果 group by


// 三组数据 分成一个嵌套的查询结果 boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);


// Include Fragments before parsingXMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);//替换 Includes 标签为对应的 sql 标签里面的值 includeParser.applyIncludes(context.getNode());


String parameterType = context.getStringAttribute("parameterType");Class<?> parameterTypeClass = resolveClass(parameterType);


//解析配置的自定义脚本语言驱动 mybatis plusString lang = context.getStringAttribute("lang");LanguageDriver langDriver = getLanguageDriver(lang);


// Parse selectKey after includes and remove them.//解析 selectKeyprocessSelectKeyNodes(id, parameterTypeClass, langDriver);


// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)//设置主键自增规则 KeyGenerator keyGenerator;String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);if (configuration.hasKeyGenerator(keyStatementId)) {keyGenerator = configuration.getKeyGenerator(keyStatementId);} else {keyGenerator = context.getBooleanAttribute("useGeneratedKeys",configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;}//解析 Sql 根据 sql 文本来判断是否需要动态解析 如果没有动态 sql 语句且 只有 #{}的时候 直接静态解析使用?占位 当有 ${} 不解析 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");//引用外部 parameterMap,已废弃 String parameterMap = context.getStringAttribute("parameterMap");//结果类型 String resultType = context.getStringAttribute("resultType");Class<?> resultTypeClass = resolveClass(resultType);//引用外部的 resultMapString resultMap = context.getStringAttribute("resultMap");//结果集类型,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种 String resultSetType = context.getStringAttribute("resultSetType");ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);if (resultSetTypeEnum == null) {resultSetTypeEnum = configuration.getDefaultResultSetType();}//(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值 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);}


找到解析 sql 的部分具体来分析,一层一层往下。


SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);RawLanguageDriver.createSqlSource 该类是 XMLLanguageDriver 的子类


@Overridepublic SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {SqlSource source = super.createSqlSource(configuration, script, parameterType);checkIsNotDynamic(source);return source;}XMLLanguageDriver.createSqlSource


public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);return builder.parseScriptNode();}XMLScriptBuilder.parseScriptNode


public SqlSource parseScriptNode() {MixedSqlNode rootSqlNode = parseDynamicTags(context);SqlSource sqlSource;//判断节点是否是动态的,包含是否包含 if、where 、choose、trim、foreach、bind、sql 标签,这个例子中我们进入 elseif (isDynamic) {//不解析 sqlSource = new DynamicSqlSource(configuration, rootSqlNode);} else {//用占位符方式来解析 sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);}return sqlSource;}这里进行判断 isDynamic 的值,这个方法我们只需要关注 textSqlNode.isDynamic()就行了。代码与之前解析 node 有些类似。


protected MixedSqlNode parseDynamicTags(XNode node) {List<SqlNode> contents = new ArrayList<>();NodeList children = node.getNode().getChildNodes();for (int i = 0; i < children.getLength(); i++) {//注意!!这里又 new 了一个 XNode,也就是说,这个节点中的 sql 语句又被解析了一次,解析方式和上文从同全局获取变量一样。//与上文不同的是,这里传入的是子节点,也就是 sql 文本语句,而上文解析的是整个 select 元素//这个 child 是临时变量,节点解析的结果不做保存 XNode child = node.newXNode(children.item(i));//判断节点类型 if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {String data = child.getStringBody("");TextSqlNode textSqlNode = new TextSqlNode(data);//这里判断语句是否是动态的 if (textSqlNode.isDynamic()) {contents.add(textSqlNode);isDynamic = true;} else {contents.add(new StaticTextSqlNode(data));}} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628String nodeName = child.getNode().getNodeName();NodeHandler handler = nodeHandlerMap.get(nodeName);if (handler == null) {throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");}handler.handleNode(child, contents);isDynamic = true;}}return new MixedSqlNode(contents);}TextSqlNode.isDynamic


public boolean isDynamic() {DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();//这里创建一个解析器进行解析 sql 语句,这里解析的是仍然是{}GenericTokenParser parser = createParser(checker);parser.parse(text);return checker.isDynamic();}private GenericTokenParser createParser(TokenHandler handler) {return new GenericTokenParser("{", "}", handler);}熟悉的代码,还是同样的解析器,用来处理 ${,和},不过这次的 hander 不同,为 DynamicCheckerTokenParser


//DynamicCheckerTokenParser.handleTokenpublic String handleToken(String content) {this.isDynamic = true;return null;}}这次的处理方式是将直接返回空,也就是说,sql 会变成 select * from user where id = null。但是返回的结果并没有被保存,parser.parse(text)并没有参数来接受它的返回值,所以这里只是用来更新 isDynamic 参数。


回到 XMLScriptBuilder.parseScriptNode 方法,这里根据 isDynamic 的布尔值,会有两种 SqlSource.DynamicSqlSource 和 RawSqlSourc Java 开源项目【ali1024.coding.net/public/P7/Java/git】 e。到这里配置文件就解析完成了,后续 sql 中的参数都是从方法中获取的,所以只能在执行的时候动态进行替换。


来到 query 查询方法,方法在第三章执行 sql 的时候简单说过。ms.getBoundSql 会获取绑定的封装 sql.


//CachingExecutor.querypublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}MappedStatement.getBoundSql


public BoundSql getBoundSql(Object parameterObject) {//获取绑定的 sqlBoundSql boundSql = sqlSource.getBoundSql(parameterObject);//获取 sql 中对应的参数 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();if (parameterMappings == null || parameterMappings.isEmpty()) {boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);}


// check for nested result maps in parameter mappings (issue #30)for (ParameterMapping pm : boundSql.getParameterMappings()) {String rmId = pm.getResultMapId();if (rmId != null) {ResultMap rm = configuration.getResultMap(rmId);if (rm != null) {hasNestedResultMaps |= rm.hasNestedResultMaps();}}}


return boundSql;}//DynamicSqlSource.getBoundSql。public BoundSql getBoundSql(Object parameterObject) {//parameterObject 中有我们方法传入的参数 DynamicContext context = new DynamicContext(configuration, parameterObject);//这里解析{}rootSqlNode.apply(context);SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());BoundSql boundSql = sqlSource.getBoundSql(parameterObject);context.getBindings().forEach(boundSql::setAdditionalParameter);return boundSql;}为什么是DynamicSqlSource而不是RawSqlSource,这个前文分析过,在替换完全局变量后,语句中如果还包含{},使用的就是 DynamicSqlSource。


//MixedSqlNode.applypublic boolean apply(DynamicContext context) {contents.forEach(node -> node.apply(context));return true;}TextSqlNode.apply


@Overridepublic boolean apply(DynamicContext context) {contents.forEach(node -> node.apply(context));return true;}这里再次创建了 ${}的解析器,这次的 handler 是 BindingTokenParser


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


private GenericTokenParser createParser(TokenHandler handler) {return new GenericTokenParser("{", "}", handler);}BindingTokenParser.handleToken,如果sql中存在{},就会将其替换成具体的参数,语句就变成 select * from user where id = 1,就能直接执行了


public String handleToken(String content) {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);}Object value = OgnlCache.getValue(content, context.getBindings());String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"checkInjection(srtValue);return srtValue;}

最后

由于篇幅限制,小编在此截出几张知识讲解的图解







用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
Mybatis源码解析:sql参数处理,原来可以这么简单-1_Java_爱好编程进阶_InfoQ写作平台