前言:
最近在使用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
這個函式引用
我們再透過程式碼驗證一下 剛才的猜想
參考: