Java中如何在不通过委托的情况下将SFunction类型Lambda转换为TypedPropertyGetter类型?
我完全理解你的困扰——用MyBatis-Plus封装抽象类简化查询时,卡在了Lambda类型转换上,还被PropertyUtils的“call site specific”异常搞头大。这个问题的核心其实是:你用lambda::apply做委托转换时,生成的新Lambda会捕获原来的SFunction实例,导致它的类里多了一个成员字段,刚好踩中了PropertyUtils的检测逻辑。
下面给你一个可行的解决方案,不用委托就能完成类型转换:
解决方案:用LambdaMetafactory直接生成目标类型Lambda
因为SFunction<T,R>的apply(T)方法和TypedPropertyGetter<T,R>的get(T)方法签名完全一致(都是接收T返回R),我们可以借助Java的LambdaMetafactory直接把SFunction的方法引用转换成TypedPropertyGetter实例,绕开中间的委托包装,这样生成的Lambda不会有任何捕获的变量,就能通过PropertyUtils的检测了。
先写一个工具类来做转换:
import java.lang.invoke.LambdaMetafactory; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.util.function.Function; import de.cronn.reflection.util.TypedPropertyGetter; public class LambdaConverter { @SuppressWarnings("unchecked") public static <T, R> TypedPropertyGetter<T, R> convertToTypedPropertyGetter(Function<T, R> function) { try { MethodHandles.Lookup lookup = MethodHandles.lookup(); // 定义SFunction.apply方法的方法类型 MethodType applyMethodType = MethodType.methodType(Object.class, Object.class); // 定义TypedPropertyGetter.get方法的方法类型 MethodType getMethodType = MethodType.methodType(Object.class, Object.class); // 获取SFunction实例的apply方法句柄 MethodHandle applyHandle = lookup.unreflect(function.getClass().getMethod("apply", Object.class)); // 通过LambdaMetafactory直接生成TypedPropertyGetter实例 return (TypedPropertyGetter<T, R>) LambdaMetafactory.metafactory( lookup, "get", // 目标接口的方法名 MethodType.methodType(TypedPropertyGetter.class), // 生成的Lambda类型 getMethodType, // 目标方法的类型(擦除后) applyHandle, // 实际调用的方法句柄 applyMethodType // 实际方法的类型(擦除后) ).getTarget().invokeExact(); } catch (Throwable e) { throw new RuntimeException("转换SFunction到TypedPropertyGetter失败", e); } } }
然后修改你抽象类里的reflectPropertyName方法,用这个工具类来转换:
private String reflectPropertyName(SFunction<T, ?> getXxxSFunc) { if (getXxxSFunc == null) return null; try { TypedPropertyGetter<T, ?> getter = LambdaConverter.convertToTypedPropertyGetter(getXxxSFunc); return PropertyUtils.getPropertyName(entityClass, getter); } catch (Exception e) { throw new RuntimeException(e); } }
为什么这个方法能解决问题?
你之前用lambda::apply的时候,相当于创建了一个新的Lambda实例,这个实例内部会捕获原来的SFunction对象,所以它的类会自动生成一个成员字段来保存这个捕获的对象——这就是PropertyUtils检测到declaredFields.length > 0的原因。
而用LambdaMetafactory生成的Lambda,是直接绑定到原来的方法引用(比如User::getUsername),没有捕获任何外部变量,所以它的类里不会有额外的成员字段,自然能通过PropertyUtils的assertHasNoDeclaredFields检测。
备选方案:直接修改抽象方法参数类型
如果你不想引入LambdaMetafactory的代码,也可以把抽象类里的getEntityByUniqueColumn方法的参数从SFunction改成TypedPropertyGetter,这样上层调用的时候直接传User::getUsername就行:
// 抽象service修改后的方法 public <R> T getEntityByUniqueColumn(TypedPropertyGetter<T, R> lambda, R value) { // ... 原有逻辑,直接用lambda获取属性名 } // 实际service调用 public Long getUid(String username) { return getEntityByUniqueColumn(User::getUsername, username); }
不过这个方案需要修改你项目中所有相关的抽象方法和调用处,如果你已经有大量代码基于SFunction,那还是第一个方案更省心。
备注:内容来源于stack exchange,提问作者Emiya Elien

