從Mybatis-Plus開始認識SerializedLambda
從Mybatis-Plus開始認識SerializedLambda
背景
對于使用過Mybatis-Plus的Java開發者來說,肯定對以下代碼不陌生:
@TableName("t_user")
@Data
public class User {
private String id;
private String name;
private String password;
private String gender;
private int age;
}
@Mapper
public interface UserDAO extends BaseMapper<User> {
}
@Service
public class UserService {
@Resource
private UserDAO userDAO;
public List<User> getUsersBetween(int minAge, int maxAge) {
return userDAO.selectList(new LambdaQueryWrapper<User>()
.ge(User::getAge, minAge)
.le(User::getAge, maxAge));
}
}
在引入Mybatis-Plus之后,只需要按照上述代碼定義出基礎的DO、DAO和Service,而不用再自己顯式編寫對應的SQL,就能完成大部分常規的CRUD操作。Mybatis-Plus的具體使用方法和實現原理此處不展開,有興趣的讀者可以移步Mybatis-Plus官網了解更多信息。
第一次看到UserService中getUsersBetween()方法的實現時,可能有不少讀者會產生一些疑惑:
User::getAge這是什么語法?- Mybatis-Plus是如何根據這個這個
User::getAge來推測出生成SQL時的列名的?
接下來我們就從這兩個問題入手,來了解Java 8開始引入的SerializedLambda
User::getAge的背后——Lambda表達式和方法引用
Lambda表達式
Lambda表達式是Java 8開始引入的一大新特性,是一個非常有用的語法糖,讓Java開發者也可以體驗一下“函數式”編程的感覺。Lambda表達式主要的功能之一就是簡化了我們創建匿名類的過程,當然,這里的匿名類只能有一個方法。舉個例子,當我們想創建一個線程時,使用匿名類可以這樣處理:
public static void main(String[] args) throws InterruptedException {
//匿名類實現了Runnable接口
Thread thread = new Thread(new Runnable() {
//重寫run方法
@Override
public void run() {
System.out.println("stdout from thread: " + Thread.currentThread().getName());
}
});
thread.start();
thread.join();
}
而使用Lambda表達式則可以簡化為:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> System.out.println("stdout from thread: " + Thread.currentThread().getName()));
thread.start();
thread.join();
}
這就是Lambda表達式最基本的也是最為核心的功能——讓編寫實現只有一個抽象方法的接口的匿名類變得簡單。而這種只有一個抽象方法的接口被稱為函數式接口
只能有一個抽象方法的言外之意是函數式接口可以有其他的非抽象方法,如靜態方法和默認方法
通常函數式接口會使用@FunctionalInterface注解修飾,表示這是一個函數式接口。此注解的作用是讓編譯器檢查被注解的接口是否符合函數式接口的規范,若不符合編譯器會產生對應的錯誤
好奇什么時候會報錯的小伙伴可參考官方文檔描述:
-
If a type is annotated with this annotation type, compilers are required to generate an error message unless:
-
The type is an interface type and not an annotation type, enum, or class.
The annotated type satisfies the requirements of a functional interface
更多Lambda表達式相關的內容可參考官方文檔: Lambda Expression和其他資料。
方法引用
有時我們編寫的Lambda表達式僅僅是簡單地調用了一個方法,而沒有進行其他操作,這時候就可以再一次進行簡化,甚至連Lambda表達式都不用寫了,直接寫被調用的方法引用就行了。 依舊以創建一個線程為例:
public class Main {
public static void main(String[] args) throws InterruptedException {
//這里Lambda表達式只有一個作用,就是調用別的方法來處理任務
Thread thread = new Thread(() -> sayHello());
thread.start();
thread.join();
}
public static void sayHello() {
System.out.println("stdout from thread: " + Thread.currentThread().getName());
}
}
對于上述代碼,似乎設計者認為() -> sayHello()這個表達式都有點多余,所以引入了方法引用,可以將上述代碼簡化為:
public class Main {
public static void main(String[] args) throws InterruptedException {
//Main::sayHello即是方法引用的寫法
Thread thread = new Thread(Main::sayHello);
thread.start();
thread.join();
}
public static void sayHello() {
System.out.println("stdout from thread: " + Thread.currentThread().getName());
}
}
按官方文檔的說法就是,這種形式更加緊湊,可讀性更高。用文檔的原話就是:
You use lambda expressions to create anonymous methods. Sometimes, however, a lambda expression does nothing but call an existing method. In those cases, it's often clearer to refer to the existing method by name. Method references enable you to do this; they are compact, easy-to-read lambda expressions for methods that already have a name.
這里有個小細節,最后一句話提到they are compact, easy-to-read lambda expressions...也正好給方法引用定了性,即方法引用本身還是一種Lambda表達式,只是形式比較特殊罷了
回到主題,說到這里,相信讀者也就明白了,User::getAge不過就是一個方法引用罷了,而更本質一點,也不過就是一個Lambda表達式而已,而其語義可以理解為它指向了User類中的getAge方法
說明白了User::getAge是何物之后,接下來就該看看Mybatis-Plus是如何使用它的了
Mybatis-Plus是怎么利用方法引用的?
通過源碼跟蹤,會發現Mybatis-Plus中有一個名為AbstractLambdaWrapper的類,其中有一個名為columnToString()的方法,其作用就是通過Getter提取出列名。其實現如下:
//Mybatis-Plus中將Getter轉換為列名的方法。參數column即為對應要解析的Getter的方法引用
protected String columnToString(SFunction<T, ?> column) {
return this.columnToString(column, true);
}
protected String columnToString(SFunction<T, ?> column, boolean onlyColumn) {
ColumnCache cache = this.getColumnCache(column);
return onlyColumn ? cache.getColumn() : cache.getColumnSelect();
}
columnToString()僅是一個入口,具體邏輯則是在同類的getColumnCache()方法中:
protected ColumnCache getColumnCache(SFunction<T, ?> column) {
//從Getter方法引用中提取元數據。元數據中就包含了Getter的方法名
LambdaMeta meta = LambdaUtils.extract(column);
//從Getter方法名中截取字段名
String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName());
//下邊是Mybatis-Plus緩存相關的邏輯,可忽略
Class<?> instantiatedClass = meta.getInstantiatedClass();
this.tryInitCache(instantiatedClass);
return this.getColumnCache(fieldName, instantiatedClass);
}
從上述代碼中可知,從Getter方法引用中提取Getter方法的具體名稱的邏輯是在LambdaUtils.extract()中完成的,再來看看這個方法的實現:
public static <T> LambdaMeta extract(SFunction<T, ?> func) {
if (func instanceof Proxy) {
//從IDEA代理對象獲取,這個邏輯不重要,可以忽略掉
return new IdeaProxyLambdaMeta((Proxy)func);
} else {
try {
//重點在這里,通過反射從方法引用(Lambda表達式)中找到'writeReplace'方法
Method method = func.getClass().getDeclaredMethod("writeReplace");
method.setAccessible(true);
//反射調用writeReplace方法,將結果強制轉型為 SerializedLambda
return new ReflectLambdaMeta((SerializedLambda)method.invoke(func), func.getClass().getClassLoader());
} catch (Throwable var2) {
return new ShadowLambdaMeta(com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda.extract(func));
}
}
}
在LambdaUtils.extract()中,通過對Lambda表達式進行反射查找一個名為writeReplace()的方法并調用,最終得到的結果強制轉型為SerializedLambda類型。這就是通過方法引用得到方法具體名稱的最主要的步驟
在LambdaUtils.extract()執行完成后得到一個LambdaMeta對象,這個對象中封裝了Lambda表達式(在這里就是某個Getter的方法引用)的元數據,其中的getImplMethodName()方法的實現本質就是調用了SerializedLambda的同名方法:
public class ReflectLambdaMeta implements LambdaMeta {
...
private final SerializedLambda lambda;
...
public String getImplMethodName() {
return this.lambda.getImplMethodName();
}
...
}
再來看調用LambdaUtils.extract()后getColumnCache()函數中的代碼:
String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName());
這里調用上邊提到的getImplMethodName()方法,最終得到的就是某個方法引用對應的方法名稱,然后通過methodToProperty()再將方法名稱轉換為字段名稱:
//邏輯比較簡單,就是按照Getter的命名規則
//將getXXX 或 isXXX 的get和is前綴給拿掉,剩下的XXX就是屬性名
public static String methodToProperty(String name) {
if (name.startsWith("is")) {
name = name.substring(2);
} else {
if (!name.startsWith("get") && !name.startsWith("set")) {
throw new ReflectionException("Error parsing property name '" + name + "'. Didn't start with 'is', 'get' or 'set'.");
}
name = name.substring(3);
}
if (name.length() == 1 || name.length() > 1 && !Character.isUpperCase(name.charAt(1))) {
name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
}
return name;
}
到這里,第二個問題,Mybaits-Plus是如何將User::getAge轉換成對應列名的邏輯也就清晰了:
- Mybatis-Plus的
AbstractLambdaWrapper中columnToString(User::getAge)負責得到字符串形式的列名 columnToString(User::getAge)則是調用getColumnCache(User::getAge)方法來提取列名getColumnCache(User::getAge)中使用LambdaUtils.extract(User::getAge)來反射獲取User::getAge這個方法引用(Lambda表達式)的元數據。(核心是得到SerializedLambda對象)- 通過
SerializedLambda的getImplMethodName()方法得到了方法引用的具體名稱
注意,SerializedLambda類是JDK的,不是Mybatis-Plus的
- 得到方法名稱后,再通過
methodToProperty()從方法名獲取字段名,這一步主要是剔掉is或者get前綴
從這里也能看出來,符合標準Getter命名規范的才能被解析,即遵循getXXX / isXXX格式
最后補充一點,這只是將User::getAge這種方法引用最終轉為"age"這樣的屬性名的邏輯。Mybatis-Plus中后續還有一些注解可以控制列名的映射,這里暫不討論
SerializedLambda
通過前面的鋪墊,終于到了介紹本文的主角——SerializedLambda的時刻了
那什么是SerializedLambda?SerializedLambda顧名思義就是序列化后的Lambda。這個類中記錄了Lambda表達式的上下文信息,主要包括:
- 捕獲類信息(
capturingClass):即這個Lambda表達式是在哪個類中用到的 - 函數接口類(
functionalInterfaceClass):函數接口類路徑 - 函數接口的方法名(
functionalInterfaceMethodName):函數接口中抽象方法的名稱 - 函數接口方法簽名(
functionalInterfaceMethodSignature):函數接口中抽象方法的簽名 - 實現類(
implClass):哪個類實現了此函數接口 - 實現方法名(
implMethodName):實現此函數接口對應的方法名 - 實現方法的簽名(
implMethodSignature):實現此函數接口對應的方法的簽名 - 實現方法類型(
implMethodKind):getStatic/invokeVirtual/invokeStatic等調用類型 - 捕獲的參數(
capturedArgs):Lambda表達式可能會用到外部變量,這里記錄捕獲到的變量
從SerializedLambda包含的信息可知,我們可以通過這個類型的對象拿到關于Lambda表達式的一些基礎信息。而Mybatis-Plus正是利用了這一點,其拿到了某個Getter的方法引用(一定記住方法引用也是一種Lambda),然后調用writeReplace()方法得到關于該方法引用的SerializedLambda對象,這個對象就包含了這個方法引用的描述信息,其中就包含了這個方法引用對應方法的名稱(implMethodName)
總的來說,SerializedLambda可以理解為Lambda表達式的序列化形式,而序列化主要就是將內存對象的關鍵屬性提出來轉化為可傳輸和可持久化的形式,我們可以通過序列化后的結果大致了解到該對象的結構。SerializedLambda的一大作用正是如此,我們可以通過它來了解到原始Lambda表達式大概是由哪些關鍵因素構成的
無中生有的writeReplace方法
在前文獲取SerializedLambda對象時有這么幾行代碼:
...
func.getClass().getDeclaredMethod("writeReplace");
method.setAccessible(true);
(SerializedLambda)method.invoke(func);
...
這是典型的反射調用代碼,反射這里就不多展開說了。可能很多人關心的是,這個writeReplace()方法從何而來?有何用處?
writeReplace()并非專為SerializedLambda而設計,這個方法其實是Java的序列化機制自帶的一個擴展點,任何需要被序列化的類,可以在類中聲明這個方法來控制序列化此類對象時使用的替代對象。這樣說起來可能有點繞,下邊我們來看一個簡單的示例:
假設有一個User類,定義如下:
@Data
public class User implements Serializable {
private String id;
private String name;
private String password;
private String gender;
private int age;
//聲明writeReplace方法
public Object writeReplace() throws ObjectStreamException {
System.out.println("User's writeReplace() is been called.");
return "user";
}
}
接下來使用ObjectOutputStream來序列化User對象:
public static void main(String[] args) throws Exception {
User user = new User();
user.setName("longqinx");
ObjectOutputStream out = new ObjectOutputStream(new ByteArrayOutputStream());
out.writeObject(user);
}
執行上述代碼后可以看到控制臺輸出了User's writeReplace() is been called.,證明我們在User類中聲明的writeReplace方法確實被調用了
通過上述示例,我們可以得到初步的結論:writeReplace()方法是一個Java內部約定的方法,其作用是在序列化某個類型對象的時候,允許我們自定義一個替代對象去序列化。比如上述示例中序列化User對象時,我們使用一個String對象作為代替品。如果類中定義了此方法,則序列化時會自動調用,反之按常規序列化邏輯進行序列化
注意,這里的序列化指的是使用Java自身的序列化機制完成的序列化,而不是使用Jackson這種序列化框架
回到正題,編譯器會Lambda表達式類型自動生成一個writeReplace()方法,該方法返回一個SerializedLambda作為真正序列化的對象,以此保證對Lambda表達式的正確序列化
而我們則可以利用這一性質,主動反射調用writeReplace()方法來獲取SerializedLambda對象,從而得到Lambda表達式的一些元數據,有了這些元數據我們就能發揮創意做一些更有趣的東西
實戰——實現一個根據Getter方法引用獲取字段名的工具類
1. 定義函數接口
@FunctionalInterface
public interface Getter<T,R> extends Serializable {
R get(T t);
}
- 注意,這里必須要繼承自
Serializable接口,不然編譯器不會為對應的Lambda表達式生成writeReplace()方法,也就無法獲取到SerializedLambda對象
2. 實現工具類
public class FieldNameExtractor {
/**
* 從Getter方法引用提取字段名
*
* @param getter 方法引用,必須是getter的
* @return 字段名
*/
public static <T, R> String extractFieldNameFromGetter(Getter<T, R> getter) {
try {
//反射獲取writeReplace方法
Method writeReplace = getter.getClass().getDeclaredMethod("writeReplace");
writeReplace.setAccessible(true);
//調用writeReplace方法
SerializedLambda serializedLambda = (SerializedLambda) writeReplace.invoke(getter);
//獲取實現方法,也就是方法引用對應的方法名
String methodName = serializedLambda.getImplMethodName();
return extractFieldName(methodName);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private static String extractFieldName(String methodName) {
String fieldName;
if (methodName.startsWith("is")) {
fieldName = methodName.substring(2);
} else if (methodName.startsWith("get")) {
fieldName = methodName.substring(3);
} else {
throw new IllegalArgumentException("method name should start with 'is' or 'get'");
}
return Character.toLowerCase(fieldName.charAt(0)) + fieldName.substring(1);
}
}
3. 測試
public class Main {
public static void main(String[] args) throws Exception {
//輸出name
System.out.println(FieldNameExtractor.extractFieldNameFromGetter(User::getName));
//輸出age
System.out.println(FieldNameExtractor.extractFieldNameFromGetter(User::getAge));
}
}
函數接口定義解惑
讀者在看到上述示例代碼后,可能存在疑惑,為何Getter這個函數式接口要這樣定義,為什么有兩個泛型參數T和R?
其實只用一個泛型參數即可,這時候應該這樣定義:
@FunctionalInterface
public interface InstanceGetter<R> extends Serializable {
R get();
}
工具類中實現邏輯不變,只是調整參數類型即可:
//參數改為InstanceGetter類型,其他不變
public static <R> String extractFieldNameFromGetter(InstanceGetter<R> getter) {
try {
//反射獲取writeReplace方法
Method writeReplace = getter.getClass().getDeclaredMethod("writeReplace");
writeReplace.setAccessible(true);
//調用writeReplace方法
SerializedLambda serializedLambda = (SerializedLambda) writeReplace.invoke(getter);
//獲取實現方法,也就是方法引用對應的方法名
String methodName = serializedLambda.getImplMethodName();
return extractFieldName(methodName);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
但在使用的時候,傳遞參數時就不能用User::getName或User::getAge這樣的形式了,而應該先實例化User對象,用實例方法引用:
public class Main {
public static void main(String[] args) throws Exception {
User user = new User();
//注意這里是 user::getName而不是User::getName,是用user這個實例來得到方法引用
System.out.println(FieldNameExtractor.extractFieldNameFromGetter(user::getName));
System.out.println(FieldNameExtractor.extractFieldNameFromGetter(user::getAge));
}
}
相信看了這兩個對比之后讀者也就能察覺到其中的不同了:User::getName是通過類名引用的,而user::getName是通過實例對象引用的
前者真正要被調用時,還得知道在哪個對象上調用(類似反射的invoke),所以會有一個泛型參數 T 來表示對象的類型,而R則是Getter的返回值類型;
后者則是通過實例對象得到的方法引用,這時候Lambda能捕獲到這個實例對象,因此在調用時自然也知道該在哪個對象上調用,此時就可以省去 T 這個泛型參數了
總結
回答一開始的問題
User::getAge這是什么語法?
Java 8開始引入Lambda表達式和方法引用的概念,User::getAge這種寫法稱為方法引用,其本質上也是一種Lambda表達式
- Mybatis-Plus是如何根據這個這個
User::getAge來推測出生成SQL時的列名的?
Java中有個SerializedLambda類,其用于表示序列化后的Lambda表達式,通過此類可以獲取方法名、實現類名等眾多關于Lambda表達式的元數據。對于一個可序列化的Lambda表達式,可通過反射調用其writeReplace方法獲取關聯的SerializedLambda對象。
當對User::getAge這個Lambda表達式執行此操作時,得到的SerializedLambda中就包含了User類中getAge()這個方法的名稱、簽名等信息。此時通過getter命名規范,去掉is或get前綴,并將首字符小寫即可得到字段名
其他一些沒有提到的
在筆者實際的研究過程中,充分利用了IDEA進行調試,但限于篇幅,這個過程并未在本文中詳細描述。感興趣的讀者可以自己動手去認真調試一番。這里給幾個思路:
- 在寫函數式接口時,試一試繼承
Serializable和不繼承時反射調用writeReplace()方法的結果 - 拿到一個Lambda表達式對象,嘗試反射一下其中有哪些方法
- 反射一下使用了Lambda表達式的類,看看有什么特別之處
- 獲取一個Lambda表達式關聯的
SerializedLambda對象,看看里邊存了些什么

浙公網安備 33010602011771號