通過Lambda函數的方式獲取屬性名稱
前言
最近在使用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
實現步驟
-
定義一個函數式接口, 用來接收lambda方法引用
注意: 函數式接口必須繼承Serializable接口才能獲取方法信息
@FunctionalInterface public interface SFunction<T> extends Serializable { Object apply(T t); } -
定義一個工具類, 用來解析獲取屬性名稱
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這個函數引用


我們再通過代碼驗證一下 剛才的猜想

參考:

浙公網安備 33010602011771號