1. 程式人生 > 實用技巧 >從原始碼的角度搞懂 Java 動態代理!

從原始碼的角度搞懂 Java 動態代理!

前言

最近,看了一下關於RMI(Remote Method Invocation)相關的知識,遇到了一個動態代理的問題,然後就決定探究一下動態代理。

這裡先科普一下RMI。

RMI

像我們平時寫的程式,物件之間互相呼叫方法都是在同一個JVM中進行,而RMI可以實現一個JVM上的物件呼叫另一個JVM上物件的方法,即遠端呼叫。

介面定義

定義一個遠端物件介面,實現Remote介面來進行標記。

publicinterfaceUserInterfaceextendsRemote{
voidsayHello()throwsRemoteException;
}

遠端物件定義

定義一個遠端物件類,繼承UnicastRemoteObject來實現Serializable和Remote介面,並實現介面方法。

publicclassUserextendsUnicastRemoteObjectimplementsUserInterface{
publicUser()throwsRemoteException{}
@Override
publicvoidsayHello(){
System.out.println("HelloWorld");
}
}

服務端

啟動服務端,將user物件在登錄檔上進行註冊。

publicclassRmiServer{
publicstaticvoidmain(String[]args)throwsRemoteException,AlreadyBoundException,MalformedURLException{
Useruser=newUser();
LocateRegistry.createRegistry(8888);
Naming.bind("rmi://127.0.0.1:8888/user",user);
System.out.println("rmiserverisstarting...");
}
}

啟動服務端:

客戶端

從服務端登錄檔獲取遠端物件,在服務端呼叫sayHello()方法。

publicclassRmiClient{
publicstaticvoidmain(String[]args)throwsRemoteException,NotBoundException,MalformedURLException{
UserInterfaceuser=(UserInterface)Naming.lookup("rmi://127.0.0.1:8888/user");
user.sayHello();
}
}

服務端執行結果:​至此,一個簡單的RMI demo完成。

動態代理

提出問題

看了看RMI程式碼,覺得UserInterface這個介面有點多餘,如果客戶端使用Naming.lookup()獲取的物件不強轉成UserInterface,直接強轉成User是不是也可以,於是試了一下,就報了以下錯誤:​似曾相識又有點陌生的$Proxy0,翻了翻塵封的筆記找到了是動態代理的知識點,寥寥幾筆帶過,所以決定梳理一下動態代理,重新整理一份筆記。

動態代理Demo

介面定義

publicinterfaceUserInterface{
voidsayHello();
}

真實角色定義

publicclassUserimplementsUserInterface{
@Override
publicvoidsayHello(){
System.out.println("HelloWorld");
}
}

呼叫處理類定義

代理類呼叫真實角色的方法時,其實是呼叫與真實角色繫結的處理類物件的invoke()方法,而invoke()呼叫的是真實角色的方法。

這裡需要實現 InvocationHandler 介面以及invoke()方法。

publicclassUserHandlerimplementsInvocationHandler{
privateUseruser;
publicUserProxy(Useruser){
this.user=user;
}
@Override
publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{
System.out.println("invokingstart....");
method.invoke(user);
System.out.println("invokingstop....");
returnuser;
}
}

執行類

publicclassMain{
publicstaticvoidmain(String[]args){
Useruser=newUser();
//處理類和真實角色繫結
UserHandleruserHandler=newUserHandler(user);
//開啟將代理類class檔案儲存到本地模式,平時可以省略
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
//動態代理生成代理物件$Proxy0
Objecto=Proxy.newProxyInstance(Main.class.getClassLoader(),newClass[]{UserInterface.class},userHandler);
//呼叫的其實是invoke()
((UserInterface)o).sayHello();
}

執行結果:​這樣動態代理的基本用法就學完了,可是還有好多問題不明白。

  1. 動態代理是怎麼呼叫的invoke()方法?

  2. 處理類UserHandler有什麼作用?

  3. 為什麼要將類載入器和介面類陣列當作引數傳入newProxyInstance?

假如讓你去實現動態代理,你有什麼設計思路?

猜想

動態代理,是不是和靜態代理,即設計模式的代理模式有相同之處呢?

簡單捋一捋代理模式實現原理:真實角色和代理角色共同實現一個介面並實現抽象方法A,代理類持有真實角色物件,代理類在A方法中呼叫真實角色物件的A方法。在Main中例項化代理物件,呼叫其A方法,間接呼叫了真實角色的A方法。

「實現程式碼」

//介面和真實角色物件就用上面程式碼
//代理類,實現UserInterface介面
publicclassUserProxyimplementsUserInterface{
//持有真實角色物件
privateUseruser=newUser();
@Override
publicvoidsayHello(){
System.out.println("invokingstart....");
//在代理物件的sayHello()裡呼叫真實角色的sayHello()
user.sayHello();
System.out.println("invokingstop....");
}
}
//執行類
publicclassMain{
publicstaticvoidmain(String[]args){
//例項化代理角色物件
UserInterfaceuserProxy=newUserProxy();
//呼叫了代理物件的sayHello(),其實是呼叫了真實角色的sayHello()
userProxy.sayHello();
}

拿開始的動態代理程式碼和靜態代理比較,介面、真實角色都有了,區別就是多了一個UserHandler處理類,少了一個UserProxy代理類。

接著對比一下兩者的處理類和代理類,發現UserHandler的invoke()和UserProxy的sayHello()這兩個方法的程式碼都是一樣的。那麼,是不是新建一個UserProxy類,然後實現UserInterface介面並持有UserHandler的物件,在sayHello()方法中呼叫UserHandler的invoke()方法,就可以動態代理了。

「程式碼大概就是這樣的」

//猜想的代理類結構,動態代理生成的代理是com.sun.proxy.$Proxy0
publicclassUserProxyimplementsUserInterface{
//持有處理類的物件
privateInvocationHandlerhandler;
publicUserProxy(InvocationHandlerhandler){
this.handler=handler;
}
//實現sayHello()方法,並呼叫invoke()
@Override
publicvoidsayHello(){
try{
handler.invoke(this,UserInterface.class.getMethod("sayHello"),null);
}catch(Throwablethrowable){
throwable.printStackTrace();
}
}
}
//執行類
publicstaticvoidmain(String[]args){
Useruser=newUser();
UserHandleruserHandler=newUserHandler(user);
UserProxyproxy=newUserProxy(userHandler);
proxy.sayHello();
}

輸出結果:

上面的代理類程式碼是寫死的,而動態代理是當你呼叫Proxy.newProxyInstance()時,會根據你傳入的引數來動態生成這個代理類程式碼,如果讓我實現,會是以下這個流程。

  1. 根據你傳入的Class[]介面陣列,代理類會來實現這些介面及其方法(這裡就是sayHello()),並且持有你傳入的userHandler物件,使用檔案流將預先設定的包名、類名、方法名等一行行程式碼寫到本地磁碟,生成$Proxy0.java檔案

  2. 使用編譯器將編譯成Proxy0.class

  3. 根據你傳入的ClassLoader將$Proxy0.class載入到JMV中

  4. 呼叫Proxy.newProxyInstance()就會返回一個$Proxy0的物件,然後呼叫sayHello(),就執行了裡面userHandler的invoke()

以上就是對動態代理的一個猜想過程,下面就通過debug看看原始碼是怎麼實現的。

在困惑的日子裡學會擁抱原始碼

擁抱原始碼

呼叫流程圖

這裡先用PPT畫一個流程圖,可以跟著流程圖來看後面的原始碼。

流程圖

「從newProxyInstance()設定斷點」

newProxyInstance()

newProxyInstance()程式碼分為上下兩部分,上部分是獲取類,下部分是通過反射構建Proxy0物件。

「上部分程式碼」

newProxyInstance()

從名字看就知道getProxyClass0()是核心方法,step into

getProxyClass0()

getProxyClass()

裡面呼叫了WeakCache物件的get()方法,這裡暫停一下debug,先講講WeakCache類。

WeakCache

顧名思義,它是一個弱引用快取。那什麼是是弱引用呢,是不是還有強引用呢?

弱引用

WeakReference就是弱引用類,作為包裝類來包裝其他物件,在進行GC時,其中的包裝物件會被回收,而WeakReference物件會被放到引用佇列中。

舉個栗子:

//這就是強引用,只要不寫str1=null,str1指向的這個字串不就會被垃圾回收
Stringstr1=newString("hello");
ReferenceQueuereferenceQueue=newReferenceQueue();
//只要垃圾回收,這個str2裡面包裝的物件就會被回收,但是這個弱引用物件不會被回收,即word會被回收,但是str2指向的弱引用物件不會
//每個弱引用關聯一個ReferenceQueue,當包裝的物件被回收,這個弱引用物件會被放入引用佇列中
WeakReference<String>str2=newWeakReference<>(newString("world"),referenceQueue);
//執行gc
System.gc();
Thread.sleep(3);
//輸出被回收包裝物件的弱引用物件:java.lang.ref.WeakReference@2077d4de
//可以debug看一下,弱引用物件的referent變數指向的包裝物件已經為null
System.out.println(referenceQueue.poll());

WeakCache的結構

其實整個WeakCache的都是圍繞著成員變數map來工作的,構建了一個一個<K,<K,V>>格式的二級快取,在動態代理中對應的型別是<類載入器, <介面Class, 代理Class>>,它們都使用了弱引用進行包裝,這樣在垃圾回收的時候就可以直接回收,減少了堆記憶體佔用。

//存放已回收弱引用的佇列
privatefinalReferenceQueue<K>refQueue=newReferenceQueue<>();
//使用ConcurrentMap實現的二級快取結構
privatefinalConcurrentMap<Object,ConcurrentMap<Object,Supplier<V>>>map=newConcurrentHashMap<>();
//可以不關注這個,這個是用來標識二級快取中的value是否存在的,即Supplier是否被回收
privatefinalConcurrentMap<Supplier<V>,Boolean>reverseMap=newConcurrentHashMap<>();
//包裝傳入的介面class,生成二級快取的Key
privatefinalBiFunction<K,P,?>subKeyFactory=newKeyFactory();
//包裝$Proxy0,生成二級快取的Value
privatefinalBiFunction<K,P,V>valueFactory=newProxyClassFactory();

WeakCache的get()

回到debug,接著進入get()方法,看看map二級快取是怎麼生成KV的。

publicVget(Kkey,Pparameter){
Objects.requireNonNull(parameter);
//遍歷refQueue,然後將快取map中對應的失效value刪除
expungeStaleEntries();
//以ClassLoader為key,構建map的一級快取的Key,是CacheKey物件
ObjectcacheKey=CacheK.valueOf(key,refQueue);
//通過Key從map中獲取一級快取的value,即ConcurrentMap
ConcurrentMap<Object,Supplier<V>>valuesMap=map.get(cacheKey);
if(valuesMap==null){
//如果Key不存在,就新建一個ConCurrentMap放入map,這裡使用的是putIfAbsent
//如果key已經存在了,就不覆蓋並返回裡面的value,不存在就返回null並放入Key
//現在快取map的結構就是ConCurrentMap<CacheKey,ConCurrentMap<Object,Supplier>>
ConcurrentMap<Object,Supplier<V>>oldValuesMap=map.putIfAbsent(cacheKey,valuesMap=newConcurrentHashMap<>());
//如果其他執行緒已經建立了這個Key並放入就可以複用了
if(oldValuesMap!=null){
valuesMap=oldValuesMap;
}
}
//生成二級快取的subKey,現在快取map的結構就是ConCurrentMap<CacheKey,ConCurrentMap<Key1,Supplier>>
//看後面的<生成二級快取Key>!!!
ObjectsubKey=Objects.requireNonNull(subKeyFactory.apply(key,parameter));
//根據二級快取的subKey獲取value
Supplier<V>supplier=valuesMap.get(subKey);
Factoryfactory=null;

//!!!直到完成二級快取Value的構建才結束,Value是弱引用的$Proxy0.class!!!
while(true){
//第一次迴圈:suppiler肯定是null,因為還沒有將放入二級快取的KV值
//第二次迴圈:這裡suppiler不為null了!!!進入if
if(supplier!=null){
//第二次迴圈:真正生成代理物件,
//往後翻,看<生成二級快取Value>,核心!!!!!
//看完後面回到這裡:value就是弱引用後的$Proxy0.class
Vvalue=supplier.get();
if(value!=null){
//本方法及上部分的最後一行程式碼,跳轉最後的<構建$Proxy物件>
returnvalue;
}
}
//第一次迴圈:factory肯定為null,生成二級快取的Value
if(factory==null){
factory=newFactory(key,parameter,subKey,valuesMap);
}
//第一次迴圈:將subKey和factory作為KV放入二級快取
if(supplier==null){
supplier=valuesMap.putIfAbsent(subKey,factory);
if(supplier==null){
//第一次迴圈:賦值之後suppiler就不為空了,記住!!!!!
supplier=factory;
}
}
}
}
}

生成二級快取Key

在get()中呼叫subKeyFactory.apply(key, parameter),根據你newProxyInstance()傳入的介面Class[]的個數來生成二級快取的Key,這裡我們就傳入了一個UserInterface.class,所以就返回了Key1物件。

KeyFactory.apply()

不論是Key1、Key2還是KeyX,他們都繼承了WeakReference,都是包裝物件是Class的弱引用類。這裡看看Key1的程式碼。

Key1

生成二級快取Value

在上面的while迴圈中,第一次迴圈只是生成了一個空的Factory物件放入了二級快取的ConcurrentMap中。

在第二次迴圈中,才開始通過get()方法來真正的構建value。

別回頭,接著往下看。

Factory.get()生成弱引用value

「CacheValue」類是一個弱引用,是二級快取的Value值,包裝的是class,在這裡就是$Proxy0.class,至於這個類如何生成的,根據下面程式碼註釋一直看完Class檔案的生成

publicsynchronizedVget(){
//檢查是否被回收,如果被回收,會繼續執行上面的while迴圈,重新生成Factory
Supplier<V>supplier=valuesMap.get(subKey);
if(supplier!=this){
returnnull;
}
//這裡的V的型別是Class
Vvalue=null;
//這行是核心程式碼,看後面<class檔案的生成>,記住這裡返回的是Class
value=Objects.requireNonNull(valueFactory.apply(key,parameter));
//將Class物件包裝成弱引用
CacheValue<V>cacheValue=newCacheValue<>(value);
//回到上面<WeakCache的get()方法>Vvalue=supplier.get();
returnvalue;
}
}

CacheValue


Class檔案的生成

包名類名的定義與驗證

進入valueFactory.apply(key, parameter)方法,看看class檔案是怎麼生成的。

privatestaticfinalStringproxyClassNamePrefix="$Proxy";

publicClass<?>apply(ClassLoaderloader,Class<?>[]interfaces){
Map<Class<?>,Boolean>interfaceSet=newIdentityHashMap<>(interfaces.length);
//遍歷你傳入的Class[],我們只傳入了UserInterface.class
for(Class<?>intf:interfaces){
Class<?>interfaceClass=null;
//獲取介面類
interfaceClass=Class.forName(intf.getName(),false,loader);
//這裡就很明確為什麼只能傳入介面類,不是介面類會報錯
if(!interfaceClass.isInterface()){
thrownewIllegalArgumentException(
interfaceClass.getName()+"isnotaninterface");
}
StringproxyPkg=null;
intaccessFlags=Modifier.PUBLIC|Modifier.FINAL;
for(Class<?>intf:interfaces){
intflags=intf.getModifiers();
//驗證介面是否是public,不是public代理類會用介面的package,因為只有在同一包內才能繼承
//我們的UserInterface是public,所以跳過
if(!Modifier.isPublic(flags)){
accessFlags=Modifier.FINAL;
Stringname=intf.getName();
intn=name.lastIndexOf('.');
Stringpkg=((n==-1)?"":name.substring(0,n+1));
if(proxyPkg==null){
proxyPkg=pkg;
}elseif(!pkg.equals(proxyPkg)){
thrownewIllegalArgumentException(
"non-publicinterfacesfromdifferentpackages");
}
}
}
//如果介面類是public,則用預設的包
if(proxyPkg==null){
//PROXY_PACKAGE="com.sun.proxy";
proxyPkg=ReflectUtil.PROXY_PACKAGE+".";
}
//原子Int,此時num=0
longnum=nextUniqueNumber.getAndIncrement();
// com.sun.proxy.$Proxy0,這裡包名和類名就出現了!!!
StringproxyName=proxyPkg+proxyClassNamePrefix+num;
//!!!!生成class檔案,檢視後面<class檔案寫入本地>核心!!!!
byte[]proxyClassFile=ProxyGenerator.generateProxyClass(proxyName,interfaces,accessFlags);
//!!!看完下面再回來看這行!!!!
//獲取了位元組陣列之後,獲取了class的二進位制流將類載入到了JVM中
//並且返回了$Proxy0.class,返回給Factory.get()來包裝
returndefineClass0(loader,proxyName,proxyClassFile,0,proxyClassFile.length);

}
}
}

defineClass0()是Proxy類自定義的類載入的native方法,會獲取class檔案的二進位制流載入到JVM中,以獲取對應的Class物件,這一塊可以參考JVM類載入器。

class檔案寫入本地

generateProxyClass()方法會將class二進位制檔案寫入本地目錄,並返回class檔案的二進位制流,使用你傳入的類載入器載入,「這裡你知道類載入器的作用了麼」

publicstaticbyte[]generateProxyClass(finalStringname,
Class[]interfaces)
{
ProxyGeneratorgen=newProxyGenerator(name,interfaces);
//生成class檔案的二進位制,檢視後面<生成class檔案二進位制>
finalbyte[]classFile=gen.generateClassFile();
//將class檔案寫入本地
if(saveGeneratedFiles){
java.security.AccessController.doPrivileged(
newjava.security.PrivilegedAction<Void>(){
publicVoidrun(){
try{
FileOutputStreamfile=
newFileOutputStream(dotToSlash(name)+".class");
file.write(classFile);
file.close();
returnnull;
}catch(IOExceptione){
thrownewInternalError(
"I/Oexceptionsavinggeneratedfile:"+e);
}
}
});
}
//返回$Proxy0.class位元組陣列,回到上面<class檔案生成>
returnclassFile;
}

生成class檔案二進位制流

generateClassFile()生成class檔案,並存放到位元組陣列,「可以順便學一下class結構,這裡也體現了你傳入的class[]的作用」

privatebyte[]generateClassFile(){
//將hashcode、equals、toString是三個方法放入代理類中
addProxyMethod(hashCodeMethod,Object.class);
addProxyMethod(equalsMethod,Object.class);
addProxyMethod(toStringMethod,Object.class);
for(inti=0;i<interfaces.length;i++){
Method[]methods=interfaces[i].getMethods();
for(intj=0;j<methods.length;j++){
//將介面類的方法放入新建的代理類中,這裡就是sayHello()
addProxyMethod(methods[j],interfaces[i]);
}
}
for(List<ProxyMethod>sigmethods:proxyMethods.values()){
checkReturnTypes(sigmethods);
}
//給代理類增加構造方法
methods.add(generateConstructor());
for(List<ProxyMethod>sigmethods:proxyMethods.values()){
for(ProxyMethodpm:sigmethods){
//將上面的四個方法都封裝成Method型別成員變數
fields.add(newFieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;",
ACC_PRIVATE|ACC_STATIC));
//generatecodeforproxymethodandaddit
methods.add(pm.generateMethod());
}
}
//static靜態塊構造
methods.add(generateStaticInitializer());
cp.getClass(dotToSlash(className));
cp.getClass(superclassName);
for(inti=0;i<interfaces.length;i++){
cp.getClass(dotToSlash(interfaces[i].getName()));
}
cp.setReadOnly();
ByteArrayOutputStreambout=newByteArrayOutputStream();
DataOutputStreamdout=newDataOutputStream(bout);
// !!!核心點來了!這裡就開始構建class檔案了,以下都是class的結構,只寫一部分
try{
// u4 magic,class檔案的魔數,確認是否為一個能被JVM接受的class
dout.writeInt(0xCAFEBABE);
//u2minor_version,0
dout.writeShort(CLASSFILE_MINOR_VERSION);
//u2major_version,主版本號,Java8對應的是52;
dout.writeShort(CLASSFILE_MAJOR_VERSION);
//常量池
cp.write(dout);
//其他結構,可參考class檔案結構
dout.writeShort(ACC_PUBLIC|ACC_FINAL|ACC_SUPER);
dout.writeShort(cp.getClass(dotToSlash(className)));
dout.writeShort(cp.getClass(superclassName));
dout.writeShort(interfaces.length);
for(inti=0;i<interfaces.length;i++){
dout.writeShort(cp.getClass(
dotToSlash(interfaces[i].getName())));
}
dout.writeShort(fields.size());
for(FieldInfof:fields){
f.write(dout);
}
dout.writeShort(methods.size());
for(MethodInfom:methods){
m.write(dout);
}
dout.writeShort(0);
}catch(IOExceptione){
thrownewInternalError("unexpectedI/OException",e);
}
//將class檔案位元組陣列返回
returnbout.toByteArray();
}

構建$Proxy物件

newProxyInstance()上半部分經過上面層層程式碼呼叫,獲取了$Proxy0.class,接下來看下部分程式碼:

newInstance

cl就是上面獲取的Proxy0.class,h就是上面傳入的userHandler,被當做構造引數來建立$Proxy0物件。然後獲取這個動態代理物件,呼叫sayHello()方法,相當於呼叫了UserHandler的invoke(),「這裡就是UserHandler的作用」

$Proxy.class檔案

我們開啟了將代理class寫到本地目錄的功能,在專案下的com/sum/proxy目錄下找到了$Proxy0的class檔案。

「看一下反編譯的class」

packagecom.sun.proxy;

importcom.test.proxy.UserInterface;
importjava.lang.reflect.InvocationHandler;
importjava.lang.reflect.Method;
importjava.lang.reflect.Proxy;
importjava.lang.reflect.UndeclaredThrowableException;

publicfinalclass$Proxy0extendsProxyimplementsUserInterface{
privatestaticMethodm1;
privatestaticMethodm3;
privatestaticMethodm2;
privatestaticMethodm0;

public$Proxy0(InvocationHandlervar1)throws{
super(var1);
}

publicfinalbooleanequals(Objectvar1)throws{
try{
return(Boolean)super.h.invoke(this,m1,newObject[]{var1});
}catch(RuntimeException|Errorvar3){
throwvar3;
}catch(Throwablevar4){
thrownewUndeclaredThrowableException(var4);
}
}

publicfinalvoidsayHello()throws{
try{
super.h.invoke(this,m3,(Object[])null);
}catch(RuntimeException|Errorvar2){
throwvar2;
}catch(Throwablevar3){
thrownewUndeclaredThrowableException(var3);
}
}

publicfinalStringtoString()throws{
try{
return(String)super.h.invoke(this,m2,(Object[])null);
}catch(RuntimeException|Errorvar2){
throwvar2;
}catch(Throwablevar3){
thrownewUndeclaredThrowableException(var3);
}
}

publicfinalinthashCode()throws{
try{
return(Integer)super.h.invoke(this,m0,(Object[])null);
}catch(RuntimeException|Errorvar2){
throwvar2;
}catch(Throwablevar3){
thrownewUndeclaredThrowableException(var3);
}
}

static{
try{
m1=Class.forName("java.lang.Object").getMethod("equals",Class.forName("java.lang.Object"));
m3=Class.forName("com.test.proxy.UserInterface").getMethod("sayHello");
m2=Class.forName("java.lang.Object").getMethod("toString");
m0=Class.forName("java.lang.Object").getMethod("hashCode");
}catch(NoSuchMethodExceptionvar2){
thrownewNoSuchMethodError(var2.getMessage());
}catch(ClassNotFoundExceptionvar3){
thrownewNoClassDefFoundError(var3.getMessage());
}
}
}

結語

上面就是動態代理原始碼的除錯過程,與之前的猜想的代理類的生成過程比較,動態代理是直接生成class檔案,省去了java檔案和編譯這一塊。

剛開始看可能比較繞,跟著註釋及跳轉指引,耐心多看兩遍就明白了。動態代理涉及的知識點比較多,我自己看的時候,在WeakCache這一塊糾結了一陣,其實把它當成一個兩層的map對待即可,只不過裡面所有的KV都被弱引用包裝。

希望看到這篇文章的每個程式設計師最終都能成為頭髮茂盛的碼農;

推薦閱讀

為什麼阿里巴巴的程式設計師成長速度這麼快,看完他們的內部資料我懂了

位元組跳動總結的設計模式 PDF 火了,完整版開放下載

刷Github時發現了一本阿里大神的演算法筆記!標星70.5K

程式設計師50W年薪的知識體系與成長路線。

月薪在30K以下的Java程式設計師,可能聽不懂這個專案;

位元組跳動總結的設計模式 PDF 火了,完整版開放分享

關於【暴力遞迴演算法】你所不知道的思路

開闢鴻蒙,誰做系統,聊聊華為微核心

看完三件事❤️

如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。

關注公眾號 『 Java鬥帝 』,不定期分享原創知識。

同時可以期待後續文章ing