暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

kotlin中使用myibatis-plus的lambdaQuery的问题

开发架构二三事 2019-12-27
3088

从源码角度对比 java 中 myibatis plus 的 lambdaQuery 的使用来看在 kotlin 使用中的问题

java 中的使用方法

正确使用方法:

  1. LambdaQueryWrapper<PoemsAuthor> queryWrapper = Wrappers.<PoemsAuthor>lambdaQuery();

  2. SFunction<PoemsAuthor, String> function = PoemsAuthor::getName;

  3. queryWrapper.eq(function,"苏轼");

  4. PoemsAuthor poemsAuthor = poemsAuthorService.getOne(queryWrapper);

复制

错误使用方法:

  1. LambdaQueryWrapper<PoemsAuthor> queryWrapper = Wrappers.<PoemsAuthor>lambdaQuery();

  2. //@Note 这里如果是新建一个对象而不是使用lambda的写法,在myibatis-plus的内部com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda.resolve中无法识别,将会报错

  3. SFunction<PoemsAuthor, String> sFunction = new SFunction<PoemsAuthor, String>() {

  4. @Override

  5. public String apply(PoemsAuthor poemsAuthor) {

  6. return poemsAuthor.getName();

  7. }

  8. };

  9. queryWrapper.eq(sFunction,"苏轼");

  10. PoemsAuthor poemsAuthor = poemsAuthorService.getOne(queryWrapper);

复制

这里在使用时会在调用 eq 方法时报错,错误信息为"该方法仅能传入 lambda 表达式产生的合成类"

源码分析如下:

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

  1. /**

  2. * ignore

  3. */

  4. default Children eq(R column, Object val) {

  5. return eq(true, column, val);

  6. }

复制

之所以能够这样调用是因为 wrapper 类的继承关系,详细继承关系如下图:

我们再来看看 LambdaQueryWrapper 的父类 AbstractLambdaWrapper 类的签名如下:

  1. public abstract class AbstractLambdaWrapper<T, Children extends AbstractLambdaWrapper<T, Children>>

  2. extends AbstractWrapper<T, SFunction<T, ?>, Children>

复制

我们再看一下 AbstractLambdaWrapper 的父类 AbstractWrapper 的签名:

  1. public abstract class AbstractWrapper<T, R, Children extends AbstractWrapper<T, R, Children>> extends Wrapper<T>

  2. implements Compare<Children, R>, Nested<Children, Children>, Join<Children>, Func<Children, R>

复制

可以看到泛型 R 对应的类型为 SFunction,而这个 SFunction 是个什么东东呢,我们来看一看:

  1. @FunctionalInterface

  2. public interface SFunction<T, R> extends Serializable {

  3. /**

  4. * Applies this function to the given argument.

  5. *

  6. * @param t the function argument

  7. * @return the function result

  8. */

  9. R apply(T t);

  10. }

复制

看到这个我们应该会想到 java8 的 lambda 中的方法引用,比如:

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

复制

它也可以写成:

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

复制

闲话少说,我们接着看 eq 方法的处理流程,接下来会调用 com.baomidou.mybatisplus.extension.service.additional.AbstractChainWrapper#eq 方法:

  1. @Override

  2. public Children eq(boolean condition, R column, Object val) {

  3. getWrapper().eq(condition, column, val);

  4. return typedThis;

  5. }

复制

然后调用到 com.baomidou.mybatisplus.core.conditions.AbstractWrapper#eq 方法:

  1. @Override

  2. public Children eq(boolean condition, R column, Object val) {

  3. return addCondition(condition, column, EQ, val);

  4. }

复制

进入 addCondition 方法:

  1. /**

  2. * 普通查询条件

  3. *

  4. * @param condition 是否执行

  5. * @param column 属性

  6. * @param sqlKeyword SQL 关键词

  7. * @param val 条件值

  8. */

  9. protected Children addCondition(boolean condition, R column, SqlKeyword sqlKeyword, Object val) {

  10. return doIt(condition, () -> columnToString(column), sqlKeyword, () -> formatSql("{0}", val));

  11. }

复制

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

  1. @Override

  2. protected String columnToString(SFunction<T, ?> column) {

  3. return columnToString(column, true);

  4. }

复制

进入重载方法的代码为:

  1. protected String columnToString(SFunction<T, ?> column, boolean onlyColumn) {

  2. return getColumn(LambdaUtils.resolve(column), onlyColumn);

  3. }

复制

这里分为两步,先通过 LambdaUtils 的 resolve 方法校验传入的 lambda 表达式(也就是一个 Function),如果校验失败就会报错,校验成功则返回 SerializedLambda 对象。com.baomidou.mybatisplus.core.toolkit.LambdaUtils#resolve 的代码为:

  1. /**

  2. * 解析 lambda 表达式

  3. *

  4. * @param func 需要解析的 lambda 对象

  5. * @param <T> 类型,被调用的 Function 对象的目标类型

  6. * @return 返回解析后的结果

  7. */

  8. public static <T> SerializedLambda resolve(SFunction<T, ?> func) {

  9. Class clazz = func.getClass();

  10. return Optional.ofNullable(FUNC_CACHE.get(clazz))

  11. .map(WeakReference::get)

  12. .orElseGet(() -> {

  13. SerializedLambda lambda = SerializedLambda.resolve(func);

  14. FUNC_CACHE.put(clazz, new WeakReference<>(lambda));

  15. return lambda;

  16. });

  17. }

复制
  • 如果是正确的 lambda 表示式,这里获取到的 clazz 是形如 class com.ambition.poetry.JavaTest\$$Lambda$647/1708990865

  • 如果是传入 SFunction 对象这里获取到的 clazz 是形如 com.ambition.poetry.JavaTest\$1

下面会调用 SerializedLambda.resolve 方法,负责将一个支持序列的 Function 序列化为 SerializedLambda,它的代码为:

  1. /**

  2. * 通过反序列化转换 lambda 表达式,该方法只能序列化 lambda 表达式,不能序列化接口实现或者正常非 lambda 写法的对象

  3. *

  4. * @param lambda lambda对象

  5. * @return 返回解析后的 SerializedLambda

  6. */

  7. public static SerializedLambda resolve(SFunction lambda) {

  8. if (!lambda.getClass().isSynthetic()) {

  9. throw ExceptionUtils.mpe("该方法仅能传入 lambda 表达式产生的合成类");

  10. }

  11. try (ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(SerializationUtils.serialize(lambda))) {

  12. @Override

  13. protected Class<?> resolveClass(ObjectStreamClass objectStreamClass) throws IOException, ClassNotFoundException {

  14. Class<?> clazz = super.resolveClass(objectStreamClass);

  15. return clazz == java.lang.invoke.SerializedLambda.class ? SerializedLambda.class : clazz;

  16. }

  17. }) {

  18. return (SerializedLambda) objIn.readObject();

  19. } catch (ClassNotFoundException | IOException e) {

  20. throw ExceptionUtils.mpe("This is impossible to happen", e);

  21. }

  22. }

复制

会先通过 isSynthetic 对传入的 lambda 表达式的合法性进行校验,如果不合法就会抛出异常和错误信息"该方法仅能传入 lambda 表达式产生的合成类" 一如文首所提到的。我们看下校验方法 isSynthetic 代码:

  1. public boolean isSynthetic() {

  2. return (getModifiers() & SYNTHETIC) != 0;

  3. }

复制
  • SYNTHETIC 的值为 4096

  • 正确的 lambda 传入时 getModifiers()取到的值为 4112,最后解析返回的 SerializedLambda 对象格式如下: 

  • 非 lambda 传入时 getModifiers()取到的值为 0,检验将无法通过,抛出异常。

处理完成后将解析获取到的 SerializedLambda 对象传入 com.baomidou.mybatisplus.core.conditions.AbstractLambdaWrapper#getColumn 方法:

  1. private String getColumn(SerializedLambda lambda, boolean onlyColumn) {

  2. //通过get方法获取属性名

  3. String fieldName = StringUtils.resolveFieldName(lambda.getImplMethodName());

  4. if (!initColumnMap || !columnMap.containsKey(fieldName.toUpperCase(Locale.ENGLISH))) {

  5. String entityClassName = lambda.getImplClassName();

  6. //获取实体属性与库中字段的映射关系

  7. columnMap = LambdaUtils.getColumnMap(entityClassName);

  8. Assert.notEmpty(columnMap, "cannot find column's cache for \"%s\", so you cannot used \"%s\"!",

  9. entityClassName, typedThis.getClass());

  10. initColumnMap = true;

  11. }

  12. return Optional.ofNullable(columnMap.get(fieldName.toUpperCase(Locale.ENGLISH)))

  13. .map(onlyColumn ? ColumnCache::getColumn : ColumnCache::getColumnSelect)

  14. .orElseThrow(() -> ExceptionUtils.mpe("your property named \"%s\" cannot find the corresponding database column name!", fieldName));

  15. }

复制

上面的代码逻辑大致是先通过传入的 lambda 对象解析出 field 名称,然后去实体类与表的列之间的映射中获取实际的列名。这里我们可以稍微瞄一眼 resolveFieldName 方法:

  1. /**

  2. * 解析 getMethodName -> propertyName

  3. *

  4. * @param getMethodName 需要解析的

  5. * @return 返回解析后的字段名称

  6. */

  7. public static String resolveFieldName(String getMethodName) {

  8. if (getMethodName.startsWith("get")) {

  9. getMethodName = getMethodName.substring(3);

  10. } else if (getMethodName.startsWith(IS)) {

  11. getMethodName = getMethodName.substring(2);

  12. }

  13. // 小写第一个字母

  14. return StringUtils.firstToLowerCase(getMethodName);

  15. }

复制

无非是将传入的 lambda 表达式中的属性值提取出来,前提是传入的是属性的 get 方法的引用格式的 lambda 表达式。

kotlin 中

方式一:

  1. val function = SFunction<PoemsAuthor, String> { it.name }

  2. val lambdaQuery = Wrappers.lambdaQuery<PoemsAuthor>()

  3. lambdaQuery.eq(function,"杜甫")

复制

或者:

  1. val sFunction = SFunction<PoemsAuthor, String> { poemsAuthor -> poemsAuthor.name }

  2. val lambdaQuery = Wrappers.lambdaQuery<PoemsAuthor>()

  3. lambdaQuery.eq(sFunction,"杜甫")

复制

很明显,这种方式是通过新建 SFunction 对象来处理的。最终会抛出异常。

方式二: 本来是想尝试和 java 一样的写法,但是编译无法通过,因为

这里会将 PoemsAuthor::getName 识别成 KFunction 类型,KFunction 是 kotlin 中的高阶函数,与 kotlin 中的 lambda 表达式有着极其密切的关系,其中 KFunction 接受的类型如下:

KFunctionAnalogueReceiverFunction
KFunction1(Interface) -> ResultInterface.() -> Result
KFunction2(Interface, Input) -> ResultInterface.(Input) -> Result
KFunction3(Interface, In1, In2) -> ResultInterface.(In1, In2) -> Result
KSuspendFunction1suspend (Interface) -> Resultsuspend Interface.() -> Result
KSuspendFunction2suspend (Interface, Input) -> Resultsuspend Interface.(Input) -> Result

在 kotlin 中 Lambdas 表达式是花括号括起来的代码块。如果要实现一个 java 的函数式接口,需要类型加上 lambda 的方式,如:

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

复制

但是这种方式在 myibatis-plus 中又会识别出不是原生的 java lambda 表达式,从而解析出错。更多关于 kotlin 的 lambda 的内容参考:https://kotlinlang.org/docs/reference/lambdas.html

解决方法

kotlin 中用 myibatis-plus 进行查询时不使用 lambdaQuery,改用普通的 Query,问题解决。


文章转载自开发架构二三事,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论