從Mybatis-Plus開始認(rèn)識(shí)SerializedLambda的詳細(xì)過(guò)程
從Mybatis-Plus開始認(rèn)識(shí)SerializedLambda
背景
對(duì)于使用過(guò)Mybatis-Plus的Java開發(fā)者來(lái)說(shuō),肯定對(duì)以下代碼不陌生:
@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之后,只需要按照上述代碼定義出基礎(chǔ)的DO、DAO和Service,而不用再自己顯式編寫對(duì)應(yīng)的SQL,就能完成大部分常規(guī)的CRUD操作。Mybatis-Plus的具體使用方法和實(shí)現(xiàn)原理此處不展開,有興趣的讀者可以移步Mybatis-Plus官網(wǎng)了解更多信息。
第一次看到UserService
中getUsersBetween()
方法的實(shí)現(xiàn)時(shí),可能有不少讀者會(huì)產(chǎn)生一些疑惑:
User::getAge
這是什么語(yǔ)法?- Mybatis-Plus是如何根據(jù)這個(gè)這個(gè)
User::getAge
來(lái)推測(cè)出生成SQL時(shí)的列名的?
接下來(lái)我們就從這兩個(gè)問(wèn)題入手,來(lái)了解Java 8開始引入的SerializedLambda
User::getAge
的背后——Lambda表達(dá)式和方法引用
Lambda表達(dá)式
Lambda表達(dá)式是Java 8開始引入的一大新特性,是一個(gè)非常有用的語(yǔ)法糖,讓Java開發(fā)者也可以體驗(yàn)一下“函數(shù)式”編程的感覺(jué)。Lambda表達(dá)式主要的功能之一就是簡(jiǎn)化了我們創(chuàng)建匿名類的過(guò)程,當(dāng)然,這里的匿名類只能有一個(gè)方法。舉個(gè)例子,當(dāng)我們想創(chuàng)建一個(gè)線程時(shí),使用匿名類可以這樣處理:
@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)); } }
而使用Lambda表達(dá)式則可以簡(jiǎn)化為:
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表達(dá)式最基本的也是最為核心的功能——讓編寫實(shí)現(xiàn)只有一個(gè)抽象方法的接口的匿名類變得簡(jiǎn)單。而這種只有一個(gè)抽象方法的接口被稱為函數(shù)式接口
只能有一個(gè)抽象方法的言外之意是函數(shù)式接口可以有其他的非抽象方法,如靜態(tài)方法和默認(rèn)方法
通常函數(shù)式接口會(huì)使用@FunctionalInterface
注解修飾,表示這是一個(gè)函數(shù)式接口。此注解的作用是讓編譯器檢查被注解的接口是否符合函數(shù)式接口的規(guī)范,若不符合編譯器會(huì)產(chǎn)生對(duì)應(yīng)的錯(cuò)誤
好奇什么時(shí)候會(huì)報(bào)錯(cuò)的小伙伴可參考官方文檔描述:
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表達(dá)式相關(guān)的內(nèi)容可參考官方文檔: Lambda Expression和其他資料。
方法引用
有時(shí)我們編寫的Lambda表達(dá)式僅僅是簡(jiǎn)單地調(diào)用了一個(gè)方法,而沒(méi)有進(jìn)行其他操作,這時(shí)候就可以再一次進(jìn)行簡(jiǎn)化,甚至連Lambda表達(dá)式都不用寫了,直接寫被調(diào)用的方法引用就行了。 依舊以創(chuàng)建一個(gè)線程為例:
public class Main { public static void main(String[] args) throws InterruptedException { //這里L(fēng)ambda表達(dá)式只有一個(gè)作用,就是調(diào)用別的方法來(lái)處理任務(wù) Thread thread = new Thread(() -> sayHello()); thread.start(); thread.join(); } public static void sayHello() { System.out.println("stdout from thread: " + Thread.currentThread().getName()); } }
對(duì)于上述代碼,似乎設(shè)計(jì)者認(rèn)為() -> sayHello()
這個(gè)表達(dá)式都有點(diǎn)多余,所以引入了方法引用,可以將上述代碼簡(jiǎn)化為:
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()); } }
按官方文檔的說(shuō)法就是,這種形式更加緊湊,可讀性更高。用文檔的原話就是:
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.
這里有個(gè)小細(xì)節(jié),最后一句話提到they are compact, easy-to-read lambda expressions...
也正好給方法引用定了性,即方法引用本身還是一種Lambda表達(dá)式,只是形式比較特殊罷了
回到主題,說(shuō)到這里,相信讀者也就明白了,User::getAge
不過(guò)就是一個(gè)方法引用罷了,而更本質(zhì)一點(diǎn),也不過(guò)就是一個(gè)Lambda表達(dá)式而已,而其語(yǔ)義可以理解為它指向了User
類中的getAge
方法
說(shuō)明白了User::getAge
是何物之后,接下來(lái)就該看看Mybatis-Plus是如何使用它的了
Mybatis-Plus是怎么利用方法引用的?
通過(guò)源碼跟蹤,會(huì)發(fā)現(xiàn)Mybatis-Plus中有一個(gè)名為AbstractLambdaWrapper
的類,其中有一個(gè)名為columnToString()
的方法,其作用就是通過(guò)Getter提取出列名。其實(shí)現(xiàn)如下:
//Mybatis-Plus中將Getter轉(zhuǎn)換為列名的方法。參數(shù)column即為對(duì)應(yīng)要解析的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()
僅是一個(gè)入口,具體邏輯則是在同類的getColumnCache()
方法中:
protected ColumnCache getColumnCache(SFunction<T, ?> column) { //從Getter方法引用中提取元數(shù)據(jù)。元數(shù)據(jù)中就包含了Getter的方法名 LambdaMeta meta = LambdaUtils.extract(column); //從Getter方法名中截取字段名 String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName()); //下邊是Mybatis-Plus緩存相關(guān)的邏輯,可忽略 Class<?> instantiatedClass = meta.getInstantiatedClass(); this.tryInitCache(instantiatedClass); return this.getColumnCache(fieldName, instantiatedClass); }
從上述代碼中可知,從Getter
方法引用中提取Getter
方法的具體名稱的邏輯是在LambdaUtils.extract()
中完成的,再來(lái)看看這個(gè)方法的實(shí)現(xiàn):
public static <T> LambdaMeta extract(SFunction<T, ?> func) { if (func instanceof Proxy) { //從IDEA代理對(duì)象獲取,這個(gè)邏輯不重要,可以忽略掉 return new IdeaProxyLambdaMeta((Proxy)func); } else { try { //重點(diǎn)在這里,通過(guò)反射從方法引用(Lambda表達(dá)式)中找到'writeReplace'方法 Method method = func.getClass().getDeclaredMethod("writeReplace"); method.setAccessible(true); //反射調(diào)用writeReplace方法,將結(jié)果強(qiáng)制轉(zhuǎn)型為 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()
中,通過(guò)對(duì)Lambda表達(dá)式進(jìn)行反射查找一個(gè)名為writeReplace()
的方法并調(diào)用,最終得到的結(jié)果強(qiáng)制轉(zhuǎn)型為SerializedLambda
類型。這就是通過(guò)方法引用得到方法具體名稱的最主要的步驟
在LambdaUtils.extract()
執(zhí)行完成后得到一個(gè)LambdaMeta
對(duì)象,這個(gè)對(duì)象中封裝了Lambda表達(dá)式(在這里就是某個(gè)Getter的方法引用)的元數(shù)據(jù),其中的getImplMethodName()
方法的實(shí)現(xiàn)本質(zhì)就是調(diào)用了SerializedLambda
的同名方法:
public class ReflectLambdaMeta implements LambdaMeta { ... private final SerializedLambda lambda; ... public String getImplMethodName() { return this.lambda.getImplMethodName(); } ... }
再來(lái)看調(diào)用LambdaUtils.extract()
后getColumnCache()
函數(shù)中的代碼:
String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName());
這里調(diào)用上邊提到的getImplMethodName()
方法,最終得到的就是某個(gè)方法引用對(duì)應(yīng)的方法名稱,然后通過(guò)methodToProperty()
再將方法名稱轉(zhuǎn)換為字段名稱:
//邏輯比較簡(jiǎn)單,就是按照Getter的命名規(guī)則 //將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; }
到這里,第二個(gè)問(wèn)題,Mybaits-Plus是如何將User::getAge
轉(zhuǎn)換成對(duì)應(yīng)列名的邏輯也就清晰了:
- Mybatis-Plus的
AbstractLambdaWrapper
中columnToString(User::getAge)
負(fù)責(zé)得到字符串形式的列名 columnToString(User::getAge)
則是調(diào)用getColumnCache(User::getAge)
方法來(lái)提取列名getColumnCache(User::getAge)
中使用LambdaUtils.extract(User::getAge)
來(lái)反射獲取User::getAge
這個(gè)方法引用(Lambda表達(dá)式)的元數(shù)據(jù)。(核心是得到SerializedLambda
對(duì)象)- 通過(guò)
SerializedLambda
的getImplMethodName()
方法得到了方法引用的具體名稱
注意,SerializedLambda類是JDK的,不是Mybatis-Plus的
- 得到方法名稱后,再通過(guò)
methodToProperty()
從方法名獲取字段名,這一步主要是剔掉is
或者get
前綴
從這里也能看出來(lái),符合標(biāo)準(zhǔn)Getter命名規(guī)范的才能被解析,即遵循getXXX / isXXX格式
最后補(bǔ)充一點(diǎn),這只是將User::getAge
這種方法引用最終轉(zhuǎn)為"age"
這樣的屬性名的邏輯。Mybatis-Plus中后續(xù)還有一些注解可以控制列名的映射,這里暫不討論
SerializedLambda
通過(guò)前面的鋪墊,終于到了介紹本文的主角——SerializedLambda
的時(shí)刻了
那什么是SerializedLambda
?SerializedLambda
顧名思義就是序列化后的Lambda。這個(gè)類中記錄了Lambda表達(dá)式的上下文信息,主要包括:
- 捕獲類信息(
capturingClass
):即這個(gè)Lambda表達(dá)式是在哪個(gè)類中用到的 - 函數(shù)接口類(
functionalInterfaceClass
):函數(shù)接口類路徑 - 函數(shù)接口的方法名(
functionalInterfaceMethodName
):函數(shù)接口中抽象方法的名稱 - 函數(shù)接口方法簽名(
functionalInterfaceMethodSignature
):函數(shù)接口中抽象方法的簽名 - 實(shí)現(xiàn)類(
implClass
):哪個(gè)類實(shí)現(xiàn)了此函數(shù)接口 - 實(shí)現(xiàn)方法名(
implMethodName
):實(shí)現(xiàn)此函數(shù)接口對(duì)應(yīng)的方法名 - 實(shí)現(xiàn)方法的簽名(
implMethodSignature
):實(shí)現(xiàn)此函數(shù)接口對(duì)應(yīng)的方法的簽名 - 實(shí)現(xiàn)方法類型(
implMethodKind
):getStatic/invokeVirtual/invokeStatic
等調(diào)用類型 - 捕獲的參數(shù)(
capturedArgs
):Lambda表達(dá)式可能會(huì)用到外部變量,這里記錄捕獲到的變量
從SerializedLambda
包含的信息可知,我們可以通過(guò)這個(gè)類型的對(duì)象拿到關(guān)于Lambda表達(dá)式的一些基礎(chǔ)信息。而Mybatis-Plus正是利用了這一點(diǎn),其拿到了某個(gè)Getter的方法引用(一定記住方法引用也是一種Lambda),然后調(diào)用writeReplace()方法得到關(guān)于該方法引用的SerializedLambda
對(duì)象,這個(gè)對(duì)象就包含了這個(gè)方法引用的描述信息,其中就包含了這個(gè)方法引用對(duì)應(yīng)方法的名稱(implMethodName
)
總的來(lái)說(shuō),SerializedLambda
可以理解為Lambda表達(dá)式的序列化形式,而序列化主要就是將內(nèi)存對(duì)象的關(guān)鍵屬性提出來(lái)轉(zhuǎn)化為可傳輸和可持久化的形式,我們可以通過(guò)序列化后的結(jié)果大致了解到該對(duì)象的結(jié)構(gòu)。SerializedLambda
的一大作用正是如此,我們可以通過(guò)它來(lái)了解到原始Lambda表達(dá)式大概是由哪些關(guān)鍵因素構(gòu)成的
無(wú)中生有的writeReplace方法
在前文獲取SerializedLambda
對(duì)象時(shí)有這么幾行代碼:
... func.getClass().getDeclaredMethod("writeReplace"); method.setAccessible(true); (SerializedLambda)method.invoke(func); ...
這是典型的反射調(diào)用代碼,反射這里就不多展開說(shuō)了??赡芎芏嗳岁P(guān)心的是,這個(gè)writeReplace()
方法從何而來(lái)?有何用處?
writeReplace()
并非專為SerializedLambda
而設(shè)計(jì),這個(gè)方法其實(shí)是Java的序列化機(jī)制自帶的一個(gè)擴(kuò)展點(diǎn),任何需要被序列化的類,可以在類中聲明這個(gè)方法來(lái)控制序列化此類對(duì)象時(shí)使用的替代對(duì)象。這樣說(shuō)起來(lái)可能有點(diǎn)繞,下邊我們來(lái)看一個(gè)簡(jiǎn)單的示例:
假設(shè)有一個(gè)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"; } }
接下來(lái)使用ObjectOutputStream
來(lái)序列化User
對(duì)象:
public static void main(String[] args) throws Exception { User user = new User(); user.setName("longqinx"); ObjectOutputStream out = new ObjectOutputStream(new ByteArrayOutputStream()); out.writeObject(user); }
執(zhí)行上述代碼后可以看到控制臺(tái)輸出了User's writeReplace() is been called.
,證明我們?cè)赨ser類中聲明的writeReplace
方法確實(shí)被調(diào)用了
通過(guò)上述示例,我們可以得到初步的結(jié)論:writeReplace()
方法是一個(gè)Java內(nèi)部約定的方法,其作用是在序列化某個(gè)類型對(duì)象的時(shí)候,允許我們自定義一個(gè)替代對(duì)象去序列化。比如上述示例中序列化User對(duì)象時(shí),我們使用一個(gè)String對(duì)象作為代替品。如果類中定義了此方法,則序列化時(shí)會(huì)自動(dòng)調(diào)用,反之按常規(guī)序列化邏輯進(jìn)行序列化
注意,這里的序列化指的是使用Java自身的序列化機(jī)制完成的序列化,而不是使用Jackson這種序列化框架
回到正題,編譯器會(huì)Lambda表達(dá)式類型自動(dòng)生成一個(gè)writeReplace()
方法,該方法返回一個(gè)SerializedLambda
作為真正序列化的對(duì)象,以此保證對(duì)Lambda表達(dá)式的正確序列化
而我們則可以利用這一性質(zhì),主動(dòng)反射調(diào)用writeReplace()
方法來(lái)獲取SerializedLambda
對(duì)象,從而得到Lambda表達(dá)式的一些元數(shù)據(jù),有了這些元數(shù)據(jù)我們就能發(fā)揮創(chuàng)意做一些更有趣的東西
實(shí)戰(zhàn)——實(shí)現(xiàn)一個(gè)根據(jù)Getter方法引用獲取字段名的工具類
1. 定義函數(shù)接口
@FunctionalInterface public interface Getter<T,R> extends Serializable { R get(T t); }
- 注意,這里必須要繼承自
Serializable
接口,不然編譯器不會(huì)為對(duì)應(yīng)的Lambda表達(dá)式生成writeReplace()
方法,也就無(wú)法獲取到SerializedLambda
對(duì)象
2. 實(shí)現(xiàn)工具類
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); //調(diào)用writeReplace方法 SerializedLambda serializedLambda = (SerializedLambda) writeReplace.invoke(getter); //獲取實(shí)現(xiàn)方法,也就是方法引用對(duì)應(yīng)的方法名 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. 測(cè)試
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)); } }
函數(shù)接口定義解惑
讀者在看到上述示例代碼后,可能存在疑惑,為何Getter這個(gè)函數(shù)式接口要這樣定義,為什么有兩個(gè)泛型參數(shù)T
和R
?
其實(shí)只用一個(gè)泛型參數(shù)即可,這時(shí)候應(yīng)該這樣定義:
@FunctionalInterface public interface InstanceGetter<R> extends Serializable { R get(); }
工具類中實(shí)現(xiàn)邏輯不變,只是調(diào)整參數(shù)類型即可:
//參數(shù)改為InstanceGetter類型,其他不變 public static <R> String extractFieldNameFromGetter(InstanceGetter<R> getter) { try { //反射獲取writeReplace方法 Method writeReplace = getter.getClass().getDeclaredMethod("writeReplace"); writeReplace.setAccessible(true); //調(diào)用writeReplace方法 SerializedLambda serializedLambda = (SerializedLambda) writeReplace.invoke(getter); //獲取實(shí)現(xiàn)方法,也就是方法引用對(duì)應(yīng)的方法名 String methodName = serializedLambda.getImplMethodName(); return extractFieldName(methodName); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { throw new RuntimeException(e); } }
但在使用的時(shí)候,傳遞參數(shù)時(shí)就不能用User::getName或User::getAge
這樣的形式了,而應(yīng)該先實(shí)例化User
對(duì)象,用實(shí)例方法引用:
public class Main { public static void main(String[] args) throws Exception { User user = new User(); //注意這里是 user::getName而不是User::getName,是用user這個(gè)實(shí)例來(lái)得到方法引用 System.out.println(FieldNameExtractor.extractFieldNameFromGetter(user::getName)); System.out.println(FieldNameExtractor.extractFieldNameFromGetter(user::getAge)); } }
相信看了這兩個(gè)對(duì)比之后讀者也就能察覺(jué)到其中的不同了:User::getName
是通過(guò)類名引用的,而user::getName
是通過(guò)實(shí)例對(duì)象引用的
前者真正要被調(diào)用時(shí),還得知道在哪個(gè)對(duì)象上調(diào)用(類似反射的invoke
),所以會(huì)有一個(gè)泛型參數(shù) T
來(lái)表示對(duì)象的類型,而R
則是Getter
的返回值類型;
后者則是通過(guò)實(shí)例對(duì)象得到的方法引用,這時(shí)候Lambda能捕獲到這個(gè)實(shí)例對(duì)象,因此在調(diào)用時(shí)自然也知道該在哪個(gè)對(duì)象上調(diào)用,此時(shí)就可以省去 T
這個(gè)泛型參數(shù)了
總結(jié)
回答一開始的問(wèn)題
User::getAge
這是什么語(yǔ)法?
Java 8開始引入Lambda表達(dá)式和方法引用的概念,User::getAge這種寫法稱為方法引用,其本質(zhì)上也是一種Lambda表達(dá)式
- Mybatis-Plus是如何根據(jù)這個(gè)這個(gè)
User::getAge
來(lái)推測(cè)出生成SQL時(shí)的列名的?
Java中有個(gè)SerializedLambda類,其用于表示序列化后的Lambda表達(dá)式,通過(guò)此類可以獲取方法名、實(shí)現(xiàn)類名等眾多關(guān)于Lambda表達(dá)式的元數(shù)據(jù)。對(duì)于一個(gè)可序列化的Lambda表達(dá)式,可通過(guò)反射調(diào)用其writeReplace方法獲取關(guān)聯(lián)的SerializedLambda對(duì)象。
當(dāng)對(duì)User::getAge這個(gè)Lambda表達(dá)式執(zhí)行此操作時(shí),得到的SerializedLambda中就包含了User類中g(shù)etAge()這個(gè)方法的名稱、簽名等信息。此時(shí)通過(guò)getter命名規(guī)范,去掉is或get前綴,并將首字符小寫即可得到字段名
其他一些沒(méi)有提到的
在筆者實(shí)際的研究過(guò)程中,充分利用了IDEA進(jìn)行調(diào)試,但限于篇幅,這個(gè)過(guò)程并未在本文中詳細(xì)描述。感興趣的讀者可以自己動(dòng)手去認(rèn)真調(diào)試一番。這里給幾個(gè)思路:
- 在寫函數(shù)式接口時(shí),試一試?yán)^承
Serializable
和不繼承時(shí)反射調(diào)用writeReplace()
方法的結(jié)果 - 拿到一個(gè)Lambda表達(dá)式對(duì)象,嘗試反射一下其中有哪些方法
- 反射一下使用了Lambda表達(dá)式的類,看看有什么特別之處
- 獲取一個(gè)Lambda表達(dá)式關(guān)聯(lián)的
SerializedLambda
對(duì)象,看看里邊存了些什么
到此這篇關(guān)于從Mybatis-Plus開始認(rèn)識(shí)SerializedLambda的文章就介紹到這了,更多相關(guān)Mybatis-Plus SerializedLambda內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
深入淺析 Spring Security 緩存請(qǐng)求問(wèn)題
這篇文章主要介紹了 Spring Security 緩存請(qǐng)求問(wèn)題,本文通過(guò)實(shí)例文字相結(jié)合的形式給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2019-04-04maven項(xiàng)目打包上傳到私有倉(cāng)庫(kù)
在項(xiàng)目開發(fā)中通常會(huì)引用其他的jar,怎樣把自己的項(xiàng)目做為一個(gè)jar包的形式發(fā)布到私服倉(cāng)庫(kù)中,本文就詳細(xì)的介紹一下,感興趣的可以了解一下2021-06-06Spring?Cloud?Feign?使用對(duì)象參數(shù)的操作
這篇文章主要介紹了Spring?Cloud?Feign?如何使用對(duì)象參數(shù)的問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02springboot2.x 接入阿里云市場(chǎng)短信發(fā)送的實(shí)現(xiàn)
本文主要介紹了springboot2.x 接入阿里云市場(chǎng)短信發(fā)送的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11Spring?Boot面試必問(wèn)之啟動(dòng)流程知識(shí)點(diǎn)詳解
SpringBoot是Spring開源組織下的子項(xiàng)目,是Spring組件一站式解決方案,主要是簡(jiǎn)化了使用Spring的難度,簡(jiǎn)省了繁重的配置,提供了各種啟動(dòng)器,開發(fā)者能快速上手,這篇文章主要給大家介紹了關(guān)于Spring?Boot面試必問(wèn)之啟動(dòng)流程知識(shí)點(diǎn)的相關(guān)資料,需要的朋友可以參考下2022-06-06java實(shí)現(xiàn)文件導(dǎo)入導(dǎo)出
這篇文章主要介紹了java實(shí)現(xiàn)文件導(dǎo)入導(dǎo)出的方法和具體示例代碼,非常的簡(jiǎn)單實(shí)用,有需要的小伙伴可以參考下2016-04-04SpringBoot實(shí)現(xiàn)ImportBeanDefinitionRegistrar動(dòng)態(tài)注入
在閱讀Spring Boot源碼時(shí),看到Spring Boot中大量使用ImportBeanDefinitionRegistrar來(lái)實(shí)現(xiàn)Bean的動(dòng)態(tài)注入,它是Spring中一個(gè)強(qiáng)大的擴(kuò)展接口,本文就來(lái)詳細(xì)的介紹一下如何使用,感興趣的可以了解一下2024-02-02