Java8中如何通過方法引用獲取屬性名詳解
前言
在我們開發(fā)過程中常常有一個(gè)需求,就是要知道實(shí)體類中Getter方法對(duì)應(yīng)的屬性名稱(Field Name),例如實(shí)體類屬性到數(shù)據(jù)庫字段的映射,我們常常是硬編碼指定 屬性名,這種硬編碼有兩個(gè)缺點(diǎn)。
1、編碼效率低:因?yàn)橐簿幋a寫屬性名,很可能寫錯(cuò),需要非常小心,時(shí)間浪費(fèi)在了不必要的檢查上。
2、容易讓開發(fā)人員踩坑:例如有一天發(fā)現(xiàn)實(shí)體類中Field Name定義的不夠明確,希望換一個(gè)Field Name,那么代碼所有硬編碼的Field Name都要跟著變更,對(duì)于未并更的地方,是無法在編譯期發(fā)現(xiàn)的。只要有未變更的地方都可能導(dǎo)致bug的出現(xiàn)。
而使用了方法引用后,如果Field Name變更及其對(duì)應(yīng)的Getter/Setter方法變更,編譯器便可以實(shí)時(shí)的幫助我們檢查變更的代碼,在編譯器給出錯(cuò)誤信息。
那么如何通過方法引用獲取Getter方法對(duì)應(yīng)的Field Name呢?
Java8中給我們提供了實(shí)現(xiàn)方式,首先要做的就是定義一個(gè)可序列化的函數(shù)式接口(實(shí)現(xiàn)Serializable),實(shí)現(xiàn)如下:
/** * Created by bruce on 2020/4/10 14:16 */ @FunctionalInterface public interface SerializableFunction<T, R> extends Function<T, R>, Serializable { }
而在使用時(shí),我們需要傳遞Getter方法引用
//方法引用 SerializableFunction<People, String> getName1 = People::getName; Field field = ReflectionUtil.getField(getName1);
下面看具體怎么解析這個(gè)SerializableFunction,完整實(shí)現(xiàn)如下ReflectionUtil
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對(duì)應(yīng)的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表達(dá)式,只能使用方法引用"); } else { throw new IllegalArgumentException(implMethodName + "不是Getter方法引用"); } // 第3步 獲取的Class是字符串,并且包名是“/”分割,需要替換成“.”,才能獲取到對(duì)應(yīng)的Class對(duì)象 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步 如果沒有找到對(duì)應(yīng)的字段應(yīng)該拋出異常 if (field != null) { return field; } throw new NoSuchFieldError(fieldName); } }
該類中主要有如下三個(gè)方法
String getFieldName(SerializableFunction<T, R> function)
獲取Field的字符串nameField getField(SerializableFunction<?, ?> function)
從緩存中查詢方法引用對(duì)應(yīng)的Field,如果沒有則通過findField(SerializableFunction<?, ?> function)方法反射獲取Field findField(SerializableFunction<?, ?> function)
反射獲取方法應(yīng)用對(duì)應(yīng)的Field
實(shí)現(xiàn)原理
1、首先我們看最后一個(gè)方法Field findField(SerializableFunction<?, ?> function)
,該方法中第一步是通過SerializableFunction對(duì)象獲取Class,即傳遞的方法引用,然后反射獲取writeReplace()
方法,并調(diào)用該方法獲取導(dǎo)SerializedLambda
對(duì)象。
2、SerializedLambda
是Java8中提供,主要就是用于封裝方法引用所對(duì)應(yīng)的信息,主要的就是方法名、定義方法的類名、創(chuàng)建方法引用所在類。
3、拿到這些信息后,便可以通過反射獲取對(duì)應(yīng)的Field。
4、而在方法Field getField(SerializableFunction<?, ?> function)
中對(duì)獲取到的Field進(jìn)行緩存,避免每次都反射獲取,造成資源浪費(fèi)。
除此之外似乎還有一些值得思考的問題
writeReplace()方法是哪來的呢?
首先簡(jiǎn)單了解一下java.io.Serializable
接口,該接口很常見,我們?cè)诔志没粋€(gè)對(duì)象或者在RPC框架之間通信使用JDK序列化時(shí)都會(huì)讓傳輸?shù)膶?shí)體類實(shí)現(xiàn)該接口,該接口是一個(gè)標(biāo)記接口沒有定義任何方法,但是該接口文檔中有這么一段描述:
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.
概要意思就是說,如果想在序列化時(shí)改變序列化的對(duì)象,可以通過在實(shí)體類中定義任意訪問權(quán)限的Object writeReplace()來改變默認(rèn)序列化的對(duì)象。
那么我們的定義的SerializableFunction中并沒有定義writeReplace()方法,這個(gè)方法是哪來的呢?
代碼中SerializableFunction
,Function
只是一個(gè)接口,但是其在最后必定也是一個(gè)實(shí)現(xiàn)類的實(shí)例對(duì)象,而方法引用其實(shí)是在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建的,當(dāng)代碼執(zhí)行到方法引用時(shí),如People::getName
,最后會(huì)經(jīng)過
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()
值得注意的是,代碼中多次編寫的同一個(gè)方法引用,他們創(chuàng)建的是不同F(xiàn)unction實(shí)現(xiàn)類,即他們的Function實(shí)例對(duì)象也并不是同一個(gè)
我們可以通過如下屬性配置將 動(dòng)態(tài)生成的Class保存到 磁盤上
java8中可以通過硬編碼
System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
jdk11 中只能使用jvm參數(shù)指定,硬編碼無效,原因是模塊化導(dǎo)致的
-Djdk.internal.lambda.dumpProxyClasses=.
示例代碼如下:
動(dòng)態(tài)生成的Class如下:
一個(gè)方法引用創(chuàng)建一個(gè)實(shí)現(xiàn)類,他們是不同的對(duì)象,那么ReflectionUtil中將SerializableFunction最為緩存key還有意義嗎?
答案是肯定有意義的!?。?/strong>因?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
這里的緩存Key應(yīng)該選用SerializableFunction#Class還是SerializableFunction實(shí)例對(duì)象好呢?
看到有些實(shí)現(xiàn)使用SerializableFunction的Class作為緩存key,代碼如下:
public static Field getField(SerializableFunction<?, ?> function) { //使用SerializableFunction的Class作為緩存key,導(dǎo)致每次都調(diào)用function.getClass() return cache.computeIfAbsent(function.getClass(), ReflectionUtil::findField); }
但是個(gè)人建議采用SerializableFunction對(duì)象,因?yàn)闊o論方法被調(diào)用多少次,方法代碼塊內(nèi)的方法引用對(duì)象始終是同一個(gè),如果采用其Class做為緩存key,每次查詢緩存時(shí)都需要調(diào)用native方法function.getClass()
獲取其Class,也是一種資源損耗。
總結(jié):Java如何通過方法引用獲取屬性名實(shí)現(xiàn)及思考至此結(jié)束。直接使用ReflectionUtil即可
到此這篇關(guān)于Java8中如何通過方法引用獲取屬性名的文章就介紹到這了,更多相關(guān)Java8通過方法引用獲取屬性名內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解SpringBoot2.0的@Cacheable(Redis)緩存失效時(shí)間解決方案
這篇文章主要介紹了詳解SpringBoot2.0的@Cacheable(Redis)緩存失效時(shí)間解決方案,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Quarkus的Spring擴(kuò)展快速改造Spring項(xiàng)目
這篇文章主要為大家介紹了Quarkus的Spring項(xiàng)目擴(kuò)展,帶大家快速改造Spring項(xiàng)目示例演繹,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-02-02Springboot配置過濾器實(shí)現(xiàn)過程解析
這篇文章主要介紹了Springboot配置過濾器實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08Java使用線程池實(shí)現(xiàn)socket編程的方法詳解
這篇文章主要為大家詳細(xì)介紹了Java使用線程池實(shí)現(xiàn)socket編程的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03SpringBoot Mybatis如何配置多數(shù)據(jù)源并分包
這篇文章主要介紹了SpringBoot Mybatis如何配置多數(shù)據(jù)源并分包,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05使用@Validated注解進(jìn)行校驗(yàn)卻沒有效果的解決
這篇文章主要介紹了使用@Validated注解進(jìn)行校驗(yàn)卻沒有效果的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-04-04