透過Lambda函式的方式獲取屬性名稱

張鐵牛發表於2023-10-19

前言:

最近在使用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

相關文章