MyBatis-Plus中LambdaQueryWrapper的探究

文章介绍了MyBatis-Plus的LambdaQueryWrapper如何利用Java8的Lambda表达式和SerializedLambda接口,实现自动识别并获取Lambda表达式对应的实体类字段,简化查询条件构建。重点讲解了序列化和反序列化在其中的作用及原理。
摘要由CSDN通过智能技术生成

MyBatis-Plus的条件构造器LambdaQueryWrapper是开发中常用的工具,和普通的QueryWrapper不同的是,LambdaQueryWrappe

可以识别Lambda表达式,获取Lambda表达式对应的字段名称,在使用上更方便,此外,当对象的字段发生变更时也更安全。

我们知道,Lambda表达式本质上是一个匿名内部类,实现了Function,重写了apply方法。比如下面这样的:

// Lambda表达式
User::getName
// 匿名内部类
new SFunction<User, String>() {
    @Override
    public String apply(User user) {
        return user.getName();
    }
}

那么LambdaQueryWrapper是怎么获取到Lambda表达式对应的字段名称的呢?

看一下AI的解答:LambdaQueryWrapper之所以能获取到Lambda表达式所对应的字段名称,实际上并没有采用通过实例化对象并逐个字段赋值的方式来比较结果这么复杂的手段。而是巧妙地利用了Java 8中Lambda表达式的序列化机制

在Java 8中,Lambda表达式会被编译为一个实现了java.lang.invoke.SerializedLambda接口的类的实例。这个SerializedLambda实例保存了生成它的类、方法签名以及对应方法的参数索引等信息。MyBatis-Plus正是利用了这一特性,通过反射调用Lambda表达式的writeReplace方法得到SerializedLambda对象,进而从中解析出字段名。

具体来说,MyBatis-Plus内部会将Lambda表达式转换为SerializedLambda对象进行反序列化处理,然后分析SerializedLambda对象中的方法描述符和其他相关信息,从而准确地定位到Lambda表达式中引用的实体类属性字段。这样就无需实际操作实例对象,也能智能地构建SQL查询条件所需的字段名。

从个人角度来思考,我们可以通过反射获取到apply方法,进而知道方法的参数是一个User,拿到User.class,但是似乎就止步于此了,反射对apply方法的内部构造 return user.getName()似乎是无能为力的。

也许可以通过newInstance方法获取User实例,给实例的每一个字段赋不同值,然后调用Lambda表达式得到结果,进行一一对比?先不说如何给字段赋值,简单的String,Integer等基本类型倒是好说,集合类型呢,复杂的对象类型呢?这样也太蠢了,而且也并不是所有的对象都可以简单的实例化的。那么MyBatis-Plus又是怎么实现这样的逻辑的呢?先说结论吧,是通过序列化反序列化的方法获取到的字段名称。
看源码:先从LambdaQueryWrapper中随便挑个带有SFunction参数的方法,一路向下找,在AbstractLambdaWrapper类下面会找到这样一个方法

private String getColumn(SerializedLambda lambda, boolean onlyColumn) {
    Class<?> aClass = lambda.getInstantiatedType();
    this.tryInitCache(aClass);
    String fieldName = PropertyNamer.methodToProperty(lambda.getImplMethodName());
    ColumnCache columnCache = this.getColumnCache(fieldName, aClass);
    return onlyColumn ? columnCache.getColumn() : columnCache.getColumnSelect();
}

可以看到是通过lambda.getImplMethodName方法拿到了一个方法名称,然后通过PropertyNamer.methodToProperty方法得到对应的属性名称。后面的步骤我们都会,implMethodName是lambda对象的一个属性,那么我们的关注点就要放在这个方法的参数SerializedLambda lambda上了。我们往回看,

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

LambdaUtils.resolve(column)方法

public static <T> SerializedLambda resolve(SFunction<T, ?> func) {
    Class<?> clazz = func.getClass();
    String name = clazz.getName();
    return (SerializedLambda)Optional.ofNullable((WeakReference)FUNC_CACHE.get(name)).map(Reference::get).orElseGet(() -> {
        SerializedLambda lambda = SerializedLambda.resolve(func);
        FUNC_CACHE.put(name, new WeakReference(lambda));
        return lambda;
    });
}

这儿主要是一个缓存的作用,关于这个缓存,值得一提的是,虽然MyBatis-Plus这儿用的是clazz.getName,但是实际测试后发现同一个class下同一个位置的Lambda表达式,即使在多线程环境中,也会复用同一个对象。因此即使这儿用func做缓存的key,理论上也是可以的。我们继续往下看,找到SerializedLambda.resolve方法

public static SerializedLambda resolve(SFunction<?, ?> lambda) {
		if (!lambda.getClass().isSynthetic()) {
			throw ExceptionUtils.mpe("该方法仅能传入 lambda 表达式产生的合成类", new Object[0]);
		} else {
			try {
				// 先序列化成byte[],在封装成ObjectInputStream
				ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(SerializationUtils.serialize(lambda))) {
					protected Class<?> resolveClass(ObjectStreamClass objectStreamClass) throws IOException, ClassNotFoundException {
						Class clazz;
						try {
							clazz = ClassUtils.toClassConfident(objectStreamClass.getName());
						} catch (Exception var4) {
							clazz = super.resolveClass(objectStreamClass);
						}

						// 这儿用自定义的SerializedLambda替换java原生的SerializedLambda
						return clazz == java.lang.invoke.SerializedLambda.class ? SerializedLambda.class : clazz;
					}
				};

				SerializedLambda var2;
				try {
					// 反序列化成SerializedLambda
					var2 = (SerializedLambda) objIn.readObject();
				} catch (Throwable var5) {
					try {
						objIn.close();
					} catch (Throwable var4) {
						var5.addSuppressed(var4);
					}

					throw var5;
				}

				objIn.close();
				return var2;
			} catch (IOException | ClassNotFoundException var6) {
				0
				throw ExceptionUtils.mpe("This is impossible to happen", var6, new Object[0]);
			}
		}
	}

可以看到,在这个方法中主要做了3件事

1.将Lambda表达式序列化,

2.将序列化后的Lambda表达式反序列化,

3.使用自定义的SerializedLambda顶替java原生的SerializedLambda作为反序列化结果。

那么问题来了,按照正常的思维,我们将一个对象序列化,再反序列化,应该得到这个对象的本身,为什么会得到一个SerializedLambda呢?要弄明白这个问题,我们还得回头看看SerializedLambda的源码介绍,开头有这样一段话

 * <p>Implementors of serializable lambdas, such as compilers or language
 * runtime libraries, are expected to ensure that instances deserialize properly.
 * One means to do so is to ensure that the {@code writeReplace} method returns
 * an instance of {@code SerializedLambda}, rather than allowing default
 * serialization to proceed.
翻译:可序列化lambda的实现程序(如编译器或语言运行库)应确保实例正确反序列化。
这样做的一种方法是确保{@code writeReplace}方法返回{@code SerializedLambda}的实例,而不是允许默认序列化继续进行。

提取几个关键词,可序列化的Lambda,writeReplace,序列化。

先说可序列化的Lambda,回头看LambdaQueryWrapper的参数,是一个SFunction,除了实现了Function外,还实现了Serializable,确实满足可序列化的Lambda的条件。也就是说,文章最开头匿名内部类的写法是错误的,不支持的。

再往后,提到了writeReplace方法,我们如果看编译后的class文件,可以看到Lambda表达式里面出现了一个writeReplace方法,那么,我们可不可以使用反射直接调用这个方法呢?答案是可以的。

public SerializedLambda getSerializedLambda(SFunction<?, ?> column){
    Class<?> columnClass = column.getClass();
    if (!columnClass.isSynthetic()) {
        throw new RuntimeException("该方法仅能传入lambda表达式产生的合成类");
    }
    Method writeReplace;
    try {
        writeReplace = columnClass.getDeclaredMethod("writeReplace");
    } catch (NoSuchMethodException e) {
        throw new RuntimeException(e);
    }
    writeReplace.setAccessible(true);

    SerializedLambda serializedLambda;
    try {
        serializedLambda = (SerializedLambda) writeReplace.invoke(column);
    } catch (IllegalAccessException | InvocationTargetException e) {
        throw new RuntimeException("解析失败Lambda表达式失败");
    }
}

这样的代码同样可以获取到SerializedLambda对象,并且由于省略的反序列化的过程,性能上是要比MyBatis-Plus的那种方法要快的。至于MyBatis-Plus为什么不用这种方法,我们就无从得知了。

最后总结一下:

1. LambdaQueryWrapper中的SFunction只能用Lambda表达式,不能用内部类。
2. 可序列化的Lambda表达式在编译后会生成一个writeReplace方法,返回值是一个SerializedLambda对象。
3. SerializedLambda对象中包含了很多Lambda表达式的详细信息,比如实现的方法名称等。

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值