利用FunctionalInterface获取类、字段、方法
背景说明
最近有看到同事用mybatis-plus的LambdaQueryWrapper写查询逻辑,其中本来应该传列名的位置直接使用了lambda来完成:
好奇之下,对其中原理进行了一点点学习。
原理说明
提示:更多信息详见源码
LambdaMetafactory
类、SerializedLambda
类javadoc说明。
原理
如果生成的lambda类需要实现java.io.Serializable
的话,那么在生成的lambda实现类中,就会有一个名为writeReplace
的方法来作该lambda类的序列化支持(直观的,可见下面的示例)。writeReplace
的返回值是SerializedLambda
,我们通过反射调用拿到writeReplace
返回的SerializedLambda
对象后,就可以获得你所写的lambda表达式中所涉及到的类、方法等信息了(看一下SerializedLambda的构造,就知道可以拿到哪些东西了):
原理示例
-
首先,有一个集成了
java.io.Serializable
能力的FunctionalInterface
功能接口:/** * 集成了{@link Serializable}能力的{@link Function} * <pre> * 当然,获得{@link SerializedLambda},不一定非要使用Function,使用其他的{@link FunctionalInterface}也行, * 只要保证以下两点即可: * 1. 会生成lambda实现类 * 2. 生成的lambda实现类要实现Serializable * 注:如果只是获取SerializedLambda实例本身,那么除了上述FunctionalInterface的方式外,还可以通过其它方式获得(如:序列化/反序列化等)。 * </pre> * * @author JustryDeng * @since 2022/4/9 9:24 */ public interface SFunction<T, R> extends Function<T, R>, Serializable { }
注:
FunctionalInterface
是为了能自动生成lambda实现类;Serializable
是为了保证自动生成lambda实现类对Serializable
作功能支持,即:生成writeReplace
方法。 -
然后,我们有这样一个简化的lambda写法:
// SerializedLambdaUtil.getFieldName(SFunction<T, ?> sFunction) // 即:Person::getName是对SFunction<T, ?>实现的一种简化的lambda写法 SerializedLambdaUtil.getFieldName(Person::getName)
-
其中,对于lambda写法
Person::getName
,JVM会自动生成实现类:final class MainTests$$Lambda$1 implements SFunction { @Hidden public Object apply(Object var1) { return ((Person)var1).getName(); } private final Object writeReplace() { return new SerializedLambda( MainTests.class, "com/example/lambda/demo/SFunction", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", 5, "com/example/lambda/test/Person", "getName", "()Ljava/lang/String;", "(Lcom/example/lambda/test/Person;)Ljava/lang/Object;", new Object[0] ); } }
注:上述展示的这个JVM自动生成的类,是我运行时直接dump class出来后反编译出来的。
可以看到,自动生成的lambda类中存在writeReplace方法,该方法返回SerializedLambda对象。
实际应用
应用说明
本应用仿mybatis-plus,即:利用FunctionalInterface,获取字段名,也可称为:利用SerializedLambda,获取字段名。
工具类封装
-
SFunction:一种FunctionalInterface能力支持接口
import java.io.Serializable; import java.lang.invoke.SerializedLambda; import java.util.function.Function; /** * 集成了{@link Serializable}能力的{@link Function} * <pre> * 当然,获得{@link SerializedLambda},不一定非要使用Function,使用其他的{@link FunctionalInterface}也行, * 只要保证以下两点即可: * 1. 会生成lambda实现类 * 2. 生成的lambda实现类要实现Serializable * 注:如果只是获取SerializedLambda实例本身,那么除了上述FunctionalInterface的方式外,还可以通过其它方式获得(如:序列化/反序列化等)。 * </pre> * * @author JustryDeng * @since 2022/4/9 9:24 */ public interface SFunction<T, R> extends Function<T, R>, Serializable { }
-
SerializedLambdaUtil:SerializedLambda工具类
import org.apache.commons.lang3.StringUtils; import java.io.Serializable; import java.lang.invoke.SerializedLambda; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * {@link SerializedLambda}工具类 * * @author JustryDeng * @since 2022/4/9 11:43 */ public class SerializedLambdaUtil { public static FieldNameParser defaultFieldNameParser = new FieldNameParser(){}; /** * @see SerializedLambdaUtil#getFieldName(SFunction) */ public static <T> String getFieldName(SFunction<T, ?> sFunction) { return getFieldName(sFunction, defaultFieldNameParser); } /** * 获取字段名称 */ public static <T> String getFieldName(SFunction<T, ?> sFunction, FieldNameParser fieldNameParser) { return getFieldName(getSerializedLambda(sFunction), fieldNameParser); } /** * 获取lambda表达式字段名称 * <pre> * 假设你的lambda表达式部分是这样写的:<code>Person::getFirstName</code>, * 那么,此方法的目的就是获取到getFirstName方法对应的(Person类中的对应字段的)字段名 * </pre> */ public static String getFieldName(SerializedLambda serializedLambda, FieldNameParser fieldNameParser) { String implClassLongName = getImplClassLongName(serializedLambda); String implMethodName = getImplMethodName(serializedLambda); try { return fieldNameParser.parseFieldName(Class.forName(implClassLongName), implMethodName); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } /** * 获取lambda表达式中,实现方法的方法名 * <p> * 说明: * 假设你的lambda表达式部分是这样写的:<code>Person::getFirstName</code>,<br/> * 那么这里获取到的就是Person.getFirstName()的方法名getFirstName * </p> * * @param serializedLambda * serializedLambda对象 * @return 实现方法的方法名 <br /> * 形如:getFirstName */ private static String getImplMethodName(SerializedLambda serializedLambda) { return serializedLambda.getImplMethodName(); } /** * 获取lambda表达式中,实现方法的类的全类名 * <p> * 说明: * 假设你的lambda表达式部分是这样写的:<code>Person::getFirstName</code>,<br/> * 那么这里获取到的就是Person的全类名,形如:<code>com.example.lambda.test.Person</code> * </p> * * @param serializedLambda * serializedLambda对象 * @return 实现方法的类的全类名 <br /> * 形如:com.example.lambda.test.Person */ private static String getImplClassLongName(SerializedLambda serializedLambda) { return serializedLambda.getImplClass().replace("/", "."); } /** * 获取SerializedLambda实例 * * @param potentialLambda * lambda实例 * @return SerializedLambda实例 */ private static <T extends Serializable> SerializedLambda getSerializedLambda(T potentialLambda) { try{ Class<?> potentialLambdaClass = potentialLambda.getClass(); // lambda类属于合成类 if (!potentialLambdaClass.isSynthetic()) { throw new IllegalArgumentException("potentialLambda must be lambda-class"); } Method writeReplaceMethod = potentialLambdaClass.getDeclaredMethod("writeReplace"); boolean isAccessible = writeReplaceMethod.isAccessible(); writeReplaceMethod.setAccessible(true); Object writeReplaceObject = writeReplaceMethod.invoke(potentialLambda); writeReplaceMethod.setAccessible(isAccessible); if (writeReplaceObject == null || !SerializedLambda.class.isAssignableFrom(writeReplaceObject.getClass())) { throw new IllegalArgumentException("potentialLambda must be lambda-class. writeReplaceObject should not be " + writeReplaceObject); } return (SerializedLambda)writeReplaceObject; } catch( NoSuchMethodException | IllegalAccessException | InvocationTargetException e ){ throw new IllegalArgumentException("potentialLambda must be lambda-class", e); } } /** * 字段名解析器 * * @author JustryDeng * @since 2022/4/9 11:31 */ public interface FieldNameParser { /** * 解析字段名 * <pre> * 假设你的lambda表达式部分是这样写的:<code>Person::getFirstName</code>, * 那么, * clazz就对应Person类 * methodName就对应getFirstName * </pre> * * @param clazz * 字段所在的类 * @param methodName * 与字段相关的方法(如:该字段的getter方法) * @return 解析字段名 */ default String parseFieldName(Class<?> clazz, String methodName) { return StringUtils.uncapitalize(methodName.substring("get".length())); } } }
测试一下
-
测试辅助实体模型
import lombok.Data; @Data @SuppressWarnings("all") public class Person { private String name; private String nickName; }
-
测试类
public class MainTests { /** * 测试 */ public static void main(String[] args) { /* ------------------------- 测试 SerializedLambdaUtil工具类 ------------------------- */ System.out.println(); System.out.println("通过FunctionalInterface方式,获得字段名:" + SerializedLambdaUtil.getFieldName(Person::getName)); System.out.println("通过FunctionalInterface方式,获得字段名:" + SerializedLambdaUtil.getFieldName(Person::getNickName)); /* ------------------------- dump运行时class并反编译,以观察生成的lambda实现类 ------------------------- */ // dump class Map<String, Map<String, byte[]>> dumpedClasses = NonExitClassFileTransformerExecutor .create("com.example.lambda.test", null, false) .exec(); List<byte[]> list = new ArrayList<>(8); dumpedClasses.forEach((k, map) -> list.addAll(map.values())); // 反编译 List<Triple<String, String, String>> triples = Decompiler.defaultDecompiler().decompileAsTriple(list); String projectRootDir = PathUtil.getProjectRootDir(MainTests.class); projectRootDir = projectRootDir.replace("/target/classes/", "/src/main/resources/sourceCode/"); IOUtil.delete(new File(projectRootDir)); for (Triple<String, String, String> triple : triples) { IOUtil.toFile(triple.getRight().getBytes(StandardCharsets.UTF_8), new File(projectRootDir, triple.getLeft() + ".java"), true); } } }
-
运行main方法,观察结果
注:dump class、反编译工具见https://gitee.com/JustryDeng/components。
注意事项
SerializedLambda在很多时候确实很好用,但因为使用过程中不可避免的用到了反射,所以需要考虑性能问题。虽然高版本jdk中对反射做了很多优化,性能也接近于直接调用了,但是我们在代码层面还可以继续优化,比如考虑引入缓存之类的机制等等。