黑马程序员技术交流社区

标题: 详解Java8中如何通过方法引用获取属性名 [打印本页]

作者: 逆风TO    时间: 2020-4-28 13:47
标题: 详解Java8中如何通过方法引用获取属性名
本帖最后由 逆风TO 于 2020-4-28 13:58 编辑

在我们开发过程中常常有一个需求,就是要知道实体类中Getter方法对应的属性名称(Field Name),例如实体类属性到数据库字段的映射,我们常常是硬编码指定 属性名,这种硬编码有两个缺点。
1、编码效率低:因为要硬编码写属性名,很可能写错,需要非常小心,时间浪费在了不必要的检查上。
2、容易让开发人员踩坑:例如有一天发现实体类中Field Name定义的不够明确,希望换一个Field Name,那么代码所有硬编码的Field Name都要跟着变更,对于未并更的地方,是无法在编译期发现的。只要有未变更的地方都可能导致bug的出现。
而使用了方法引用后,如果Field Name变更及其对应的Getter/Setter方法变更,编译器便可以实时的帮助我们检查变更的代码,在编译器给出错误信息。

那么如何通过方法引用获取Getter方法对应的Field Name呢?

Java8中给我们提供了实现方式,首先要做的就是定义一个可序列化的函数式接口(实现Serializable),实现如下:
[Java] 纯文本查看 复制代码
/**
* Created by bruce on 2020/4/10 14:16
*/
@FunctionalInterface
public interface SerializableFunction<T, R> extends Function<T, R>, Serializable {

}

而在使用时,我们需要传递Getter方法引用
[Java] 纯文本查看 复制代码
 //方法引用
SerializableFunction<People, String> getName1 = People::getName;
Field field = ReflectionUtil.getField(getName1);

下面看具体怎么解析这个SerializableFunction,完整实现如下ReflectionUtil
[Java] 纯文本查看 复制代码
public class ReflectionUtil {

    private static Map<SerializableFunction<?, ?>, Field> cache = new ConcurrentHashMap<>();

    public static <T, R> String getFieldName(SerializableFunction<T, R> function) {
        Field field = ReflectionUtil.getField(function);
        return field.getName();
    }
    public static Field getField(SerializableFunction<?, ?> function) {
        return cache.computeIfAbsent(function, ReflectionUtil::findField);
    }
    public static Field findField(SerializableFunction<?, ?> function) {
        Field field = null;
        String fieldName = null;
        try {
            // 第1步 获取SerializedLambda
            Method method = function.getClass().getDeclaredMethod("writeReplace");
            method.setAccessible(Boolean.TRUE);
            SerializedLambda serializedLambda = (SerializedLambda) method.invoke(function);
            // 第2步 implMethodName 即为Field对应的Getter方法名
            String implMethodName = serializedLambda.getImplMethodName();
            if (implMethodName.startsWith("get") && implMethodName.length() > 3) {
                fieldName = Introspector.decapitalize(implMethodName.substring(3));

            } else if (implMethodName.startsWith("is") && implMethodName.length() > 2) {
                fieldName = Introspector.decapitalize(implMethodName.substring(2));
            } else if (implMethodName.startsWith("lambda$")) {
                throw new IllegalArgumentException("SerializableFunction不能传递lambda表达式,只能使用方法引用");
               
            } else {
                throw new IllegalArgumentException(implMethodName + "不是Getter方法引用");
            }
            // 第3步 获取的Class是字符串,并且包名是“/”分割,需要替换成“.”,才能获取到对应的Class对象
            String declaredClass = serializedLambda.getImplClass().replace("/", ".");
            Class<?> aClass = Class.forName(declaredClass, false, ClassUtils.getDefaultClassLoader());

            // 第4步  Spring 中的反射工具类获取Class中定义的Field
            field = ReflectionUtils.findField(aClass, fieldName);

        } catch (Exception e) {
            e.printStackTrace();
        }
        // 第5步 如果没有找到对应的字段应该抛出异常
        if (field != null) {
            return field;
        }
        throw new NoSuchFieldError(fieldName);
    }
}
该类中主要有如下三个方法


String getFieldName(SerializableFunction<T, R> function) 获取Field的字符串name


Field getField(SerializableFunction<?, ?> function)从缓存中查询方法引用对应的Field,如果没有则通过findField(SerializableFunction<?, ?> function)方法反射获取


Field findField(SerializableFunction<?, ?> function) 反射获取方法应用对应的Field


实现原理


1、首先我们看最后一个方法Field findField(SerializableFunction<?, ?> function),该方法中第一步是通过SerializableFunction对象获取Class,即传递的方法引用,然后反射获取writeReplace()方法,并调用该方法获取导SerializedLambda对象。
       2、SerializedLambda是Java8中提供,主要就是用于封装方法引用所对应的信息,主要的就是方法名、定义方法的类名、创建方法引用所在类。
      3、拿到这些信息后,便可以通过反射获取对应的Field。
      4、而在方法Field getField(SerializableFunction<?, ?> function)中对获取到的Field进行缓存,避免每次都反射获取,造成资源浪费。


除此之外似乎还有一些值得思考的问题


writeReplace()方法是哪来的呢?


首先简单了解一下java.io.Serializable接口,该接口很常见,我们在持久化一个对象或者在RPC框架之间通信使用JDK序列化时都会让传输的实体类实现该接口,该接口是一个标记接口没有定义任何方法,但是该接口文档中有这么一段描述:
[Java] 纯文本查看 复制代码
Serializable classes that need to designate an alternative object to be used when writing an object to the stream should implement this special method with the exact signature:
   ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
   
This writeReplace method is invoked by serialization if the method exists and it would be accessible from a method defined within the class of the object being serialized. Thus, the method can have private, protected and package-private access. Subclass access to this method follows java accessibility rules.

概要意思就是说,如果想在序列化时改变序列化的对象,可以通过在实体类中定义任意访问权限的Object writeReplace()来改变默认序列化的对象。

那么我们的定义的SerializableFunction中并没有定义writeReplace()方法,这个方法是哪来的呢?
代码中SerializableFunction,Function只是一个接口,但是其在最后必定也是一个实现类的实例对象,而方法引用其实是在运行时动态创建的,当代码执行到方法引用时,如People::getName,最后会经过
java.lang.invoke.LambdaMetafactory
java.lang.invoke.InnerClassLambdaMetafactory
去动态的创建实现类。而在动态创建实现类时则会判断函数式接口是否实现了Serializable,如果实现了,则添加writeReplace()





值得注意的是,代码中多次编写的同一个方法引用,他们创建的是不同Function实现类,即他们的Function实例对象也并不是同一个
我们可以通过如下属性配置将 动态生成的Class保存到 磁盘上
[Java] 纯文本查看 复制代码
 System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");

示例代码如下:

动态生成的Class如下:

一个方法引用创建一个实现类,他们是不同的对象,那么ReflectionUtil中将SerializableFunction最为缓存key还有意义吗?

答案是肯定有意义的!!!因为同一方法中的定义的Function只会动态的创建一次实现类并只实例化一次,当该方法被多次调用时即可走缓存中查询该方法引用对应的Field

这里的缓存Key应该选用SerializableFunction#Class还是SerializableFunction实例对象好呢?

看到有些实现使用SerializableFunction的Class作为缓存key,代码如下:

[Java] 纯文本查看 复制代码
public static Field getField(SerializableFunction<?, ?> function) {
     //使用SerializableFunction的Class作为缓存key,导致每次都调用function.getClass()
     return cache.computeIfAbsent(function.getClass(), ReflectionUtil::findField);
}

但是个人建议采用SerializableFunction对象,因为无论方法被调用多少次,方法代码块内的方法引用对象始终是同一个,如果采用其Class做为缓存key,每次查询缓存时都需要调用native方法function.getClass()获取其Class,也是一种资源损耗。


总结:Java如何通过方法引用获取属性名实现及思考至此结束。直接使用ReflectionUtil即可


本文转载自:https://blog.csdn.net/u013202238/article/details/105779686




作者: 孙丽    时间: 2020-5-4 11:35
6666666666666666666666
作者: 孙丽    时间: 2020-5-4 11:39
6666666666666666666666
作者: 我是小圆圆    时间: 2020-5-4 11:44
感谢分享哦~
作者: 王锦    时间: 2020-5-4 11:44
666666666666666
作者: 我是小圆圆    时间: 2020-5-4 11:46
感谢分享哦~
作者: zhaosongzhi    时间: 2020-5-4 11:47
作者棒棒哒
作者: zhaosongzhi    时间: 2020-5-4 11:56
作者棒棒哒
作者: zhaosongzhi    时间: 2020-5-4 12:05
作者棒棒哒
作者: zhaosongzhi    时间: 2020-5-4 12:13
作者棒棒哒
作者: 霍尔    时间: 2020-5-4 12:16
6666666666666666666
作者: zhaosongzhi    时间: 2020-5-4 12:22
作者棒棒哒
作者: zhaosongzhi    时间: 2020-5-4 12:30
作者棒棒哒
作者: zhaosongzhi    时间: 2020-5-4 12:39
作者棒棒哒
作者: zhaosongzhi    时间: 2020-5-4 12:47
作者棒棒哒
作者: 零度☆黎明    时间: 2020-5-4 13:02
不错, 不错 .................. ..................
作者: zplxwl    时间: 2020-5-4 13:08
很不错哦!!!
作者: 半个程序员    时间: 2020-5-4 13:39

感谢分享哦~
作者: hello!!!    时间: 2020-5-4 14:19

作者: 马洁    时间: 2020-5-4 15:56
全是干货,你看看
作者: 耙丫丫    时间: 2020-5-4 16:12
666666666666666666666666
作者: sdjadyhm    时间: 2020-5-4 16:28

感谢分享哟~  棒棒哒加油~~
作者: 哦嗨呦    时间: 2020-5-4 16:53
好人一生平安
作者: 黑马程序员啊    时间: 2020-5-4 18:33
6666666666666666
作者: manyihang    时间: 2020-5-4 20:09
666666666666666666666
作者: mydorling11    时间: 2020-5-5 09:37
2132131231232121321
作者: 逆风TO    时间: 2020-5-5 09:47
感谢分享呀~
作者: 逆风TO    时间: 2020-5-5 09:48
感谢分享呀~
作者: lvxinvip    时间: 2020-5-5 09:50

作者: hongping    时间: 2020-5-5 10:31
全是干货,你看看
作者: 1467584    时间: 2020-5-5 15:25
66666666666666666666666666
作者: 章鱼顶呱呱    时间: 2020-5-5 15:26
666666666666666666
作者: yujq    时间: 2020-5-5 21:02
666666666666666666666666
作者: daoqin    时间: 2020-5-6 09:24
感谢分享~加油哦~
作者: 咨询部王丹    时间: 2020-5-6 09:52
非常有帮助   
作者: 大安    时间: 2020-5-6 09:57

感谢分享哦~
作者: kdhdjdj    时间: 2020-5-6 14:27

作者棒棒哒
作者: kdhdjdj    时间: 2020-5-6 14:28

作者棒棒哒
作者: 影@子~    时间: 2020-5-7 09:59
感谢分享
作者: 大智叔叔    时间: 2020-5-7 15:41

很不错哦!!!
作者: 大智叔叔    时间: 2020-5-7 15:43

很不错哦!!!
作者: 你不爱我    时间: 2020-5-7 16:10
感谢分享                  
作者: 举个栗子    时间: 2020-5-7 16:56
66666666666666666666666666
作者: 小公举    时间: 2020-5-7 17:25
太牛了吧膜拜
作者: 王微    时间: 2020-5-7 17:50
感谢分享哦~
作者: 我爱我1022    时间: 2020-5-7 17:51

作者: Emmmmm~    时间: 2020-5-8 09:46

666666666666666666
作者: lzq123    时间: 2020-5-8 10:01
66666666666666666666666666666
作者: lzq123    时间: 2020-5-8 10:03
666666666666666666666666666666666666666666
作者: jsnoob    时间: 2020-5-8 10:04
加油加油加油!!!
作者: 雨落轻舟    时间: 2020-5-8 11:49
总结到位,加油。。。
作者: 素问    时间: 2020-5-8 13:17
感谢分享,加油~~~~!!!!
作者: 殷凯老师    时间: 2020-5-8 14:26
66666666666666




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2