XMLLanguageDriver是ibatis的默認解析sql節點幫助類,其中的方法會調用生成DynamicSqlSource和RawSqlSource這兩個幫助類,本文將對此作下簡單的簡析
應用場景
我們在編寫mybatis的sql語句的時候,經常用到的是#{}的字符去替代其中的查詢入參,偶爾也會在網上看到${}這樣的字符使用。
經過筆者分析源碼得知,其實前者調用的為RawSqlSource幫助類進行生成具體的sql,而后者則是通過DynamicSqlSource幫助類來實現的。
選用邏輯
我們還是回到XMLLanguageDriver解析類,不管是xml方式還是注解方式解析sql,都會用到TextSqlNode這個包裝類,我們可以作下簡單的分析。
這里就以注解方式的解析sql為例
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
//此處兼容XML方式的解析,條件以<script>為頭結點
if (script.startsWith("<script>")) { // issue #3
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
//①解析Configuration#variable變量
script = PropertyParser.parse(script, configuration.getVariables()); // issue #127
TextSqlNode textSqlNode = new TextSqlNode(script);
// ②根據TextSqlNode的內部屬性isDynamic來進行解析幫助類的分配
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
return new RawSqlSource(configuration, script, parameterType);
}
}
}
PropertyParser#parse() ①
廢話不講,直接上源碼
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
// 此處專門查找`${}`關鍵字符,並替換為相應的variable值
return parser.parse(string);
}
附屬其中的VariableTokenHandler內部靜態類-對指定字符進行處理,此處特指${}
private Properties variables;
public VariableTokenHandler(Properties variables) {
this.variables = variables;
}
public String handleToken(String content) {
//如果variables屬性中存在則直接替換,沒有則返回原來的內容
if (variables != null && variables.containsKey(content)) {
return variables.getProperty(content);
}
return "${" + content + "}";
}
VariableTokenHandler的替換原則為查找variables中是否含有
${}
內的key,沒有則返回原來的格式此處插一句,這里的variables是如何配置呢,spring方面主要通過創建SqlsessionFactoryBean時設置configurationProperties屬性即可;
也可通過ibatis主配置文件的properties節點進行配置
TextSqlNode#isDynamic() ②
我們直接看源碼
public boolean isDynamic() {
// 只要找到${}這樣的字符則直接設置其內部屬性isDynamic=true
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
GenericTokenParser parser = createParser(checker);
parser.parse(text);
// 返回是否為${}方式解析
return checker.isDynamic();
}
TextSqlNode看來是用來校驗其中的sql語句是否含有
${}
字符,有則便用DynamicSqlSource
方式解析,反之則用RawSqlSource
方式解析
DynamicSqlSource
筆者此處針對含${}的SQL語句方式進行簡單的分析,而#{}的方式讀者可自行研究。也可查閱此篇簡單了解>>>Mybatis源碼解析-BoundSql
DynamicSqlSource#getBoundSql()
我們關注下其是如何組裝BoundSql對象的,源碼奉上
public BoundSql getBoundSql(Object parameterObject) {
//sql語句解析,優先解析${}字符
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
//創建StaticSqlSource,其也會去解析#{}字符。
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
//
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
此處我們只關注TextSqlNode#apply()方法,看下其是如何解析${}
TextSqlNode#apply()
解析${}字符串
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context));
//將解析得到的sql直接存到context對象中
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
附屬BindingTokenParser解析靜態內部類部分源碼
public String handleToken(String content) {
//此處拿取的相當於為方法調用的入參對象,比如dao接口query(List list),指代的便是list對象
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);
}
//ognl表達式獲取值
Object value = OgnlCache.getValue(content, context.getBindings());
return (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
}
由上面得知,${}主要就是對dao接口的參數的內部屬性的直接調用,其不會像#{}那樣將入參拼裝成?預表達式,而是直接生成表達式執行SQL語句,那么這里就會涉及到sql惡意注入的風險
此處插一句,ognl指的是Object-Graph Navigation Language,其為強大的表達式語言,使用過Struct2/Freemaker等的就會熟悉其中的語法
小結
DynamicSqlSource解析含有${}的sql語句,而RawSqlSource解析含有#{}的sql語句
DynamicSqlSource涵蓋的操作比RawSqlSource多了一步,便是優先處理${}字符,其本身也會調用去解析#{}字符
${}語句的解析是最終轉換為Statement直接執行,其中的參數賦值是直接賦值,不做字符串引號拼裝;而#{}語句的解析是最終轉換為PrepareStatement預表達式來進行sql執行,安全性很高
舉個例子:sql查詢語句為select * from user where name='admin' and pwd=${pwd}
- 如果pwd參數傳入
aaa or 1=1
,則上述拼裝后的結果為select * from user where name='admin' and pwd=aaa or 1=1
。這個表達式會恆為真,直接會繞過驗證,風險賊高- 如果上述采用
#{pwd}
,則傳入aaa or 1=1
,則最后的生成語句為select * from user='admin' and pwd='aaa or 1=1'
。這個表達式驗證通不過,有較高的安全性,防止sql惡意注入
- 優推#{}方式操作入參,不建議使用${}方式