前面几节介绍了MyBatis动态SQL相关的一些概念,并分析了MyBatis动态SQL配置的解析过程,本节我们就从源码的角度分析一下MyBatis两种常用的参数占位符#{}和${}的区别。
我们首先来看${}参数占位符的解析过程。当动态SQL配置中存在${}参数占位符时,MyBatis会使用TextSqlNode对象描述对应的SQL节点,在调用TextSqlNode对象的apply()方法时会完成动态SQL的解析。也就是说,${}参数占位符的解析是在TextSqlNode类的apply()方法中完成的,下面是该方法的实现:
public boolean apply(DynamicContext context) { // 通过GenericTokenParser对象解析${}参数占位符,使用BindingTokenParser对象处理参数占位符内容 GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); // 调用GenericTokenParser对象的parse()方法解析 context.appendSql(parser.parse(text)); return true; } private GenericTokenParser createParser(TokenHandler handler) { return new GenericTokenParser("${", "}", handler); }
如上面的代码所示,在TextSqlNode类的apply()方法中,首先调用createParser()方法创建一个GenericTokenParser对象,通过GenericTokenParser对象解析${}参数占位符,然后通过BindingTokenParser对象处理参数占位符的内容。
createParser方法返回一个GenericTokenParser对象,指定openToken属性为“${”,closeToken属性为“}”,TokenHandler为BindingTokenParser对象。GenericTokenParser解析参数占位符的过程前面已经介绍过了,这里我们回顾一下:
public String parse(String text) { if (text == null || text.isEmpty()) { return ""; } // 获取第一个openToken在SQL中的位置 int start = text.indexOf(openToken, 0); // start为-1说明SQL中不存在任何参数占位符 if (start == -1) { return text; } // 將SQL转换为char数组 char[] src = text.toCharArray(); // offset用于记录已解析的#{或者}的偏移量,避免重复解析 int offset = 0; final StringBuilder builder = new StringBuilder(); // expression为参数占位符中的内容 StringBuilder expression = null; // 遍历获取所有参数占位符的内容,然后调用TokenHandler的handleToken()方法替换参数占位符 while (start > -1) { 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); while (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); offset = end + closeToken.length(); break; } } if (end == -1) { // close token was not found. builder.append(src, start, src.length - start); offset = src.length; } else { // 调用TokenHandler的handleToken()方法替换参数占位符 builder.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(); }
上面代码的核心内容是遍历获取所有${}参数占位符的内容,然后调用BindingTokenParser对象的handleToken()方法对参数占位符内容进行替换。BindingTokenParser类的handleToken()方法实现如下:
public String handleToken(String content) { // 获取Mybatis内置参数_parameter,_parameter属性中保存所有参数信息 Object parameter = context.getBindings().get("_parameter"); if (parameter == null) { context.getBindings().put("value", null); } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) { // 將参数对象添加到ContextMap对象中 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; }
如上面的代码所示,在BindingTokenParser类的handleToken()方法中,根据参数占位符名称获取对应的参数值,然后替换为对应的参数值。假设我们的SQL配置如下:
<select id="getUserByName" parameterType="java.lang.String" resultType="com.blog4java.mybatis.example.entity.UserEntity"> select * from user where name = ${userName} </select>
如果Mapper调用时传入的参数值如下:
@Test public void testGetUserByName() { // String userName = "'User6'";// 正常 String userName = "User6";// 错误,类型不匹配 UserEntity userEntity = userMapper.getUserByName(userName); System.out.println(userEntity); }
上面的Mapper调用将会抛出异常,原因是TextSqlNode类的apply()方法中解析${}参数占位符时,只是对参数占位符内容进行替换,将参数占位符替换为对应的参数值,因此SQL配置解析后的内容如下:
select * from user where name =User6
#{}参数占位符的解析过程前面已经介绍过了,可参考SqlSourceBuilder类的parse()方法。假设我们有如下SQL配置:
<select id="getUserByName" parameterType="java.lang.String" resultType="com.blog4java.mybatis.example.entity.UserEntity"> select * from user where name = #{userName} </select>
#{}参数占位符的内容将会被替换为“?”。上面的SQL语句解析后的结果如下:
select * from user where name =?
MyBatis将会使用PreparedStatement对象与数据库进行交互,过程大致如下:
Connection connection = DriverManager.getConnection("xxx");PreparedStatement statement = connection.prepareStatement("select * from userwhere name = ?");statement.setString(1,"Test4");statement.execute();
最后我们再来总结一下#{}和${}参数占位符的区别。使用#{}参数占位符时,占位符内容会被替换成“?”,然后通过PreparedStatement对象的setXXX()方法为参数占位符设置值;而${}参数占位符内容会被直接替换为参数值。使用#{}参数占位符能够有效避免SQL注入问题,所以我们可以优先考虑使用#{}占位符,当#{}参数占位符无法满足需求时,才考虑使用${}参数占位符。
发表回复
评论列表(0条)