Java通過(guò)Lambda函數(shù)的方式獲取屬性名稱
前言:
最近在使用mybatis-plus
框架, 常常會(huì)使用lambda的方法引用獲取實(shí)體屬性, 避免出現(xiàn)大量的魔法值.
public List<User> listBySex() { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); // lambda方法引用 queryWrapper.eq(User::getSex, "男"); return userServer.list(wrapper); }
那么在我們平時(shí)的開(kāi)發(fā)過(guò)程中, 常常需要用到j(luò)ava bean的屬性名, 直接寫死屬性名字符串的形式容易產(chǎn)生bug, 比如屬性名變化, 編譯時(shí)并不會(huì)報(bào)錯(cuò), 只有在運(yùn)行時(shí)才會(huì)報(bào)錯(cuò)該對(duì)象沒(méi)有指定的屬性名稱. 而lambda的方式不僅可以簡(jiǎn)化代碼, 而且可以通過(guò)getter方法引用拿到屬性名, 避免潛在bug.
期望的效果
String userName = BeanUtils.getFieldName(User::getName);
System.out.println(userName);
// 輸出: name
實(shí)現(xiàn)步驟
定義一個(gè)函數(shù)式接口, 用來(lái)接收l(shuí)ambda方法引用
注意: 函數(shù)式接口必須繼承Serializable接口才能獲取方法信息
@FunctionalInterface public interface SFunction<T> extends Serializable { Object apply(T t); }
定義一個(gè)工具類, 用來(lái)解析獲取屬性名稱
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對(duì)應(yīng)的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步 如果沒(méi)有找到對(duì)應(yīng)的字段應(yīng)該拋出異常 if (field == null) { throw new RuntimeException("No such class 「"+ implClass +"」 field 「" + fieldName + "」."); } return field; } static Field getField(String fieldName, SerializedLambda serializedLambda) { try { // 獲取的Class是字符串,并且包名是“/”分割,需要替換成“.”,才能獲取到對(duì)應(yīng)的Class對(duì)象 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之后的字符串并轉(zhuǎn)換首字母為小寫 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); } } }
測(cè)試
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; } }
執(zhí)行測(cè)試 輸出結(jié)果
原理剖析
為什么SFunction必須繼承Serializable
首先簡(jiǎn)單了解一下java.io.Serializable
接口,該接口很常見(jiàn),我們?cè)诔志没粋€(gè)對(duì)象或者在RPC框架之間通信使用JDK序列化時(shí)都會(huì)讓傳輸?shù)膶?shí)體類實(shí)現(xiàn)該接口,該接口是一個(gè)標(biāo)記接口沒(méi)有定義任何方法,但是該接口文檔中有這么一段描述:
概要意思就是說(shuō),如果想在序列化時(shí)改變序列化的對(duì)象,可以通過(guò)在實(shí)體類中定義任意訪問(wèn)權(quán)限的Object writeReplace()來(lái)改變默認(rèn)序列化的對(duì)象。
代碼中SFunction
只是一個(gè)接口, 但是其在最后必定也是一個(gè)實(shí)現(xiàn)類的實(shí)例對(duì)象,而方法引用其實(shí)是在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建的,當(dāng)代碼執(zhí)行到方法引用時(shí),如User::getName
,最后會(huì)經(jīng)過(guò)
java.lang.invoke.LambdaMetafactory java.lang.invoke.InnerClassLambdaMetafactory
去動(dòng)態(tài)的創(chuàng)建實(shí)現(xiàn)類。而在動(dòng)態(tài)創(chuàng)建實(shí)現(xiàn)類時(shí)則會(huì)判斷函數(shù)式接口是否實(shí)現(xiàn)了Serializable
,如果實(shí)現(xiàn)了,則添加writeReplace
方法
也就是說(shuō)我們代碼BeanUtils#getSerializedLambda
方法中反射調(diào)用的writeReplace
方法是在生成函數(shù)式接口實(shí)現(xiàn)類時(shí)添加進(jìn)去的.
SFunction Class中的writeReplace方法
從上文中我們得知 當(dāng)SFunction
繼承Serializable
時(shí), 底層在動(dòng)態(tài)生成SFunction
的實(shí)現(xiàn)類時(shí)添加了writeReplace
方法, 那這個(gè)方法有什么用?
首先 我們將動(dòng)態(tài)生成的類保存到磁盤上看一下
我們可以通過(guò)如下屬性配置將 動(dòng)態(tài)生成的Class保存到 磁盤上
java8中可以通過(guò)硬編碼
System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
例如:
jdk11 中只能使用jvm參數(shù)指定,硬編碼無(wú)效,原因是模塊化導(dǎo)致的
-Djdk.internal.lambda.dumpProxyClasses=.
例如:
執(zhí)行方法后輸出文件如下:
其中實(shí)現(xiàn)類的類名是有具體含義的
其中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]);
}
}
通過(guò)源碼得知 調(diào)用writeReplace
方法是為了獲取到方法返回的SerializedLambda
對(duì)象
SerializedLambda
: 是Java8中提供,主要就是用于封裝方法引用所對(duì)應(yīng)的信息,主要的就是方法名、定義方法的類名、創(chuàng)建方法引用所在類。拿到這些信息后,便可以通過(guò)反射獲取對(duì)應(yīng)的Field。
值得注意的是,代碼中多次編寫的同一個(gè)方法引用,他們創(chuàng)建的是不同F(xiàn)unction實(shí)現(xiàn)類,即他們的Function實(shí)例對(duì)象也并不是同一個(gè)。
一個(gè)方法引用創(chuàng)建一個(gè)實(shí)現(xiàn)類,他們是不同的對(duì)象,那么BeanUtils中將SFunction作為緩存key還有意義嗎?
答案是肯定有意義的?。?!因?yàn)橥环椒ㄖ械亩x的Function只會(huì)動(dòng)態(tài)的創(chuàng)建一次實(shí)現(xiàn)類并只實(shí)例化一次,當(dāng)該方法被多次調(diào)用時(shí)即可走緩存中查詢?cè)摲椒ㄒ脤?duì)應(yīng)的Field。
通過(guò)內(nèi)部類實(shí)現(xiàn)類的類名規(guī)則我們也能大致推斷出來(lái), 只要申明lambda的相對(duì)位置不變, 那么對(duì)應(yīng)的Function實(shí)現(xiàn)類包括對(duì)象都不會(huì)變。
通過(guò)在剛才的示例代碼中添加一行, 就能說(shuō)明該問(wèn)題, 之前15號(hào)對(duì)應(yīng)的是getName
, 而此時(shí)的15號(hào)class對(duì)應(yīng)的是getAge
這個(gè)函數(shù)引用
我們?cè)偻ㄟ^(guò)代碼驗(yàn)證一下 剛才的猜想
參考資料:
https://blog.csdn.net/u013202238/article/details/105779686
https://blog.csdn.net/qq_39809458/article/details/101423610
到此這篇關(guān)于通過(guò)Lambda函數(shù)的方式獲取屬性名稱的文章就介紹到這了,更多相關(guān)Lambda函數(shù)獲取屬性名稱內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring DATA JPA 中findAll 進(jìn)行OrderBy方式
這篇文章主要介紹了Spring DATA JPA 中findAll 進(jìn)行OrderBy方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11RocketMQ中消費(fèi)者概念和消費(fèi)流程詳解
這篇文章主要介紹了RocketMQ中消費(fèi)者概念和消費(fèi)流程詳解,RocketMQ是一款高性能、高可靠性的分布式消息中間件,消費(fèi)者是RocketMQ中的重要組成部分,消費(fèi)者負(fù)責(zé)從消息隊(duì)列中獲取消息并進(jìn)行處理,需要的朋友可以參考下2023-10-10解決Swagger2返回map復(fù)雜結(jié)構(gòu)不能解析的問(wèn)題
這篇文章主要介紹了解決Swagger2返回map復(fù)雜結(jié)構(gòu)不能解析的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07java集合類遍歷的同時(shí)如何進(jìn)行刪除操作
這篇文章主要介紹了java集合類遍歷的同時(shí)如何進(jìn)行刪除操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09Java實(shí)現(xiàn)線程的暫停和恢復(fù)的示例詳解
這幾天的項(xiàng)目中,客戶給了個(gè)需求,希望我可以開(kāi)啟一個(gè)任務(wù),想什么時(shí)候暫停就什么時(shí)候暫停,想什么時(shí)候開(kāi)始就什么時(shí)候開(kāi)始,所以本文小編給大家介紹了Java實(shí)現(xiàn)線程的暫停和恢復(fù)的示例,需要的朋友可以參考下2023-11-11SpringMVC實(shí)現(xiàn)全局異常處理器的經(jīng)典案例
文章介紹了如何使用@ControllerAdvice和相關(guān)注解實(shí)現(xiàn)SpringMVC的全局異常處理,通過(guò)統(tǒng)一的異常處理類和自定義業(yè)務(wù)異常類,可以將所有控制器的異常集中處理,并以JSON格式返回給前端,感興趣的朋友一起看看吧2025-03-03