利用FunctionalInterface获取类字段方法

Posted justry_deng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用FunctionalInterface获取类字段方法相关的知识,希望对你有一定的参考价值。

利用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中对反射做了很多优化,性能也接近于直接调用了,但是我们在代码层面还可以继续优化,比如考虑引入缓存之类的机制等等。

相关资料

以上是关于利用FunctionalInterface获取类字段方法的主要内容,如果未能解决你的问题,请参考以下文章

关于Java的File类字节流和字符流

JAVAJDK8新特性:函数式接口@FunctionalInterface的使用说明JDK8新特性:函数式接口@FunctionalInterface的使用说明

为啥在 Java 8 中使用 @FunctionalInterface 注解

阅源-jdk8-FunctionalInterface注解

函数式接口@FunctionalInterface,构建一对多Service结构

markdown FunctionalInterface