天天看點

Mybatis源碼閱讀(二):動态節點解析2.1 —— SqlSource和SqlNode

*************************************優雅的分割線 **********************************

分享一波:程式員賺外快-必看的巅峰幹貨

如果以上内容對你覺得有用,并想擷取更多的賺錢方式和免費的技術教程

請關注微信公衆号:HB荷包

Mybatis源碼閱讀(二):動态節點解析2.1 —— SqlSource和SqlNode

一個能讓你學習技術和賺錢方法的公衆号,持續更新

前言

前面的文章介紹了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語句(帶問号)。

public interface SqlSource {

BoundSql getBoundSql(Object parameterObject);

}

[點選并拖拽以移動]

SqlSource的繼承關系如下圖所示。每個實作類都比較簡單,下面隻做簡單的說明。

DynamicSqlSource用于處理動态語句(帶有動态sql标簽),RawSqlSource用于處理靜态語句(沒有動态sql标簽),二者最終會解析成StaticSqlSource。StaticSqlSource可能會帶有問号。這裡暫時隻将代碼簡單的貼出來,部分内容需要結合後面才可以加注釋(如SqlNode)

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);

}

}

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;

}

}

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片段。這裡也先将主要代碼放出來。

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 StringJoiner sqlBuilder = new StringJoiner(" ");

private int uniqueNumber = 0;

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);

}

public void appendSql(String sql) {

sqlBuilder.add(sql);

}

public String getSql() {

return sqlBuilder.toString().trim();

}

public int getUniqueNumber() {

return uniqueNumber++;

}

}

SqlNode

SqlNode表示Sql節點中的動态Sql。該類(接口)隻有一個apply方法,用于解析動态Sql節點,并調用DynamicContext的appendSql方法去拼接sql語句。

public interface SqlNode {

boolean apply(DynamicContext context);

}

SqlNode實作類很多,如圖所示。光看實作類的名稱,想必大家都可以猜出這些實作類的作用了。下面将對這些實作類一一解釋

StaticTextSqlNode使用text字段記錄非動态Sql節點,apply方法直接将text字段追加到DynamicContext.sqlBuilder;MixedSqlNode中使用contents字段存放子節點的動态sql,apply方法則是周遊contents去調用每個SqlNode的apply方法,代碼都比較簡單就不貼出來了。

TextSqlNode

TextSqlNode表示包含 的 s q l 節 點 , i s D y n a m i c 方 法 用 于 檢 測 s q l 中 是 否 包 含 {}的sql節點,isDynamic方法用于檢測sql中是否包含 的sql節點,isDynamic方法用于檢測sql中是否包含{}占位符。該類的apply方法會使用GenericTokenParser将 占 位 符 解 析 成 實 際 意 義 的 參 數 值 , 因 此 {}占位符解析成實際意義的參數值,是以 占位符解析成實際意義的參數值,是以{}在mybatis中會有注入風險,應當慎用,盡量用于非前端傳遞的參數。這裡比較特殊的場景就是order by。order by後面隻能使用${}占位符,是以前端操作排序列時,務必要做防注入處理。

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) {

// 這裡辨別解析的是 占 位 符 r e t u r n n e w G e n e r i c T o k e n P a r s e r ( " {}占位符 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;
 }

 @Override
 public 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;
 }

 @Override
 public String handleToken(String content) {
     this.isDynamic = true;
     return null;
 }
           

}

}

IfSqlNode

該類表示mybatis中的if标簽。if标簽中使用的其實就是Ognl語句,是以可以有一些很花哨的寫法,如調用參數的equals方法等,這裡不對Ognl表達式做過多的介紹。

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;

}

}

TrimSqlNode

trimSqlNode用于根據解析結果添加或删除字尾活字首。

public class TrimSqlNode implements SqlNode {

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;

}

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 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());
 }

 @Override
 public Map<String, Object> getBindings() {
     return delegate.getBindings();
 }

 @Override
 public void bind(String name, Object value) {
     delegate.bind(name, value);
 }

 @Override
 public int getUniqueNumber() {
     return delegate.getUniqueNumber();
 }

 @Override
 public void appendSql(String sql) {
     sqlBuffer.append(sql);
 }

 @Override
 public 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);
         }
     }
 }
           

}

}

WhereSqlNode&SetSqlNode

WhereSqlNode和SetSqlNode分别表示where節點和set節點。這兩個類繼承了TrimSqlNode,是以自帶處理前字尾的功能。

WhereSqlNode将and、or兩個關鍵字作為需要删除的字首。當where的第一個條件以這兩個開頭時,會将and或者or删除。而SetSqlNode則會删除字首或者字尾的嘤文逗号。這裡隻貼出WhereSqlNode代碼。

public class WhereSqlNode extends TrimSqlNode {

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);

}

}

ForeachSqlNode

在動态Sql語句中建構in條件時,往往需要周遊一個集合,是以使用foreach标簽。這裡需要着重介紹一下FilteredDynamicContext這個内部類。該類繼承了DynamicContext,用來處理foreach中的#{}占位符。這裡是對其不完全的處理。如#{item}會被處理乘#{__frch_item_index值}這種格式,用來表示周遊中的每一項。

public class ForEachSqlNode implements SqlNode {

public static final String ITEM_PREFIX = “_frch”;

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;
 }

 @Override
 public Map<String, Object> getBindings() {
     return delegate.getBindings();
 }

 @Override
 public void bind(String name, Object value) {
     delegate.bind(name, value);
 }

 @Override
 public void appendSql(String sql) {
     if (!prefixApplied && sql != null && sql.trim().length() > 0) {
         delegate.appendSql(prefix);
         prefixApplied = true;
     }
     delegate.appendSql(sql);
 }

 @Override
 public String getSql() {
     return delegate.getSql();
 }

 @Override
 public int getUniqueNumber() {
     return delegate.getUniqueNumber();
 }
           

}

}

剩餘的如ChooseSqlNode請讀者自行閱讀,代碼也都比較容易了解。

結語

本次文章隻是介紹一下動态sql解析時常用的類和接口,之後的文章對動态sql進行介紹時将不再對這些類進行贅述。

最後說一些閑話。

其實堅持寫部落格是一件很難的事情。七月份入職以來,便開始考慮寫部落格的事,起初不知道從哪寫起,部落格品質并不高。後來慢慢愛上了閱讀源碼這件事。其實mybatis源碼我已經參照某本書讀完了,但是閱讀完之後我并沒有覺得有何收獲和見解,對源碼的了解也比較淺顯,是以便想着通過撰寫部落格的方式去加深對源碼的認知。Mybatis插件機制是很重要的特性,而想編寫一個好的插件就需要對源碼有深刻的了解,是以源碼不得不讀,對于一個java程式員來說這也是必修課。在這幾篇部落格的撰寫下,我慢慢養成了寫部落格的習慣,也知道什麼該寫,什麼不該寫。部落格中大部分的内容其實都在代碼注釋上,是以顯得部落格内容不多,需要閱讀者仔細閱讀代碼注釋(但願我的部落格有人看吧。)。養成一個習慣不容易,這段時間劃水的過程中對撰寫部落格這件事也有所懈怠(說實話差點都忘了我還開了這麼大一個坑。)

*************************************優雅的分割線 **********************************

分享一波:程式員賺外快-必看的巅峰幹貨

如果以上内容對你覺得有用,并想擷取更多的賺錢方式和免費的技術教程

請關注微信公衆号:HB荷包

Mybatis源碼閱讀(二):動态節點解析2.1 —— SqlSource和SqlNode

一個能讓你學習技術和賺錢方法的公衆号,持續更新