Q:#
与$
的区别是什么?
A:#
会在sql中使用占位符,有效得防止了sql
注入,$
会把参数直接拼接到sql
中可能会引发sql
注入。
如果你只知道这些区别,或者想知道为什么两种写法会产生这些区别,那么你就可以静下来看看下面我写的。
DynamicSqlSource
中有一个getBoundSql
方法,如下:
public BoundSql getBoundSql(Object 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);
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
注意其中的apply
方法,这将是$
被解析的地方。其中rootSqlNode
是通过构造函数传递过来的一般会是一个MixedSqlNode
,看MixedSqlNode
的apply
方法:
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
继续对内部的SqlNode
调用apply
方法,其中文本类型的sql会被解析为TextSqlNode
。下面我们看一下TextSqlNode
的apply
方法:
public boolean apply(DynamicContext context) {
GenericTokenParser parser = new GenericTokenParser("${", "}", new BindingTokenParser(context));
context.appendSql(parser.parse(text));
return true;
}
private static class BindingTokenParser implements TokenHandler {
private DynamicContext context;
public BindingTokenParser(DynamicContext context) {
this.context = context;
}
public String handleToken(String content) {
try {
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());
return (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + content + "'. Cause: " + e, e);
}
}
}
TextSqlNode
中的apply
所有需要的内部类BindingTokenParser
也一并贴出来了。
看到这里我们应该先看看GenericTokenParser
中怎么为我们解析的:
public String parse(String text) {
StringBuilder builder = new StringBuilder();
if (text != null) {
String after = text;
int start = after.indexOf(openToken);
int end = after.indexOf(closeToken);
while (start > -1) {
if (end > start) {
String before = after.substring(0, start);
String content = after.substring(start + openToken.length(), end);
String substitution;
// check if variable has to be skipped
if (start > 0 && text.charAt(start - 1) == '\\') {
before = before.substring(0, before.length() - 1);
substitution = new StringBuilder(openToken).append(content).append(closeToken).toString();
} else {
substitution = handler.handleToken(content);
}
builder.append(before);
builder.append(substitution);
after = after.substring(end + closeToken.length());
} else if (end > -1) {
String before = after.substring(0, end);
builder.append(before);
builder.append(closeToken);
after = after.substring(end + closeToken.length());
} else {
break;
}
start = after.indexOf(openToken);
end = after.indexOf(closeToken);
}
builder.append(after);
}
return builder.toString();
}
不出意料,这个类只是将sql
中被openToken
和closeToken
所包围的token
用TokenHandle
类来解析,那我们就可以继续回到TextSqlNode
中了,可以看到上面的BindingTokenParser
中有一个handleToken
方法这就是产生最开始Q&A区别的地方,这个方法会直接将${}
所包围的token
用传递进来的参数解析出来并返回解析之后的value
,也就是这个BindingTokenParser
会直接将sql
中的${}
部分用参数解析完并拼接回sql
。如果你是一个老手,估计都不会滥用${}
,那让我们回到开始的DynamicSqlSource
中吧,继续看DynamicSqlSource
中下面的代码:
public BoundSql getBoundSql(Object 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);
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
在rootSqlNode.apply(context)
之后,还会有一个SqlSourceBuilder
这个类也有一个parse
方法(在这个地方不得不说下即使Mybatis
现在是最流行的ORM
框架之一但是它的设计上确实不怎么样,现在随便来一个类都有一个parse
方法,为什么不直接抽象出一个接口来),这个parse
方法就是处理#
的地方了。下面我们来看看:
public SqlSource parse(String originalSql, Class> parameterType) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
private List parameterMappings = new ArrayList();
private Class> parameterType;
public ParameterMappingTokenHandler(Configuration configuration, Class> parameterType) {
super(configuration);
this.parameterType = parameterType;
}
public List getParameterMappings() {
return parameterMappings;
}
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
private ParameterMapping buildParameterMapping(String content) {
Map propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");
String jdbcType = propertiesMap.get("jdbcType");
Class> propertyType;
MetaClass metaClass = MetaClass.forClass(parameterType);
if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(jdbcType)) {
propertyType = java.sql.ResultSet.class;
} else if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
if (jdbcType != null) {
builder.jdbcType(resolveJdbcType(jdbcType));
}
Class> javaType = null;
String typeHandlerAlias = null;
for (Map.Entry entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
}
}
if (typeHandlerAlias != null) {
builder.typeHandler((TypeHandler>) resolveTypeHandler(javaType, typeHandlerAlias));
}
return builder.build();
}
老规矩先上代码再分析,上面是SqlSourceBuilder
的apply
方法以及用到的TokenHandle
的内部实现类ParameterMappingTokenHandle
。
先看最简单的产生#
和$
区别的地方,ParameterMappingTokenHandle
的handleToken
方法中不管你传过来的是什么都是直接返回一个占位符?
。
接下来要介绍#
和$
的第二个区别了##:看上面ParameterMappingTokenHandle
类的parseParameterMapping
方法我们可以发现在#{}
中可以写一些其它的东西,比如javaType
、jdbcType
、typeHandler
等,所以我们可以写出类似这种的sql
:#{id,javaType=String,jdbcType=VARCHAR,typeHandler=cn.fay.mybatis.MyStringTypeHandler}
,我们可以在sql
中指定变量的类型以及设置这个变量时对应所需要用到的TypeHandler
当然如果你用到了自定义的TypeHandler
的话,你要在mybatis
的配置中声明一下,如下:
这里需要注意的是mybatis
的配置文件中对typeAliases
、typeHandlers
、plugings
、mappers
等元素的顺序是有要求的不能乱,如果你在使用中遇到了问题解决不了,可以过来问我。
至此,#
和$
的区别应该说得差不多了,有问题可以来沟通。