前言:
最近在使用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;
}
}
執行測試 輸出結果
首先簡單瞭解一下java.io.Serializable
介面,該介面很常見,我們在持久化一個物件或者在RPC框架之間通訊使用JDK序列化時都會讓傳輸的實體類實現該介面,該介面是一個標記介面沒有定義任何方法,但是該介面檔案中有這麼一段描述:
概要意思就是說,如果想在序列化時改變序列化的物件,可以通過在實體類中定義任意存取許可權的Object writeReplace()來改變預設序列化的物件。
程式碼中SFunction
只是一個介面, 但是其在最後必定也是一個實現類的範例物件,而方法參照其實是在執行時動態建立的,當程式碼執行到方法參照時,如User::getName
,最後會經過
java.lang.invoke.LambdaMetafactory
java.lang.invoke.InnerClassLambdaMetafactory
去動態的建立實現類。而在動態建立實現類時則會判斷函數式介面是否實現了Serializable
,如果實現了,則新增writeReplace
方法
也就是說我們程式碼BeanUtils#getSerializedLambda
方法中反射呼叫的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