mybatis源码(八) Mybatis中的#{} 和${} 占位符的区别
使用#{} 参数占位符时,占位符内容会被替换成 “?” 然后通过PreparedStatement 对象的setXxx()方法为参数占位符设置值;能够有效避免SQL注入的问题,所以应优先使用#{},当#{}无法满足时,在考虑用${}
而${} 参数占位符内容会被直接替换为参数值.
1.${} 参数占位符的解析过程是在TextSqlNode类的apply()中完成的
TextSqlNode部分源码如下:
public class TextSqlNode implements SqlNode { private final String text; private final Pattern injectionFilter; @Override 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); } }
这里的GenericTokenParser的parse(text)方法里完成的
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()方法实现如下:
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) { // 获取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; }
2.#{} 的解析过程可参考SqlSourceBuilder.parse()方法
public class SqlSourceBuilder extends BaseBuilder { private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName"; public SqlSourceBuilder(Configuration configuration) { super(configuration); } public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) { // ParameterMappingTokenHandler为Mybatis参数映射处理器,用于处理SQL中的#{}参数占位符 ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); // Token解析器,用于解析#{}参数 GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); // 调用GenericTokenParser对象的parse()方法將#{}参数占位符转换为? String sql = parser.parse(originalSql); return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); }
同样的。这里使用ParameterMappingTokenHandler 处理器解析的。和${} 一样都是使用GenericTokenParser.parse方法进行解析的,只是处理器不一样。
#{} 占位符使用ParameterMappingTokenHandler 的部分源码如下:
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>(); private Class<?> parameterType; private MetaObject metaParameters; public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) { super(configuration); this.parameterType = parameterType; this.metaParameters = configuration.newMetaObject(additionalParameters); } public List<ParameterMapping> getParameterMappings() { return parameterMappings; } @Override public String handleToken(String content) { parameterMappings.add(buildParameterMapping(content)); return "?"; }
从上面的代码可以看出,SQL配置中的所有#{}参数占位符内容都被替换成了"?" 字符,为什么要替换成一一个"?" 字符呢?
因为MyBatis默认情况下会使用PreparedStatement对象与数据库进行交互,因此#{}参数占位符内容被替换成了问号,然后调用PreparedStatement对象的setXXX()方法为参数占位符设置值。
3.#{ } 和${} 的使用案例
3.1 ${} 的使用:假设我们的sql如下:
<select id="getUserByName" parameterType="java.lang.String" resultType="com.blog4java.mybatis.example.entity.User"> select * from user where name = ${userName} </select>
如果mapper调用的时候,传入的参数值如下:
@Test public void testGetUserByName() { String userName = "Test4"; UserEntity userEntity = userMapper.getUserByName(userName); System.out.println(userEntity); }
就会抛出如下异常
org.apache.ibatis.exceptions.PersistenceException: ### Error building SqlSession. ### The error may exist in SQL Mapper Configuration ### Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: org.apache.ibatis.builder.BuilderException: Error registering typeAlias for 'velocityDriver'. Cause: java.lang.ClassNotFoundException: Cannot find class: org.mybatis.scripting.velocity.Driver
上面的Mapper调用将会抛出异常,原因是TextSqlNode类的apply()方法中解析${}参数占位符时,只是对参数占位符内容进行替换,将参数占位符替换为对应的参数值,因此SQL 配置解析后的内容如下:
select * from user where name = Test4
因此,语句不合法,正确的写法应该是,在参数前后,加入一个单引号,如下所示:
@Test public void testGetUserByName() { String userName = "'Test4'"; UserEntity userEntity = userMapper.getUserByName(userName); System.out.println(userEntity); }
3.2 #{} 的使用:假设我们的sql如下:
select * from user where name = #{userName}
#{} 参数占位符会被解析成“?” 上面的Sql语句解析结果为
select * from user where name = ?
Mybatis会使用PreparedStatement对象与数据库进行交互,大致过程如下:
@Test public void test001() throws SQLException { Connection connection = DriverManager.getConnection("xxx"); PreparedStatement statement = connection.prepareStatement("select * from user where name = ? "); statement.setString(1,"Test"); statement.execute(); }