mybatis学习之路----#{}, ${}两种传参数方式的区别--附源码解读
2017-09-09 21:38
435 查看
点滴记载,点滴进步,愿自己更上一层楼。
首先下个结论,
${} 会将传入的参数完全拼接到sql语句中,也就是相当于一个拼接符号。
也就是,最后的处理方式就相当于
String sql = select * from user where id=${value}....
mybatis会将 ${value} 完全替换为参数 value 的值 相当于replace("${value}",
value)的过程。
实际上mybatis 是先将sql转成char数组
然后截取 "${"前头的部分放入到容器,替换 以"${"开头 以
"}"结尾的内容。所以说它的作用相当于拼接符号。拼接后直接作为sql语句的一部分,所以如
果参数是可执行代码,sql是会直接执行的。这就是为什么它会导致sql注入。
#{} 是一个占位符, mybatis最后会将这个占位符,替换成?,
最后才进行prepareStatement的相应位置的?的替换,也就是 state.setString(序号,值),setInt(序号,值)....
熟悉jdbc的人对这段应该不陌生。
下面来进行源码级别的论证。
首先写一个带有${} #{} 两种参数方式的sql
首先说明 mybatis处理 "${}" 和 "#{}" 的主要处理逻辑的源码位于
GenericTokenParser.java 的parse方法中。
源码奉上:
mybatis对这两种参数的处理分为两个阶段,
首先为构建sqlsessionFactory的时候,这个时候的处理为,如果sql中含有${}则该条sql不做处理,如果sql中全是#{}则替换为 ? 。
然后是sqlSession执行sql阶段,该阶段首先会将${value} 原封不动的替换为 value传过来的值,然后在将sql中的#{} 替换为 ?
最后才是preparestatement 将sql中的?替换为参数值,最后执行sql。
大概的处理逻辑就是这个,下面是源码跟踪与说明。让人更明了。
首先第一阶段,构建sqlsessionFactory阶段。
入口 SqlSessionFactoryBuilder 的 方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
var5 = this.build(e.parse());
然后会进入 XMLConfigBuilder 的
继续深入会到XMLMapperBuilder的
继续深入会进入到 XMLStatementBuilder的
SqlSource sqlSource = langDriver.createSqlSource(this.configuration, this.context, parameterTypeClass);
继续深入,XMLScriptBuilder
然后 parser.parse(this.text); 就到了我门的目的地了。
终于到了最上面的GenericTokenParser 的解析部分了。
可以看到,
1 首先弄了个容器,Stringbuilder用来装替换后的sql。
2 然后是判断sql非空等等。然后将sql转成 char[] src 数组
3 开始循环,拿到 this.openToken 的位置,这里估计会问,this.openToken是什么鬼,通过debug发现这个就是 “${”.也就是如果sql语句里面包含有${} 的话 会进入循环
4 来看看循环体干了什么,
if(start > 0 && src[start - 1] == 92) { 首先判断 this.openToken开始位置
然后看其前以为是不是 "\" 92 对应的char就是反斜杠。显然不进,
int end = text.indexOf(this.closeToken, start);
if(end == -1) { 然后拿到 this.closeToken 的位置 显然也不会进这个,那就只能进另外的else了。
// 将 ${ 前面的字符串放入到builder
builder.append(src, offset, start - offset);
// 记录this.openToken的位置坐标
offset = start + this.openToken.length();
// 拿到this.openToken和this.closeToken中间的字符串
String content = new String(src, offset, end - offset);
// 替换该字符串
builder.append(this.handler.handleToken(content));
// 记录this.closeToken 所在位置
offset = end + this.closeToken.length();
如果还有${}内容,继续循环,
注意上面代码中的this.openToken 可能为${ 也可能为 #{
走完了这步之后如果发现sql中确实有${},就将isDynamic标识为true,这个标识很有用,具体可以看XMLScriptBuilder
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resource);的执行过程。
sqlsession的执行sql过程,还要进入到那个parse方法中进行替换操作,
这个时候要分为两种情况,如果构建SqlSessionFactory
的时候,
SqlSource 的实例为DynamicSqlSource 的话,因为sql还是xml中的形态,所以它会做两件事,
第一件就是将${}替换为对应的value值,
第二件事就是将#{}替换为?
也就是 DynamicSqlSource 的
最后sql的样子就是
select * from t_user where username like '%黄%' and password=?
最后才是statement执行sql,返回查询结果。注意上面的替换后的sql,并不是将${}替换为?,而是替换为传过来的参数值。
本来想简单弄弄算了,结果弄得这么繁琐,都有点mybatis源码解读的意思了。
上面对#{} ${}的区别,在源码层次做了讲解,希望有所帮助,反正我现在是印象深刻了。
-_- -_- -_-
-_- -_- -_- -_-
-_-
首先下个结论,
${} 会将传入的参数完全拼接到sql语句中,也就是相当于一个拼接符号。
也就是,最后的处理方式就相当于
String sql = select * from user where id=${value}....
mybatis会将 ${value} 完全替换为参数 value 的值 相当于replace("${value}",
value)的过程。
实际上mybatis 是先将sql转成char数组
然后截取 "${"前头的部分放入到容器,替换 以"${"开头 以
"}"结尾的内容。所以说它的作用相当于拼接符号。拼接后直接作为sql语句的一部分,所以如
果参数是可执行代码,sql是会直接执行的。这就是为什么它会导致sql注入。
#{} 是一个占位符, mybatis最后会将这个占位符,替换成?,
最后才进行prepareStatement的相应位置的?的替换,也就是 state.setString(序号,值),setInt(序号,值)....
熟悉jdbc的人对这段应该不陌生。
下面来进行源码级别的论证。
首先写一个带有${} #{} 两种参数方式的sql
<!--模糊查询 demo2 直接用concat拼字符串 此种可以有效防止sql注入 --> <select id="findUserByName4" parameterType="com.soft.mybatis.model.User" resultMap="userMap"> select * from t_user where username like concat('%', #{username} ,'%') and password=#{password} </select>接口
/** * 验证${} #{} 两种传参处理方式 * @return */ List<User> findUserByName04(User user);实现
/** * 模糊查询第三种方式 直接接收拼好的字符串 * @return */ public List<User> findUserByName04(User user) { String statementId = "test.findUserByName4"; return findUserList(statementId,user); } /** * 由于都需要三种方式查询除了两个地方不一样其他的处理都相同,此处抽取相同部分 * @param statementId * @param param * @return */ private List<User> findUserList(String statementId, Object param){ SqlSession sqlSession = null; try { sqlSession = SqlsessionUtil.getSqlSession(); return sqlSession.selectList(statementId,param); } catch (Exception e) { e.printStackTrace(); } finally { SqlsessionUtil.closeSession(sqlSession); } return new ArrayList<User>(); }测试代码:
@Test public void findUserByName04() throws Exception { User user = new User(); user.setUsername("小黄"); user.setPassword("123456"); List<User> users = dao.findUserByName04(user); for(User userss:users){ System.out.println("findUserByName04:" + userss); } }
首先说明 mybatis处理 "${}" 和 "#{}" 的主要处理逻辑的源码位于
GenericTokenParser.java 的parse方法中。
源码奉上:
public String parse(String text) { StringBuilder builder = new StringBuilder(); if(text != null && text.length() > 0) { char[] src = text.toCharArray(); int offset = 0; for(int start = text.indexOf(this.openToken, offset); start > -1; start = text.indexOf(this.openToken, offset)) { if(start > 0 && src[start - 1] == 92) { builder.append(src, offset, start - offset - 1).append(this.openToken); offset = start + this.openToken.length(); } else { int end = text.indexOf(this.closeToken, start); if(end == -1) { builder.append(src, offset, src.length - offset); offset = src.length; } else { builder.append(src, offset, start - offset); offset = start + this.openToken.length(); String content = new String(src, offset, end - offset); builder.append(this.handler.handleToken(content)); offset = end + this.closeToken.length(); } } } if(offset < src.length) { builder.append(src, offset, src.length - offset); } } return builder.toString(); }
mybatis对这两种参数的处理分为两个阶段,
首先为构建sqlsessionFactory的时候,这个时候的处理为,如果sql中含有${}则该条sql不做处理,如果sql中全是#{}则替换为 ? 。
然后是sqlSession执行sql阶段,该阶段首先会将${value} 原封不动的替换为 value传过来的值,然后在将sql中的#{} 替换为 ?
最后才是preparestatement 将sql中的?替换为参数值,最后执行sql。
大概的处理逻辑就是这个,下面是源码跟踪与说明。让人更明了。
首先第一阶段,构建sqlsessionFactory阶段。
入口 SqlSessionFactoryBuilder 的 方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
var5 = this.build(e.parse());
然后会进入 XMLConfigBuilder 的
private void parseConfiguration(XNode root) { try { this.propertiesElement(root.evalNode("properties")); this.typeAliasesElement(root.evalNode("typeAliases")); this.pluginElement(root.evalNode("plugins")); this.objectFactoryElement(root.evalNode("objectFactory")); this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); this.settingsElement(root.evalNode("settings")); this.environmentsElement(root.evalNode("environments")); this.databaseIdProviderElement(root.evalNode("databaseIdProvider")); this.typeHandlerElement(root.evalNode("typeHandlers")); this.mapperElement(root.evalNode("mappers")); } catch (Exception var3) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3); } }这个方法处理各种节点,看到this.mapperElement(root.evalNode("mappers"));是不是很熟悉,这里就是处理mapper文件的地方,
继续深入会到XMLMapperBuilder的
private void configurationElement(XNode context) { try { String e = context.getStringAttribute("namespace"); if(e.equals("")) { throw new BuilderException("Mapper\'s namespace cannot be empty"); } else { this.builderAssistant.setCurrentNamespace(e); this.cacheRefElement(context.evalNode("cache-ref")); this.cacheElement(context.evalNode("cache")); this.parameterMapElement(context.evalNodes("/mapper/paramet ebd1 erMap")); this.resultMapElements(context.evalNodes("/mapper/resultMap")); this.sqlElement(context.evalNodes("/mapper/sql")); this.buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } } catch (Exception var3) { throw new BuilderException("Error parsing Mapper XML. Cause: " + var3, var3); } }看到 this.buildStatementFromContext(context.evalNodes("select|insert|update|delete")); 又眼熟,这里就是处理sql的入口。
继续深入会进入到 XMLStatementBuilder的
public void parseStatementNode() { String id = this.context.getStringAttribute("id"); String databaseId = this.context.getStringAttribute("databaseId"); if(this.databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { Integer fetchSize = this.context.getIntAttribute("fetchSize"); Integer timeout = this.context.getIntAttribute("timeout"); String parameterMap = this.context.getStringAttribute("parameterMap"); String parameterType = this.context.getStringAttribute("parameterType"); Class parameterTypeClass = this.resolveClass(parameterType); String resultMap = this.context.getStringAttribute("resultMap"); String resultType = this.context.getStringAttribute("resultType"); String lang = this.context.getStringAttribute("lang"); LanguageDriver langDriver = this.getLanguageDriver(lang); Class resultTypeClass = this.resolveClass(resultType); String resultSetType = this.context.getStringAttribute("resultSetType"); StatementType statementType = StatementType.valueOf(this.context.getStringAttribute("statementType", StatementType.PREPARED.toString())); ResultSetType resultSetTypeEnum = this.resolveResultSetType(resultSetType); String nodeName = this.context.getNode().getNodeName(); SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = this.context.getBooleanAttribute("flushCache", Boolean.valueOf(!isSelect)).booleanValue(); boolean useCache = this.context.getBooleanAttribute("useCache", Boolean.valueOf(isSelect)).booleanValue(); boolean resultOrdered = this.context.getBooleanAttribute("resultOrdered", Boolean.valueOf(false)).booleanValue(); XMLIncludeTransformer includeParser = new XMLIncludeTransformer(this.configuration, this.builderAssistant); includeParser.applyIncludes(this.context.getNode()); this.processSelectKeyNodes(id, parameterTypeClass, langDriver); SqlSource sqlSource = langDriver.createSqlSource(this.configuration, this.context, parameterTypeClass); String resultSets = this.context.getStringAttribute("resultSets"); String keyProperty = this.context.getStringAttribute("keyProperty"); String keyColumn = this.context.getStringAttribute("keyColumn"); String keyStatementId = id + "!selectKey"; keyStatementId = this.builderAssistant.applyCurrentNamespace(keyStatementId, true); Object keyGenerator; if(this.configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = this.configuration.getKeyGenerator(keyStatementId); } else { keyGenerator = this.context.getBooleanAttribute("useGeneratedKeys", Boolean.valueOf(this.configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))).booleanValue()?new Jdbc3KeyGenerator():new NoKeyGenerator(); } this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); } }一看一大段,都是干啥的呀,其实大眼一扫其实也不吓人,就是在处理一些参数类型,statementId等等。我们把眼光放到
SqlSource sqlSource = langDriver.createSqlSource(this.configuration, this.context, parameterTypeClass);
继续深入,XMLScriptBuilder
private List<SqlNode> parseDynamicTags(XNode node) { ArrayList 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; if(child.getNode().getNodeType() != 4 && child.getNode().getNodeType() != 3) { if(child.getNode().getNodeType() == 1) { nodeName = child.getNode().getNodeName(); XMLScriptBuilder.NodeHandler var8 = (XMLScriptBuilder.NodeHandler)this.nodeHandlers.get(nodeName); if(var8 == null) { throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement."); } var8.handleNode(child, contents); this.isDynamic = true; } } else { nodeName = child.getStringBody(""); TextSqlNode handler = new TextSqlNode(nodeName); if(handler.isDynamic()) { contents.add(handler); this.isDynamic = true; } else { contents.add(new StaticTextSqlNode(nodeName)); } } } return contents; }到此很接近目标了,通过 if(handler.isDynamic()) { 进入到 TextSqlNode 的
public boolean isDynamic() { TextSqlNode.DynamicCheckerTokenParser checker = new TextSqlNode.DynamicCheckerTokenParser(); GenericTokenParser parser = this.createParser(checker); parser.parse(this.text); return checker.isDynamic(); }它的createParser
private GenericTokenParser createParser(TokenHandler handler) { return new GenericTokenParser("${", "}", handler); }原来他首先校验的是${}
然后 parser.parse(this.text); 就到了我门的目的地了。
终于到了最上面的GenericTokenParser 的解析部分了。
可以看到,
1 首先弄了个容器,Stringbuilder用来装替换后的sql。
2 然后是判断sql非空等等。然后将sql转成 char[] src 数组
3 开始循环,拿到 this.openToken 的位置,这里估计会问,this.openToken是什么鬼,通过debug发现这个就是 “${”.也就是如果sql语句里面包含有${} 的话 会进入循环
4 来看看循环体干了什么,
if(start > 0 && src[start - 1] == 92) { 首先判断 this.openToken开始位置
然后看其前以为是不是 "\" 92 对应的char就是反斜杠。显然不进,
int end = text.indexOf(this.closeToken, start);
if(end == -1) { 然后拿到 this.closeToken 的位置 显然也不会进这个,那就只能进另外的else了。
// 将 ${ 前面的字符串放入到builder
builder.append(src, offset, start - offset);
// 记录this.openToken的位置坐标
offset = start + this.openToken.length();
// 拿到this.openToken和this.closeToken中间的字符串
String content = new String(src, offset, end - offset);
// 替换该字符串
builder.append(this.handler.handleToken(content));
// 记录this.closeToken 所在位置
offset = end + this.closeToken.length();
如果还有${}内容,继续循环,
注意上面代码中的this.openToken 可能为${ 也可能为 #{
走完了这步之后如果发现sql中确实有${},就将isDynamic标识为true,这个标识很有用,具体可以看XMLScriptBuilder
public SqlSource parseScriptNode() { List contents = this.parseDynamicTags(this.context); MixedSqlNode rootSqlNode = new MixedSqlNode(contents); Object sqlSource = null; if(this.isDynamic) { sqlSource = new DynamicSqlSource(this.configuration, rootSqlNode); } else { sqlSource = new RawSqlSource(this.configuration, rootSqlNode, this.parameterType); } return (SqlSource)sqlSource; }该标识导致两种结果,如果为true就直接返回了一个DynamicSqlSource实例,它的构造函数仅仅做了简单的事情
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) { this.configuration = configuration; this.rootSqlNode = rootSqlNode; }但是如果sql中没有${}的话就会返回,RawSqlSource的实例看它的构造函数
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) { SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class clazz = parameterType == null?Object.class:parameterType; this.sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap()); }它会调用 SqlSourceBuilder 的parse方法,最后又会进入GenericTokenParser的parse方法 进行#{}的替换工作,具体自己debug吧,不然没完了。
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) { SqlSourceBuilder.ParameterMappingTokenHandler handler = new SqlSourceBuilder.ParameterMappingTokenHandler(this.configuration, parameterType, additionalParameters); GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); String sql = parser.parse(originalSql); return new StaticSqlSource(this.configuration, sql, handler.getParameterMappings()); }上面的过程仅仅是
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resource);的执行过程。
sqlsession的执行sql过程,还要进入到那个parse方法中进行替换操作,
这个时候要分为两种情况,如果构建SqlSessionFactory
的时候,
SqlSource 的实例为DynamicSqlSource 的话,因为sql还是xml中的形态,所以它会做两件事,
第一件就是将${}替换为对应的value值,
第二件事就是将#{}替换为?
也就是 DynamicSqlSource 的
public BoundSql getBoundSql(Object parameterObject) { DynamicContext context = new DynamicContext(this.configuration, parameterObject); this.rootSqlNode.apply(context);//第一次替换${} SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(this.configuration); Class parameterType = parameterObject == null?Object.class:parameterObject.getClass(); SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());// 替换#{}->? BoundSql boundSql = sqlSource.getBoundSql(parameterObject); Iterator i$ = context.getBindings().entrySet().iterator(); while(i$.hasNext()) { Entry entry = (Entry)i$.next(); boundSql.setAdditionalParameter((String)entry.getKey(), entry.getValue()); } return boundSql; }
最后sql的样子就是
select * from t_user where username like '%黄%' and password=?
最后才是statement执行sql,返回查询结果。注意上面的替换后的sql,并不是将${}替换为?,而是替换为传过来的参数值。
本来想简单弄弄算了,结果弄得这么繁琐,都有点mybatis源码解读的意思了。
上面对#{} ${}的区别,在源码层次做了讲解,希望有所帮助,反正我现在是印象深刻了。
-_- -_- -_-
-_- -_- -_- -_-
-_-
相关文章推荐
- nginx服务器详细安装过程(使用yum 和 源码包两种安装方式,并说明其区别)
- nginx服务器详细安装过程(使用yum 和 源码包两种安装方式,并说明其区别)
- C++中参数传递的两种方式:传值与传址及它们的区别
- nginx服务器详细安装过程(使用yum 和 源码包两种安装方式,并说明其区别)
- 报表中两种传空参数方式的区别
- Java两种参数传递方式的区别
- FastDFS的配置、部署与API使用解读——设置FastDFS配置参数的两种方式
- nginx服务器详细安装过程(使用yum 和 源码包两种安装方式,并说明其区别)
- C++中参数传递的两种方式:传值与传址及它们的区别
- nginx服务器详细安装过程(使用yum 和 源码包两种安装方式,并说明其区别)
- nginx服务器详细安装过程(使用yum 和 源码包两种安装方式,并说明其区别)
- nginx服务器详细安装过程(使用yum 和 源码包两种安装方式,并说明其区别)
- nginx服务器详细安装过程(使用yum 和 源码包两种安装方式,并说明其区别)
- mybatis两种接收参数的方式#{args}和${args}的区别小知识
- nginx服务器详细安装过程(使用yum 和 源码包两种安装方式,并说明其区别)
- nginx服务器详细安装过程(使用yum 和 源码包两种安装方式,并说明其区别)
- nginx服务器详细安装过程(使用yum 和 源码包两种安装方式,并说明其区别)
- android 拨打电话 源码 两种方式以及区别
- Runnable和Thread 两种实现方式的区别和联系:
- HttpServletRequest request 获取form参数的两种方式