前言:

最近在使用mybatis-plus框架, 常常会使用lambda的方法引用获取实体属性, 避免出现大量的魔法值.

public List<User> listBySex() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// lambda方法引用
queryWrapper.eq(User::getSex, "男");
return userServer.list(wrapper);
}

那么在我们平时的开发过程中, 常常需要用到java bean的属性名, 直接写死属性名字符串的形式容易产生bug, 比如属性名变化, 编译时并不会报错, 只有在运行时才会报错该对象没有指定的属性名称. 而lambda的方式不仅可以简化代码, 而且可以通过getter方法引用拿到属性名, 避免潜在bug.

期望的效果

String userName = BeanUtils.getFieldName(User::getName);
System.out.println(userName);
// 输出: name

实现步骤

  1. 定义一个函数式接口, 用来接收lambda方法引用

    注意: 函数式接口必须继承Serializable接口才能获取方法信息

    @FunctionalInterface
    public interface SFunction<T> extends Serializable {
    Object apply(T t);
    }
  2. 定义一个工具类, 用来解析获取属性名称

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.util.ClassUtils;
    import org.springframework.util.ReflectionUtils; import java.beans.Introspector;
    import java.lang.invoke.SerializedLambda;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap; @Slf4j
    public class BeanUtils {
    private static final Map<SFunction<?>, Field> FUNCTION_CACHE = new ConcurrentHashMap<>(); public static <T> String getFieldName(SFunction<T> function) {
    Field field = BeanUtils.getField(function);
    return field.getName();
    } public static <T> Field getField(SFunction<T> function) {
    return FUNCTION_CACHE.computeIfAbsent(function, BeanUtils::findField);
    } public static <T> Field findField(SFunction<T> function) {
    // 第1步 获取SerializedLambda
    final SerializedLambda serializedLambda = getSerializedLambda(function);
    // 第2步 implMethodName 即为Field对应的Getter方法名
    final String implClass = serializedLambda.getImplClass();
    final String implMethodName = serializedLambda.getImplMethodName();
    final String fieldName = convertToFieldName(implMethodName);
    // 第3步 Spring 中的反射工具类获取Class中定义的Field
    final Field field = getField(fieldName, serializedLambda); // 第4步 如果没有找到对应的字段应该抛出异常
    if (field == null) {
    throw new RuntimeException("No such class 「"+ implClass +"」 field 「" + fieldName + "」.");
    } return field;
    } static Field getField(String fieldName, SerializedLambda serializedLambda) {
    try {
    // 获取的Class是字符串,并且包名是“/”分割,需要替换成“.”,才能获取到对应的Class对象
    String declaredClass = serializedLambda.getImplClass().replace("/", ".");
    Class<?>aClass = Class.forName(declaredClass, false, ClassUtils.getDefaultClassLoader());
    return ReflectionUtils.findField(aClass, fieldName);
    }
    catch (ClassNotFoundException e) {
    throw new RuntimeException("get class field exception.", e);
    }
    } static String convertToFieldName(String getterMethodName) {
    // 获取方法名
    String prefix = null;
    if (getterMethodName.startsWith("get")) {
    prefix = "get";
    }
    else if (getterMethodName.startsWith("is")) {
    prefix = "is";
    } if (prefix == null) {
    throw new IllegalArgumentException("invalid getter method: " + getterMethodName);
    } // 截取get/is之后的字符串并转换首字母为小写
    return Introspector.decapitalize(getterMethodName.replace(prefix, ""));
    } static <T> SerializedLambda getSerializedLambda(SFunction<T> function) {
    try {
    Method method = function.getClass().getDeclaredMethod("writeReplace");
    method.setAccessible(Boolean.TRUE);
    return (SerializedLambda) method.invoke(function);
    }
    catch (Exception e) {
    throw new RuntimeException("get SerializedLambda exception.", e);
    }
    }
    }

测试

public class Test {
public static void main(String[] args) {
SFunction<User> user = User::getName;
final String fieldName = BeanUtils.getFieldName(user);
System.out.println(fieldName);
} @Data
static class User {
private String name; private int age;
}
}

执行测试 输出结果

原理剖析

为什么SFunction必须继承Serializable

首先简单了解一下java.io.Serializable接口,该接口很常见,我们在持久化一个对象或者在RPC框架之间通信使用JDK序列化时都会让传输的实体类实现该接口,该接口是一个标记接口没有定义任何方法,但是该接口文档中有这么一段描述:

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

代码中SFunction只是一个接口, 但是其在最后必定也是一个实现类的实例对象,而方法引用其实是在运行时动态创建的,当代码执行到方法引用时,如User::getName,最后会经过

java.lang.invoke.LambdaMetafactory
java.lang.invoke.InnerClassLambdaMetafactory

去动态的创建实现类。而在动态创建实现类时则会判断函数式接口是否实现了Serializable,如果实现了,则添加writeReplace方法

也就是说我们代码BeanUtils#getSerializedLambda方法中反射调用的writeReplace方法是在生成函数式接口实现类时添加进去的.

SFunction Class中的writeReplace方法

从上文中我们得知 当SFunction继承Serializable时, 底层在动态生成SFunction的实现类时添加了writeReplace方法, 那这个方法有什么用?

首先 我们将动态生成的类保存到磁盘上看一下

我们可以通过如下属性配置将 动态生成的Class保存到 磁盘上

java8中可以通过硬编码

 System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");

例如:

jdk11 中只能使用jvm参数指定,硬编码无效,原因是模块化导致的

-Djdk.internal.lambda.dumpProxyClasses=.

例如:

执行方法后输出文件如下:

其中实现类的类名是有具体含义的

其中Test$Lambda$15.class信息如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
// package test.java8.lambdaimpl; import java.lang.invoke.SerializedLambda;
import java.lang.invoke.LambdaForm.Hidden;
import test.java8.lambdaimpl.Test.User; // $FF: synthetic class
final class Test$$Lambda$15 implements SFunction {
private Test$$Lambda$15() {
} @Hidden
public Object apply(Object var1) {
return ((User)var1).getName();
} private final Object writeReplace() {
return new SerializedLambda(Test.class, "test/java8/lambdaimpl/SFunction", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", 5, "test/java8/lambdaimpl/Test$User", "getName", "()Ljava/lang/String;", "(Ltest/java8/lambdaimpl/Test$User;)Ljava/lang/Object;", new Object[0]);
}
}

通过源码得知 调用writeReplace方法是为了获取到方法返回的SerializedLambda对象

SerializedLambda: 是Java8中提供,主要就是用于封装方法引用所对应的信息,主要的就是方法名、定义方法的类名、创建方法引用所在类。拿到这些信息后,便可以通过反射获取对应的Field。

值得注意的是,代码中多次编写的同一个方法引用,他们创建的是不同Function实现类,即他们的Function实例对象也并不是同一个。

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

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

通过内部类实现类的类名规则我们也能大致推断出来, 只要申明lambda的相对位置不变, 那么对应的Function实现类包括对象都不会变。

通过在刚才的示例代码中添加一行, 就能说明该问题, 之前15号对应的是getName, 而此时的15号class对应的是getAge这个函数引用

我们再通过代码验证一下 刚才的猜想

参考:

https://blog.csdn.net/u013202238/article/details/105779686

https://blog.csdn.net/qq_39809458/article/details/101423610

通过Lambda函数的方式获取属性名称的更多相关文章

  1. 定义通用的可通过lambda表达式树来获取属性信息

    我们一般获取某个类型或对象的属性信息均采用以下几种方法: 一.通过类型来获取属性信息 var p= typeof(People).GetProperty("Age");//获取指定 ...

  2. js 获取属性名称,再根据这个属性名获取值

    if (result.success) { var obj = JSON.parse(result.data); var sltObj = document.getElementById(" ...

  3. js 获取属性名称

    $(function ()        {            myfun();        })        function myfun()        {            var ...

  4. 利用Lambda获取属性名称

    感谢下面这篇博文给我的思路: http://www.cnblogs.com/daimage/archive/2012/04/10/2440186.html 上面文章的博主给出的代码是可用的,但是调用方 ...

  5. (函数封装)获取class名称

    使用原生JavaScript,获取类操作符时:即使使用getElementByClassName,在Firefox和IE9以下是不兼容的.Firefox下是可以用它获取的到元素而IE不行,一般框架都会 ...

  6. 【C++11】新特性——Lambda函数

    本篇文章由:http://www.sollyu.com/c11-new-lambda-function/ 文章列表 本文章为系列文章 [C++11]新特性--auto的使用 http://www.so ...

  7. C#通过属性名称获取(读取)属性值的方法

    之前在开发一个程序,希望能够通过属性名称读取出属性值,但是由于那时候不熟悉反射,所以并没有找到合适的方法,做了不少的重复性工作啊! 然后今天我再上网找了找,被我找到了,跟大家分享一下. 其实原理并不复 ...

  8. 获取JSON对象的属性名称

    1.问题背景 一个json对象,是以键值对组成,通过循环json对象,获取json对象中的属性名称 2.实现源码 <!DOCTYPE html PUBLIC "-//W3C//DTD ...

  9. C# 反射获取属性值、名称、类型以及集合的属性值、类型名称

    实体类 class Product { public string Id { get; set; } public string Name { get; set; } public List<P ...

  10. 【java】java获取对象属性类型、属性名称、属性值

    java获取对象属性类型.属性名称.属性值 获取属性 修饰符:[在Field[]循环中使用] String modifier = Modifier.toString(fields[i].getModi ...

随机推荐

  1. 云享·案例丨打造数智物流底座,华为云DTSE助力物联云仓解锁物流新“速度”

    摘要:华为云凭借领先的技术和快速响应的开发者支持服务,助力物联亿达实现云上资源高可用.提升系统安全性与稳定性,为物联亿达提供了扎实的数字化基础. 本文分享自华为云社区<云享·案例丨打造数智物流底 ...

  2. 从钢铁行业数字化管控与超自动化融合,看华为WeAutomate能力进化

    文/王吉伟 钢铁行业的数字化转型,历来都是值得探讨的热点话题. 2022年,我国粗钢产量10.13亿吨,占据了全球钢铁供给市场的半壁江山. 这组数据可谓非常抢眼,但仍旧难掩诸多企业的各种经营问题. 钢 ...

  3. 基于GPT搭建私有知识库聊天机器人(五)函数调用

    文章链接: 基于GPT搭建私有知识库聊天机器人(一)实现原理 基于GPT搭建私有知识库聊天机器人(二)环境安装 基于GPT搭建私有知识库聊天机器人(三)向量数据训练 基于GPT搭建私有知识库聊天机器人 ...

  4. debezium之mysql配置

    实验环境 全部部署于本地虚拟机 1 mysql 参考 官方文档 和 根据官方示例镜像(debezium/example-mysql,mysql版本为8.0.32) 1.1 创建用户 官方镜像里一共有三 ...

  5. Linux 压缩文件用法

    # tar 命令:可以用来压缩或解压缩文件: # 压缩 tar -czvf filename.tar.gz files # 解压缩 tar -xzvf filename.tar.gz # gzip 命 ...

  6. Linux 命令:time

    参考链接: time 命令

  7. [etcd]简介与安装

    简介 etcd是一个采用Raft协议实现强一致性的分布式键值数据库,它提供了一种可靠的方式存储需要被分布式系统或机器集群访问的数据. 常见使用场景:服务注册与发现.键值对存储.消息发布和订阅.分布式锁 ...

  8. 压测工具sysbench的使用

    前言 sysBench是一个模块化的.跨平台.多线程基准测试工具,主要用于评估测试各种不同系统参数下的数据库负载情况.sysbench提供如下测试: (1)CPU性能 (2)磁盘IO性能 (3)调度程 ...

  9. nflsoj 选数1 2 3

    5711 取数-1 状态表示:1维 集合:前 \(i\) 个数里面所有的选法和 属性:所有的选法和的最大值 状态计算:选或不选 选:\(f(i-1)+a_i\) 不选:\(f(i-1)\) #incl ...

  10. ZS Shuffles Cards 题解

    ZS Shuffles Cards 题解 我们把每一次抽一些数字牌再抽到 joker 视作一局游戏. 每局期望轮数 首先考虑 \(f_i\) 表示每一局游戏抽出 \(i\) 张牌的概率. 那么就是先抽 ...