kotlin中使用myibatis-plus的lambdaQuery的問題

從源碼角度對比 java 中 myibatis plus 的 lambdaQuery 的使用來看在 kotlin 使用中的問題

java 中的使用方法

正確使用方法:

LambdaQueryWrapper<PoemsAuthor> queryWrapper = Wrappers.<PoemsAuthor>lambdaQuery();          SFunction<PoemsAuthor, String> function = PoemsAuthor::getName;          queryWrapper.eq(function,"蘇軾");          PoemsAuthor poemsAuthor = poemsAuthorService.getOne(queryWrapper);

錯誤使用方法:

LambdaQueryWrapper<PoemsAuthor> queryWrapper = Wrappers.<PoemsAuthor>lambdaQuery();  //@Note 這裡如果是新建一個對象而不是使用lambda的寫法,在myibatis-plus的內部com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda.resolve中無法識別,將會報錯  SFunction<PoemsAuthor, String> sFunction = new SFunction<PoemsAuthor, String>() {      @Override      public String apply(PoemsAuthor poemsAuthor) {          return poemsAuthor.getName();      }  };  queryWrapper.eq(sFunction,"蘇軾");  PoemsAuthor poemsAuthor = poemsAuthorService.getOne(queryWrapper);

這裡在使用時會在調用 eq 方法時報錯,錯誤信息為"該方法僅能傳入 lambda 表達式產生的合成類"

源碼分析如下:

第一步調用 com.baomidou.mybatisplus.core.conditions.interfaces.Compare#eq(R, java.lang.Object)方法:

/**       * ignore       */      default Children eq(R column, Object val) {          return eq(true, column, val);      }

之所以能夠這樣調用是因為 wrapper 類的繼承關係,詳細繼承關係如下圖:

我們再來看看 LambdaQueryWrapper 的父類 AbstractLambdaWrapper 類的簽名如下:

public abstract class AbstractLambdaWrapper<T, Children extends AbstractLambdaWrapper<T, Children>>      extends AbstractWrapper<T, SFunction<T, ?>, Children>

我們再看一下 AbstractLambdaWrapper 的父類 AbstractWrapper 的簽名:

public abstract class AbstractWrapper<T, R, Children extends AbstractWrapper<T, R, Children>> extends Wrapper<T>      implements Compare<Children, R>, Nested<Children, Children>, Join<Children>, Func<Children, R>

可以看到泛型 R 對應的類型為 SFunction,而這個 SFunction 是個什麼東東呢,我們來看一看:

@FunctionalInterface  public interface SFunction<T, R> extends Serializable {        /**       * Applies this function to the given argument.       *       * @param t the function argument       * @return the function result       */      R apply(T t);  }

看到這個我們應該會想到 java8 的 lambda 中的方法引用,比如:

Function<CharSequence, Boolean> isBlank = StringUtils::isBlank;

它也可以寫成:

SFunction<CharSequence, Boolean> isBlank1 = StringUtils::isBlank;

閑話少說,我們接着看 eq 方法的處理流程,接下來會調用 com.baomidou.mybatisplus.extension.service.additional.AbstractChainWrapper#eq 方法:

@Override      public Children eq(boolean condition, R column, Object val) {          getWrapper().eq(condition, column, val);          return typedThis;      }

然後調用到 com.baomidou.mybatisplus.core.conditions.AbstractWrapper#eq 方法:

@Override      public Children eq(boolean condition, R column, Object val) {          return addCondition(condition, column, EQ, val);      }

進入 addCondition 方法:

/**       * 普通查詢條件       *       * @param condition  是否執行       * @param column     屬性       * @param sqlKeyword SQL 關鍵詞       * @param val        條件值       */      protected Children addCondition(boolean condition, R column, SqlKeyword sqlKeyword, Object val) {          return doIt(condition, () -> columnToString(column), sqlKeyword, () -> formatSql("{0}", val));      }

它調用的 columnToString 方法為 com.baomidou.mybatisplus.core.conditions.AbstractLambdaWrapper#columnToString(com.baomidou.mybatisplus.core.toolkit.support.SFunction

@Override      protected String columnToString(SFunction<T, ?> column) {          return columnToString(column, true);      }

進入重載方法的代碼為:

protected String columnToString(SFunction<T, ?> column, boolean onlyColumn) {          return getColumn(LambdaUtils.resolve(column), onlyColumn);      }

這裡分為兩步,先通過 LambdaUtils 的 resolve 方法校驗傳入的 lambda 表達式(也就是一個 Function),如果校驗失敗就會報錯,校驗成功則返回 SerializedLambda 對象。com.baomidou.mybatisplus.core.toolkit.LambdaUtils#resolve 的代碼為:

/**       * 解析 lambda 表達式       *       * @param func 需要解析的 lambda 對象       * @param <T>  類型,被調用的 Function 對象的目標類型       * @return 返回解析後的結果       */      public static <T> SerializedLambda resolve(SFunction<T, ?> func) {          Class clazz = func.getClass();          return Optional.ofNullable(FUNC_CACHE.get(clazz))              .map(WeakReference::get)              .orElseGet(() -> {                  SerializedLambda lambda = SerializedLambda.resolve(func);                  FUNC_CACHE.put(clazz, new WeakReference<>(lambda));                  return lambda;              });      }
  • 如果是正確的 lambda 表示式,這裡獲取到的 clazz 是形如 class com.ambition.poetry.JavaTest$$Lambda$647/1708990865
  • 如果是傳入 SFunction 對象這裡獲取到的 clazz 是形如 com.ambition.poetry.JavaTest$1

下面會調用 SerializedLambda.resolve 方法,負責將一個支持序列的 Function 序列化為 SerializedLambda,它的代碼為:

/**       * 通過反序列化轉換 lambda 表達式,該方法只能序列化 lambda 表達式,不能序列化接口實現或者正常非 lambda 寫法的對象       *       * @param lambda lambda對象       * @return 返回解析後的 SerializedLambda       */      public static SerializedLambda resolve(SFunction lambda) {          if (!lambda.getClass().isSynthetic()) {              throw ExceptionUtils.mpe("該方法僅能傳入 lambda 表達式產生的合成類");          }          try (ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(SerializationUtils.serialize(lambda))) {              @Override              protected Class<?> resolveClass(ObjectStreamClass objectStreamClass) throws IOException, ClassNotFoundException {                  Class<?> clazz = super.resolveClass(objectStreamClass);                  return clazz == java.lang.invoke.SerializedLambda.class ? SerializedLambda.class : clazz;              }          }) {              return (SerializedLambda) objIn.readObject();          } catch (ClassNotFoundException | IOException e) {              throw ExceptionUtils.mpe("This is impossible to happen", e);          }      }

會先通過 isSynthetic 對傳入的 lambda 表達式的合法性進行校驗,如果不合法就會拋出異常和錯誤信息"該方法僅能傳入 lambda 表達式產生的合成類" 一如文首所提到的。我們看下校驗方法 isSynthetic 代碼:

public boolean isSynthetic() {          return (getModifiers() & SYNTHETIC) != 0;      }
  • SYNTHETIC 的值為 4096
  • 正確的 lambda 傳入時 getModifiers()取到的值為 4112,最後解析返回的 SerializedLambda 對象格式如下:
  • 非 lambda 傳入時 getModifiers()取到的值為 0,檢驗將無法通過,拋出異常。

處理完成後將解析獲取到的 SerializedLambda 對象傳入 com.baomidou.mybatisplus.core.conditions.AbstractLambdaWrapper#getColumn 方法:

private String getColumn(SerializedLambda lambda, boolean onlyColumn) {          //通過get方法獲取屬性名          String fieldName = StringUtils.resolveFieldName(lambda.getImplMethodName());          if (!initColumnMap || !columnMap.containsKey(fieldName.toUpperCase(Locale.ENGLISH))) {              String entityClassName = lambda.getImplClassName();              //獲取實體屬性與庫中字段的映射關係              columnMap = LambdaUtils.getColumnMap(entityClassName);              Assert.notEmpty(columnMap, "cannot find column's cache for "%s", so you cannot used "%s"!",                  entityClassName, typedThis.getClass());              initColumnMap = true;          }          return Optional.ofNullable(columnMap.get(fieldName.toUpperCase(Locale.ENGLISH)))              .map(onlyColumn ? ColumnCache::getColumn : ColumnCache::getColumnSelect)              .orElseThrow(() -> ExceptionUtils.mpe("your property named "%s" cannot find the corresponding database column name!", fieldName));      }

上面的代碼邏輯大致是先通過傳入的 lambda 對象解析出 field 名稱,然後去實體類與表的列之間的映射中獲取實際的列名。這裡我們可以稍微瞄一眼 resolveFieldName 方法:

/**       * 解析 getMethodName -> propertyName       *       * @param getMethodName 需要解析的       * @return 返回解析後的字段名稱       */      public static String resolveFieldName(String getMethodName) {          if (getMethodName.startsWith("get")) {              getMethodName = getMethodName.substring(3);          } else if (getMethodName.startsWith(IS)) {              getMethodName = getMethodName.substring(2);          }          // 小寫第一個字母          return StringUtils.firstToLowerCase(getMethodName);      }

無非是將傳入的 lambda 表達式中的屬性值提取出來,前提是傳入的是屬性的 get 方法的引用格式的 lambda 表達式。

kotlin 中

方式一:

val function = SFunction<PoemsAuthor, String> { it.name }  val lambdaQuery = Wrappers.lambdaQuery<PoemsAuthor>()  lambdaQuery.eq(function,"杜甫")

或者:

val sFunction = SFunction<PoemsAuthor, String> { poemsAuthor -> poemsAuthor.name }  val lambdaQuery = Wrappers.lambdaQuery<PoemsAuthor>()  lambdaQuery.eq(sFunction,"杜甫")

很明顯,這種方式是通過新建 SFunction 對象來處理的。最終會拋出異常。

方式二: 本來是想嘗試和 java 一樣的寫法,但是編譯無法通過,因為

這裡會將 PoemsAuthor::getName 識別成 KFunction 類型,KFunction 是 kotlin 中的高階函數,與 kotlin 中的 lambda 表達式有着極其密切的關係,其中 KFunction 接受的類型如下:

KFunction

Analogue

ReceiverFunction

KFunction1

(Interface) -> Result

Interface.() -> Result

KFunction2

(Interface, Input) -> Result

Interface.(Input) -> Result

KFunction3

(Interface, In1, In2) -> Result

Interface.(In1, In2) -> Result

KSuspendFunction1

suspend (Interface) -> Result

suspend Interface.() -> Result

KSuspendFunction2

suspend (Interface, Input) -> Result

suspend Interface.(Input) -> Result

在 kotlin 中 Lambdas 表達式是花括號括起來的代碼塊。如果要實現一個 java 的函數式接口,需要類型加上 lambda 的方式,如:

SFunction<PoemsAuthor, String> { poemsAuthor -> poemsAuthor.name }

但是這種方式在 myibatis-plus 中又會識別出不是原生的 java lambda 表達式,從而解析出錯。更多關於 kotlin 的 lambda 的內容參考:https://kotlinlang.org/docs/reference/lambdas.html

解決方法

kotlin 中用 myibatis-plus 進行查詢時不使用 lambdaQuery,改用普通的 Query,問題解決。