A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

© 逆风TO 黑马粉丝团   /  2020-4-28 13:47  /  4543 人查看  /  52 人回复  /   0 人收藏 转载请遵从CC协议 禁止商业使用本文

本帖最后由 逆风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()

1.jpg

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

示例代码如下:
4.jpg
动态生成的Class如下:
5.jpg
一个方法引用创建一个实现类,他们是不同的对象,那么ReflectionUtil中将SerializableFunction最为缓存key还有意义吗?

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

这里的缓存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



52 个回复

倒序浏览
6666666666666666666666
回复 使用道具 举报
6666666666666666666666
回复 使用道具 举报
感谢分享哦~
回复 使用道具 举报
666666666666666
回复 使用道具 举报
感谢分享哦~
回复 使用道具 举报
作者棒棒哒
回复 使用道具 举报
作者棒棒哒
回复 使用道具 举报
作者棒棒哒
回复 使用道具 举报
作者棒棒哒
回复 使用道具 举报
6666666666666666666
回复 使用道具 举报
作者棒棒哒
回复 使用道具 举报
作者棒棒哒
回复 使用道具 举报
作者棒棒哒
回复 使用道具 举报
作者棒棒哒
回复 使用道具 举报
不错, 不错 .................. ..................
回复 使用道具 举报
很不错哦!!!
回复 使用道具 举报

感谢分享哦~
回复 使用道具 举报
回复 使用道具 举报
全是干货,你看看
回复 使用道具 举报
123下一页
您需要登录后才可以回帖 登录 | 加入黑马