1. 程式人生 > 程式設計 >Java8中如何通過方法引用獲取屬性名詳解

Java8中如何通過方法引用獲取屬性名詳解

前言

在我們開發過程中常常有一個需求,就是要知道實體類中Getter方法對應的屬性名稱(Field Name),例如實體類屬性到資料庫欄位的對映,我們常常是硬編碼指定 屬性名,這種硬編碼有兩個缺點。

1、編碼效率低:因為要硬編碼寫屬性名,很可能寫錯,需要非常小心,時間浪費在了不必要的檢查上。

2、容易讓開發人員踩坑:例如有一天發現實體類中Field Name定義的不夠明確,希望換一個Field Name,那麼程式碼所有硬編碼的Field Name都要跟著變更,對於未並更的地方,是無法在編譯期發現的。只要有未變更的地方都可能導致bug的出現。

而使用了方法引用後,如果Field Name變更及其對應的Getter/Setter方法變更,編譯器便可以實時的幫助我們檢查變更的程式碼,在編譯器給出錯誤資訊。

那麼如何通過方法引用獲取Getter方法對應的Field Name呢?

Java8中給我們提供了實現方式,首先要做的就是定義一個可序列化的函式式介面(實現Serializable),實現如下:

/**
 * Created by bruce on 2020/4/10 14:16
 */
@FunctionalInterface
public interface SerializableFunction<T,R> extends Function<T,R>,Serializable {
 
}

而在使用時,我們需要傳遞Getter方法引用

 //方法引用
SerializableFunction<People,String> getName1 = People::getName;
Field field = ReflectionUtil.getField(getName1);

下面看具體怎麼解析這個SerializableFunction,完整實現如下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對應的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序列化時都會讓傳輸的實體類實現該介面,該介面是一個標記介面沒有定義任何方法,但是該介面文件中有這麼一段描述:

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()

Java8中如何通過方法引用獲取屬性名詳解

Java8中如何通過方法引用獲取屬性名詳解

Java8中如何通過方法引用獲取屬性名詳解

值得注意的是,程式碼中多次編寫的同一個方法引用,他們建立的是不同Function實現類,即他們的Function例項物件也並不是同一個
我們可以通過如下屬性配置將 動態生成的Class儲存到 磁碟上

java8中可以通過硬編碼

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

jdk11 中只能使用jvm引數指定,硬編碼無效,原因是模組化導致的

-Djdk.internal.lambda.dumpProxyClasses=.

示例程式碼如下:

Java8中如何通過方法引用獲取屬性名詳解

動態生成的Class如下:

Java8中如何通過方法引用獲取屬性名詳解

一個方法引用建立一個實現類,他們是不同的物件,那麼ReflectionUtil中將SerializableFunction最為快取key還有意義嗎?

答案是肯定有意義的!!!因為同一方法中的定義的Function只會動態的建立一次實現類並只例項化一次,當該方法被多次呼叫時即可走快取中查詢該方法引用對應的Field

Java8中如何通過方法引用獲取屬性名詳解

這裡的快取Key應該選用SerializableFunction#Class還是SerializableFunction例項物件好呢?

看到有些實現使用SerializableFunction的Class作為快取key,程式碼如下:

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即可

到此這篇關於Java8中如何通過方法引用獲取屬性名的文章就介紹到這了,更多相關Java8通過方法引用獲取屬性名內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!