*************************************優雅的分割線 **********************************
分享一波:程序員賺外快-必看的巔峰干貨
如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程
請關注微信公眾號:HB荷包
一個能讓你學習技術和賺錢方法的公眾號,持續更新
前言
前面的文章介紹了mybatis核心配置文件和mapper文件的解析,之后因為加班比較重,加上個人也比較懶,一拖就是將近半個月,今天抽空開始第二部分的閱讀。
由前面的文章可知,mapper文件中定義的Sql節點會被解析成MappedStatement,其中的SQL語句會被解析成SqlSource。而Sql語句中定義的動態sql節點(如if節點、foreach節點)會被解析成SqlNode。SqlNode節點的解析中會使用到Ognl表達式(沒錯就是是struts2用的那玩意。本以為隨著struts2和jsp淡出開發環境,這種動態標簽也會隨之過時,沒想到mybatis里依然沿用了ognl),這個內容介紹起來有點麻煩,因此感興趣的讀者請自行了解一下。
SqlSource
Sql節點中的Sql語句會被解析成SqlSource,SqlSource接口中只定義了一個方法 getBoundSql 。該方法用于表示解析后的Sql語句(帶問號)。
/**
-
該接口用于標識映射文件或者注解中定義的sql語句
-
這里的sql可能帶有#{}等標志
-
@author Clinton Begin
*/
public interface SqlSource {/**
- 可執行的sql
- @param parameterObject
- @return
*/
BoundSql getBoundSql(Object parameterObject);
}
[點擊并拖拽以移動]
SqlSource的繼承關系如下圖所示。每個實現類都比較簡單,下面只做簡單的說明。
DynamicSqlSource用于處理動態語句(帶有動態sql標簽),RawSqlSource用于處理靜態語句(沒有動態sql標簽),二者最終會解析成StaticSqlSource。StaticSqlSource可能會帶有問號。這里暫時只將代碼簡單的貼出來,部分內容需要結合后面才可以加注釋(如SqlNode)
/**
-
處理靜態sql語句
-
@since 3.2.0
-
@author Eduardo Macarron
*/
public class RawSqlSource implements SqlSource {private final SqlSource sqlSource;
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
rootSqlNode.apply(context);
return context.getSql();
}@Override
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
}
/**
-
負責解析動態sql語句
-
包含#{}占位符
-
@author Clinton Begin
*/
public class DynamicSqlSource implements SqlSource {private final Configuration configuration;
private final SqlNode rootSqlNode;public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}@Override
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, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
/**
-
經過DynamicSqlSource和RawSqlSource處理后
-
這里存放的sql可能含有?占位符
-
@author Clinton Begin
*/
public class StaticSqlSource implements SqlSource {private final String sql;
private final List parameterMappings;
private final Configuration configuration;public StaticSqlSource(Configuration configuration, String sql) {
this(configuration, sql, null);
}public StaticSqlSource(Configuration configuration, String sql, List parameterMappings) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.configuration = configuration;
}@Override
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}
ProviderSqlSource暫時不貼出來(還沒讀到這里)
DynamicContext
DynamicContext用于記錄解析動態Sql時產生的Sql片段。這里也先將主要代碼放出來。
/**
-
用于記錄解析動態SQL語句之后產生的SQL語句片段
-
可以認為它是一個用于記錄動態SQL語句解析生產的容器
-
@author Clinton Begin
*/
public class DynamicContext {public static final String PARAMETER_OBJECT_KEY = “_parameter”;
public static final String DATABASE_ID_KEY = “_databaseId”;static {
OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}/**
- 參數上下文
/
private final ContextMap bindings;
/* - 在SQL弄得解析動態SQL時,會將解析后的SQL語句片段添加到該屬性總保存
- 最終拼湊出一條完整的SQL
*/
private final StringJoiner sqlBuilder = new StringJoiner(" ");
private int uniqueNumber = 0;
/**
- 構造中初始化bindings集合
- @param configuration
- @param parameterObject 運行時用戶傳入的參數。
*/
public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
// 非Map就去找對應的類型處理器
MetaObject metaObject = configuration.newMetaObject(parameterObject);
boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
bindings = new ContextMap(metaObject, existsTypeHandler);
} else {
bindings = new ContextMap(null, false);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
public Map<String, Object> getBindings() {
return bindings;
}public void bind(String name, Object value) {
bindings.put(name, value);
}/**
- 追加SQL片段
- @param sql
*/
public void appendSql(String sql) {
sqlBuilder.add(sql);
}
/**
- 獲取解析后的SQL語句
- @return
*/
public String getSql() {
return sqlBuilder.toString().trim();
}
public int getUniqueNumber() {
return uniqueNumber++;
} - 參數上下文
}
SqlNode
SqlNode表示Sql節點中的動態Sql。該類(接口)只有一個apply方法,用于解析動態Sql節點,并調用DynamicContext的appendSql方法去拼接sql語句。
/**
-
@author Clinton Begin
*/
public interface SqlNode {/**
- 根據用戶傳入的實參去解析動態SQL節點
- 并調用DynamicContext.appendSql將解析后的SQL片段
- 追加到DynamicContext.sqlBuilder保存
- @param context
- @return
*/
boolean apply(DynamicContext context);
}
SqlNode實現類很多,如圖所示。光看實現類的名稱,想必大家都可以猜出這些實現類的作用了。下面將對這些實現類一一解釋
StaticTextSqlNode使用text字段記錄非動態Sql節點,apply方法直接將text字段追加到DynamicContext.sqlBuilder;MixedSqlNode中使用contents字段存放子節點的動態sql,apply方法則是遍歷contents去調用每個SqlNode的apply方法,代碼都比較簡單就不貼出來了。
TextSqlNode
TextSqlNode表示包含的sql節點,isDynamic方法用于檢測sql中是否包含{}的sql節點,isDynamic方法用于檢測sql中是否包含的sql節點,isDynamic方法用于檢測sql中是否包含{}占位符。該類的apply方法會使用GenericTokenParser將占位符解析成實際意義的參數值,因此{}占位符解析成實際意義的參數值,因此占位符解析成實際意義的參數值,因此{}在mybatis中會有注入風險,應當慎用,盡量用于非前端傳遞的參數。這里比較特殊的場景就是order by。order by后面只能使用${}占位符,因此前端操作排序列時,務必要做防注入處理。
/**
-
包含${}的sql
-
@author Clinton Begin
*/
public class TextSqlNode implements SqlNode {
private final String text;
private final Pattern injectionFilter;public TextSqlNode(String text) {
this(text, null);
}public TextSqlNode(String text, Pattern injectionFilter) {
this.text = text;
this.injectionFilter = injectionFilter;
}public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}@Override
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}private GenericTokenParser createParser(TokenHandler handler) {
// 這里標識解析的是占位符returnnewGenericTokenParser("{}占位符 return new GenericTokenParser("占位符returnnewGenericTokenParser("{", “}”, handler);
}private static class BindingTokenParser implements TokenHandler {
private DynamicContext context;private Pattern injectionFilter;public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {this.context = context;this.injectionFilter = injectionFilter;}@Overridepublic String handleToken(String content) {// 獲取用戶提供的實參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解析content的值Object value = OgnlCache.getValue(content, context.getBindings());String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"checkInjection(srtValue);return srtValue;}private void checkInjection(String value) {if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());}}
}
private static class DynamicCheckerTokenParser implements TokenHandler {
private boolean isDynamic;public DynamicCheckerTokenParser() {// Prevent Synthetic Access}public boolean isDynamic() {return isDynamic;}@Overridepublic String handleToken(String content) {this.isDynamic = true;return null;}
}
}
IfSqlNode
該類表示mybatis中的if標簽。if標簽中使用的其實就是Ognl語句,因此可以有一些很花哨的寫法,如調用參數的equals方法等,這里不對Ognl表達式做過多的介紹。
/**
-
if節點
-
@author Clinton Begin
/
public class IfSqlNode implements SqlNode {
/*- if節點的test表達式值
/
private final ExpressionEvaluator evaluator;
/* - if節點的test表達式
/
private final String test;
/* - if節點的子節點
*/
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}@Override
public boolean apply(DynamicContext context) {
// 檢測表達式是否為true,來決定是否執行apply方法
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
} - if節點的test表達式值
}
TrimSqlNode
trimSqlNode用于根據解析結果添加或刪除后綴活前綴。
/**
-
根據解析結果添加或刪除后綴或前綴
-
@author Clinton Begin
*/
public class TrimSqlNode implements SqlNode {/**
- trim節點的子節點
/
private final SqlNode contents;
/* - 前綴
/
private final String prefix;
/* - 后綴
/
private final String suffix;
/* - 如果trim節點包裹的SQL是空語句,刪除指定的前綴,如where
/
private final List prefixesToOverride;
/* - 如果trim節點包裹的SQL是空語句,刪除指定的后綴,如逗號
*/
private final List suffixesToOverride;
private final Configuration configuration;
public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
}protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List prefixesToOverride, String suffix, List suffixesToOverride) {
this.contents = contents;
this.prefix = prefix;
this.prefixesToOverride = prefixesToOverride;
this.suffix = suffix;
this.suffixesToOverride = suffixesToOverride;
this.configuration = configuration;
}@Override
public boolean apply(DynamicContext context) {
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
boolean result = contents.apply(filteredDynamicContext);
// 處理前綴和后綴
filteredDynamicContext.applyAll();
return result;
}/**
- 對prefixOverrides和suffixOverride屬性解析
- 并初始化兩個Override集合
- @param overrides
- @return
*/
private static List parseOverrides(String overrides) {
if (overrides != null) {
// 使用|分隔
final StringTokenizer parser = new StringTokenizer(overrides, “|”, false);
final List list = new ArrayList<>(parser.countTokens());
while (parser.hasMoreTokens()) {
list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
}
return list;
}
return Collections.emptyList();
}
private class FilteredDynamicContext extends DynamicContext {
/**
* 上下文對象
/
private DynamicContext delegate;
/*
* 標識已經處理過的前綴和后綴
/
private boolean prefixApplied;
private boolean suffixApplied;
/*
* 記錄子節點解析后的結果
*/
private StringBuilder sqlBuffer;public FilteredDynamicContext(DynamicContext delegate) {super(configuration, null);this.delegate = delegate;this.prefixApplied = false;this.suffixApplied = false;this.sqlBuffer = new StringBuilder();}public void applyAll() {sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);if (trimmedUppercaseSql.length() > 0) {applyPrefix(sqlBuffer, trimmedUppercaseSql);applySuffix(sqlBuffer, trimmedUppercaseSql);}delegate.appendSql(sqlBuffer.toString());}@Overridepublic Map<String, Object> getBindings() {return delegate.getBindings();}@Overridepublic void bind(String name, Object value) {delegate.bind(name, value);}@Overridepublic int getUniqueNumber() {return delegate.getUniqueNumber();}@Overridepublic void appendSql(String sql) {sqlBuffer.append(sql);}@Overridepublic String getSql() {return delegate.getSql();}/*** 處理前綴** @param sql sql* @param trimmedUppercaseSql 小寫sql*/private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {if (!prefixApplied) {prefixApplied = true;if (prefixesToOverride != null) {for (String toRemove : prefixesToOverride) {// 遍歷prefixesToOverride,如果以其中的某項開頭就從SQL語句開頭剔除if (trimmedUppercaseSql.startsWith(toRemove)) {sql.delete(0, toRemove.trim().length());break;}}}if (prefix != null) {sql.insert(0, " ");sql.insert(0, prefix);}}}/*** 處理后綴。* @param sql* @param trimmedUppercaseSql*/private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {if (!suffixApplied) {suffixApplied = true;if (suffixesToOverride != null) {for (String toRemove : suffixesToOverride) {if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {int start = sql.length() - toRemove.trim().length();int end = sql.length();sql.delete(start, end);break;}}}if (suffix != null) {sql.append(" ");sql.append(suffix);}}}
}
- trim節點的子節點
}
WhereSqlNode&SetSqlNode
WhereSqlNode和SetSqlNode分別表示where節點和set節點。這兩個類繼承了TrimSqlNode,因此自帶處理前后綴的功能。
WhereSqlNode將and、or兩個關鍵字作為需要刪除的前綴。當where的第一個條件以這兩個開頭時,會將and或者or刪除。而SetSqlNode則會刪除前綴或者后綴的嚶文逗號。這里只貼出WhereSqlNode代碼。
/**
-
where節點。繼承了TrimSqlNode
-
因此where節點自帶處理前綴后綴功能
-
@author Clinton Begin
*/
public class WhereSqlNode extends TrimSqlNode {/**
- 設置前綴是OR和AND,因此解析后的SQL如果以這倆開頭就會刪掉前綴
*/
private static List prefixList = Arrays.asList("AND ", "OR ", “AND\n”, “OR\n”, “AND\r”, “OR\r”, “AND\t”, “OR\t”);
public WhereSqlNode(Configuration configuration, SqlNode contents) {
super(configuration, contents, “WHERE”, prefixList, null, null);
} - 設置前綴是OR和AND,因此解析后的SQL如果以這倆開頭就會刪掉前綴
}
ForeachSqlNode
在動態Sql語句中構建in條件時,往往需要遍歷一個集合,因此使用foreach標簽。這里需要著重介紹一下FilteredDynamicContext這個內部類。該類繼承了DynamicContext,用來處理foreach中的#{}占位符。這里是對其不完全的處理。如#{item}會被處理乘#{__frch_item_index值}這種格式,用來表示遍歷中的每一項。
/**
-
forEach節點
-
@author Clinton Begin
*/
public class ForEachSqlNode implements SqlNode {
public static final String ITEM_PREFIX = “_frch”;/**
- 判斷循環終止的條件
/
private final ExpressionEvaluator evaluator;
/* - 迭代的集合表達式
/
private final String collectionExpression;
/* - 該節點下的節點
/
private final SqlNode contents;
/* - 循環前以什么開頭
/
private final String open;
/* - 循環后以什么結束
/
private final String close;
/* - 循環過程中的分隔符
/
private final String separator;
/* - 每次循環的變量名
/
private final String item;
/* - 當前迭代次數
*/
private final String index;
private final Configuration configuration;
public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
this.evaluator = new ExpressionEvaluator();
this.collectionExpression = collectionExpression;
this.contents = contents;
this.open = open;
this.close = close;
this.separator = separator;
this.index = index;
this.item = item;
this.configuration = configuration;
}@Override
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
// 循環之前添加open指定的字符串
applyOpen(context);
int i = 0;
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first || separator == null) {
// 是第一個循環,并且沒有間隔符
context = new PrefixedContext(context, “”);
} else {
context = new PrefixedContext(context, separator);
}
int uniqueNumber = context.getUniqueNumber();
// 將index和item添加到DynamicContext.bindings集合
if (o instanceof Map.Entry) {
@SuppressWarnings(“unchecked”)
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}
// 調用子節點的apply急需處理
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
// 拼接close
applyClose(context);
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}private void applyIndex(DynamicContext context, Object o, int i) {
if (index != null) {
context.bind(index, o);
context.bind(itemizeItem(index, i), o);
}
}private void applyItem(DynamicContext context, Object o, int i) {
if (item != null) {
context.bind(item, o);
context.bind(itemizeItem(item, i), o);
}
}private void applyOpen(DynamicContext context) {
if (open != null) {
context.appendSql(open);
}
}private void applyClose(DynamicContext context) {
if (close != null) {
context.appendSql(close);
}
}private static String itemizeItem(String item, int i) {
return ITEM_PREFIX + item + “_” + i;
}/**
-
處理#{}(不完全處理)
*/
private static class FilteredDynamicContext extends DynamicContext {
private final DynamicContext delegate;
private final int index;
private final String itemIndex;
private final String item;public FilteredDynamicContext(Configuration configuration, DynamicContext delegate, String itemIndex, String item, int i) {
super(configuration, null);
this.delegate = delegate;
this.index = i;
this.itemIndex = itemIndex;
this.item = item;
}@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}@Override
public String getSql() {
return delegate.getSql();
}/**
-
這里會將#{item}占位符解析成#{__frch_item_index值}
-
@param sql
/
@Override
public void appendSql(String sql) {
GenericTokenParser parser = new GenericTokenParser("#{", “}”, content -> {
String newContent = content.replaceFirst("^\s" + item + “(?![^.,:\s])”, itemizeItem(item, index));
if (itemIndex != null && newContent.equals(content)) {
newContent = content.replaceFirst("^\s*" + itemIndex + “(?![^.,:\s])”, itemizeItem(itemIndex, index));
}
return “#{” + newContent + “}”;
});delegate.appendSql(parser.parse(sql));
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
} -
}
private class PrefixedContext extends DynamicContext {
private final DynamicContext delegate;
private final String prefix;
private boolean prefixApplied;public PrefixedContext(DynamicContext delegate, String prefix) {super(configuration, null);this.delegate = delegate;this.prefix = prefix;this.prefixApplied = false;}public boolean isPrefixApplied() {return prefixApplied;}@Overridepublic Map<String, Object> getBindings() {return delegate.getBindings();}@Overridepublic void bind(String name, Object value) {delegate.bind(name, value);}@Overridepublic void appendSql(String sql) {if (!prefixApplied && sql != null && sql.trim().length() > 0) {delegate.appendSql(prefix);prefixApplied = true;}delegate.appendSql(sql);}@Overridepublic String getSql() {return delegate.getSql();}@Overridepublic int getUniqueNumber() {return delegate.getUniqueNumber();}
}
- 判斷循環終止的條件
}
剩余的如ChooseSqlNode請讀者自行閱讀,代碼也都比較容易理解。
結語
本次文章只是介紹一下動態sql解析時常用的類和接口,之后的文章對動態sql進行介紹時將不再對這些類進行贅述。
最后說一些閑話。
其實堅持寫博客是一件很難的事情。七月份入職以來,便開始考慮寫博客的事,起初不知道從哪寫起,博客質量并不高。后來慢慢愛上了閱讀源碼這件事。其實mybatis源碼我已經參照某本書讀完了,但是閱讀完之后我并沒有覺得有何收獲和見解,對源碼的理解也比較淺顯,因此便想著通過撰寫博客的方式去加深對源碼的認知。Mybatis插件機制是很重要的特性,而想編寫一個好的插件就需要對源碼有深刻的理解,因此源碼不得不讀,對于一個java程序員來說這也是必修課。在這幾篇博客的撰寫下,我慢慢養成了寫博客的習慣,也知道什么該寫,什么不該寫。博客中大部分的內容其實都在代碼注釋上,因此顯得博客內容不多,需要閱讀者仔細閱讀代碼注釋(但愿我的博客有人看吧。)。養成一個習慣不容易,這段時間劃水的過程中對撰寫博客這件事也有所懈怠(說實話差點都忘了我還開了這么大一個坑。)
*************************************優雅的分割線 **********************************
分享一波:程序員賺外快-必看的巔峰干貨
如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程
請關注微信公眾號:HB荷包
一個能讓你學習技術和賺錢方法的公眾號,持續更新