Q:#
與$
的區(qū)別是什么捡需?
A:#
會(huì)在sql中使用占位符坑傅,有效得防止了sql
注入呜呐,$
會(huì)把參數(shù)直接拼接到sql
中可能會(huì)引發(fā)sql
注入战得。
如果你只知道這些區(qū)別,或者想知道為什么兩種寫法會(huì)產(chǎn)生這些區(qū)別庸推,那么你就可以靜下來看看下面我寫的常侦。
DynamicSqlSource
中有一個(gè)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<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
注意其中的apply
方法贬媒,這將是$
被解析的地方聋亡。其中rootSqlNode
是通過構(gòu)造函數(shù)傳遞過來的一般會(huì)是一個(gè)MixedSqlNode
,看MixedSqlNode
的apply
方法:
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
繼續(xù)對(duì)內(nèi)部的SqlNode
調(diào)用apply
方法际乘,其中文本類型的sql會(huì)被解析為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
所有需要的內(nèi)部類BindingTokenParser
也一并貼出來了。
看到這里我們應(yīng)該先看看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();
}
不出意料,這個(gè)類只是將sql
中被openToken
和closeToken
所包圍的token
用TokenHandle
類來解析罪塔,那我們就可以繼續(xù)回到TextSqlNode
中了投蝉,可以看到上面的BindingTokenParser
中有一個(gè)handleToken
方法這就是產(chǎn)生最開始Q&A區(qū)別的地方,這個(gè)方法會(huì)直接將${}
所包圍的token
用傳遞進(jìn)來的參數(shù)解析出來并返回解析之后的value
征堪,也就是這個(gè)BindingTokenParser
會(huì)直接將sql
中的${}
部分用參數(shù)解析完并拼接回sql
瘩缆。如果你是一個(gè)老手,估計(jì)都不會(huì)濫用${}
佃蚜,那讓我們回到開始的DynamicSqlSource
中吧庸娱,繼續(xù)看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<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
在rootSqlNode.apply(context)
之后,還會(huì)有一個(gè)SqlSourceBuilder
這個(gè)類也有一個(gè)parse
方法(在這個(gè)地方不得不說下即使Mybatis
現(xiàn)在是最流行的ORM
框架之一但是它的設(shè)計(jì)上確實(shí)不怎么樣谐算,現(xiàn)在隨便來一個(gè)類都有一個(gè)parse
方法熟尉,為什么不直接抽象出一個(gè)接口來),這個(gè)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<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
private Class<?> parameterType;
public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType) {
super(configuration);
this.parameterType = parameterType;
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
private ParameterMapping buildParameterMapping(String content) {
Map<String, String> 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<String, String> 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();
}
老規(guī)矩先上代碼再分析斤儿,上面是SqlSourceBuilder
的apply
方法以及用到的TokenHandle
的內(nèi)部實(shí)現(xiàn)類ParameterMappingTokenHandle
。
先看最簡(jiǎn)單的產(chǎn)生#
和$
區(qū)別的地方腮考,ParameterMappingTokenHandle
的handleToken
方法中不管你傳過來的是什么都是直接返回一個(gè)占位符?
雇毫。
接下來要介紹#
和$
的第二個(gè)區(qū)別了##:看上面ParameterMappingTokenHandle
類的parseParameterMapping
方法我們可以發(fā)現(xiàn)在#{}
中可以寫一些其它的東西,比如javaType
踩蔚、jdbcType
棚放、typeHandler
等,所以我們可以寫出類似這種的sql
:#{id,javaType=String,jdbcType=VARCHAR,typeHandler=cn.fay.mybatis.MyStringTypeHandler}
馅闽,我們可以在sql
中指定變量的類型以及設(shè)置這個(gè)變量時(shí)對(duì)應(yīng)所需要用到的TypeHandler
當(dāng)然如果你用到了自定義的TypeHandler
的話飘蚯,你要在mybatis
的配置中聲明一下,如下:
<typeHandlers>
<typeHandler handler="cn.fay.mybatis.MyStringTypeHandler" javaType="String" jdbcType="VARCHAR"/>
</typeHandlers>
這里需要注意的是mybatis
的配置文件中對(duì)typeAliases
福也、typeHandlers
局骤、plugings
、mappers
等元素的順序是有要求的不能亂暴凑,如果你在使用中遇到了問題解決不了峦甩,可以過來問我。
至此现喳,#
和$
的區(qū)別應(yīng)該說得差不多了凯傲,有問題可以來溝通。