mybatis源码(八) Mybatis中的#{} 和${} 占位符的区别
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();
}

浙公网安备 33010602011771号