不懂Java SPI機制,怎麼進大廠

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"真正的大師永遠懷着一顆學徒的心","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"引言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在日常的項目開發中,我們爲了提升程序的擴展性,經常使用面向接口的編程思想進行編程。這不僅體現了程序設計對於修改關閉,對於擴展開放的程序設計原則,同時也實現了程序可插拔。那麼本文所闡述的SPI機制正是這種編程思想的體現。今天就和大家聊聊","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"SPI","attrs":{}}],"attrs":{}},{"type":"text","text":"到底是個什麼鬼。順便和大家一起看下","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Seata","attrs":{}}],"attrs":{}},{"type":"text","text":"框架中是怎麼使用","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"SPI","attrs":{}}],"attrs":{}},{"type":"text","text":"機制來實現框架擴展的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"什麼是SPI","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在一般的開發邏輯中,都是服務提供方進行接口定義以及不同實現,服務調用方通過","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"API","attrs":{}}],"attrs":{}},{"type":"text","text":"的方式完成一次業務調用。但是這種方式對於服務調用方來說缺乏靈活性,不能根據自己的需要進行不同的實現加載。那麼有沒有一種機制可以賦予調用方更大的決策權呢?這個時候今天的主角SPI就隆重登場了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"SPI(Service Provider Interface)","attrs":{}}],"attrs":{}},{"type":"text","text":",即服務提供者接口。聽上去有點不明覺厲,不知道表達什麼意思。按照我的理解,它就是一種服務發現機制。其本質就是將接口與實現進行解偶分離。區別於由服務實現方提供接口定義的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"API","attrs":{}}],"attrs":{}},{"type":"text","text":"方式,","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"SPI","attrs":{}}],"attrs":{}},{"type":"text","text":"需要服務調用方進行接口聲明,具體實現由第三方進行實現。簡單來說,","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"SPI","attrs":{}}],"attrs":{}},{"type":"text","text":"就是生活中的甲方,你們這些乙方想要和我合作就必須按照我的要求來幹活。通過這種方式調用方擁有了更大的靈活性,可以根據自身實際需要加載符合條件的實現。從而提升了程序的可擴展性,讓服務提供方可以面向接口編程。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這麼棒的擴展機制怎麼使用呢?我們只需要在","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"jar","attrs":{}}],"attrs":{}},{"type":"text","text":"包的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"META-INF/services/","attrs":{}}],"attrs":{}},{"type":"text","text":"目錄裏同時創建一個以服務接口命名的文件。該文件裏就是實現該服務接口的具體實現類的名稱。而當外部程序裝配這個模塊的時候,就能通過該","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"jar","attrs":{}}],"attrs":{}},{"type":"text","text":"包","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"META-INF/services/","attrs":{}}],"attrs":{}},{"type":"text","text":"裏的配置文件找到具體的實現類名,並裝載實例化,完成實現類的的加載注入。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用方提供規則說明,實際服務提供方完成具體實現。其實這種思想和","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Spring","attrs":{}}],"attrs":{}},{"type":"text","text":"中的組件掃描是類似的,都是先指定好規則,服務提供方根據規範讓框架自動進行服務發現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f0/f0fd51814e38e0c248cc02f6ebbcc947.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"重點來了,知識點來了,敲黑板了。自此我們可以發現,無論是本文談到的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"SPI","attrs":{}}],"attrs":{}},{"type":"text","text":",還是","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"SpringBoot","attrs":{}}],"attrs":{}},{"type":"text","text":"中的自動配置原理,實際都是一種約定大於配置的開發思想,通過事先約定好的內容,進行具體實現,從而提升程序的擴展性。所以希望大家在看一項技術時,除了關注技術細節,進行縱向瞭解,也要關注橫向技術對比,從而找到這些技術的共通之處,瞭解其背後的設計思想,我一直覺得這個是非常重要的,畢竟招式一直都是在變化,但是內功修煉可以催生出新的招式。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"SPI實現分析","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"SPI使用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Mysql","attrs":{}}],"attrs":{}},{"type":"text","text":"的驅動加載爲例,首先定義好需要進行擴展的模板接口,即爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"java.sql.Driver","attrs":{}}],"attrs":{}},{"type":"text","text":"接口。各個數據庫廠商可以更具自身數據庫的特點進行對應的驅動開發,但是都要遵從這個模板接口。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/52/525ffe03684da39d7ec5d516ccd99a46.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Mysql的驅動二方包中,在其 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Classpath","attrs":{}}],"attrs":{}},{"type":"text","text":" ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"路徑下的","attrs":{}}],"attrs":{}},{"type":"text","text":" META-INF/services/ 目錄中,創建一個以服務接口完全名稱一致的的文件,在這個文件中保存的內容是模板接口具體實現類的完全限定名。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7a/7a3f9229b737b81d095dc2b73cfaa7c4.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在對應的目錄中進行具體的類實現,這些實現類都實現了","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"java.sql.Driver","attrs":{}}],"attrs":{}},{"type":"text","text":"接口。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/29/293334bfdd8b558c02829c4ddeefa763.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具體的代碼實現,通過","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ServiceLoader","attrs":{}}],"attrs":{}},{"type":"text","text":"加載對應的實現類,完成類的實例化操作。當然這個ServiceLoader也可以自己定義,像","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Dubbo","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Seata","attrs":{}}],"attrs":{}},{"type":"text","text":"這樣的框架都自己定義類加載器。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"public final class ServiceLoader\n implements Iterable\n{\n \n private static final String PREFIX = \"META-INF/services/\";\n ...\n public static ServiceLoader load(Class service,\n ClassLoader loader)\n{\n return new ServiceLoader<>(service, loader);\n }\n\n public static ServiceLoader load(Class service) {\n ClassLoader cl = Thread.currentThread().getContextClassLoader();\n return ServiceLoader.load(service, cl);\n }\n\n private ServiceLoader(Class svc, ClassLoader cl) {\n service = Objects.requireNonNull(svc, \"Service interface cannot be null\");\n loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;\n acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;\n reload();\n }\n ...\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們一起來分析下這個服務加載器的工作流程,首先通過","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ServiceLoader.load()","attrs":{}}],"attrs":{}},{"type":"text","text":"進行加載。先獲取當前線程綁定的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ClassLoader","attrs":{}}],"attrs":{}},{"type":"text","text":",如果當前線程綁定的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ClassLoader","attrs":{}}],"attrs":{}},{"type":"text","text":"爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"null","attrs":{}}],"attrs":{}},{"type":"text","text":",則使用","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"SystemClassLoader","attrs":{}}],"attrs":{}},{"type":"text","text":"進行代替,而後清除一下","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"provider","attrs":{}}],"attrs":{}},{"type":"text","text":"緩存,最後創建一個","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"LazyIterator","attrs":{}}],"attrs":{}},{"type":"text","text":"。 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"LazyIterator","attrs":{}}],"attrs":{}},{"type":"text","text":"的部分源碼如下:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"private class LazyIterator implements Iterator\n {\n\n Class service;\n ClassLoader loader;\n Enumeration configs = null;\n Iterator pending = null;\n String nextName = null;\n\n\n ...\n \n public boolean hasNext() {\n if (acc == null) {\n return hasNextService();\n } else {\n PrivilegedAction action = new PrivilegedAction() {\n public Boolean run() { return hasNextService(); }\n };\n return AccessController.doPrivileged(action, acc);\n }\n }\n ...\n \n private boolean hasNextService() {\n if (nextName != null) {\n return true;\n }\n if (configs == null) {\n try {\n //key:獲取完全限定名\n String fullName = PREFIX + service.getName();\n if (loader == null)\n configs = ClassLoader.getSystemResources(fullName);\n else\n configs = loader.getResources(fullName);\n } catch (IOException x) {\n fail(service, \"Error locating configuration files\", x);\n }\n }\n while ((pending == null) || !pending.hasNext()) {\n if (!configs.hasMoreElements()) {\n return false;\n }\n pending = parse(service, configs.nextElement());\n }\n nextName = pending.next();\n return true;\n }\n ...\n\n\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"key","attrs":{}}],"attrs":{}},{"type":"text","text":":通過預定好的目錄地址以及類名來指定類的具體地址,類加載器根據這個地址來加載具體的實現類。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大致的SPI加載過程如下所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4f/4f9cbfddd21b9c476fb49fe7ec9948d4.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Seata如何使用SPI","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Seata","attrs":{}}],"attrs":{}},{"type":"text","text":"是一個分佈式事務的框架,具體的使用這裏不再贅述,有時間可以出專門寫它的文章。本節主要關注Seata是如何利用SPI的方式進行框架能力擴展的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Seata","attrs":{}}],"attrs":{}},{"type":"text","text":"框架中使用 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"EnhancedServiceLoader","attrs":{}}],"attrs":{}},{"type":"text","text":" 實現服務載入,通過名稱我們可以知道他是一種增強型的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ServiceLoader","attrs":{}}],"attrs":{}},{"type":"text","text":"。那麼相對於JDK自身的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ServiceLoader","attrs":{}}],"attrs":{}},{"type":"text","text":",他到底強在哪裏呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由下圖可知, ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"EnhancedServiceLoader","attrs":{}}],"attrs":{}},{"type":"text","text":"不僅支持","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Java","attrs":{}}],"attrs":{}},{"type":"text","text":"原生的服務發現目錄,同樣支持自己自定義的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"META-INF/seata/","attrs":{}}],"attrs":{}},{"type":"text","text":"目錄。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/97/97f31a195697680cb511e044e6807eb4.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外在具體接口實現類上都有@LoadLevel的註解,如果其中有多個配置中心實現類都被加載,那麼可以根據對應註解上的屬性","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"order","attrs":{}}],"attrs":{}},{"type":"text","text":"進行排序。將實際優先級最大的類進行加載。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d8/d81ce9922478d6b95d661345674cb885.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們都知道註冊中心是微服務體系中的必不可少的基礎組件,它記錄了服務提供者的地址信息。那麼在","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Seata","attrs":{}}],"attrs":{}},{"type":"text","text":"中,","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Seata","attrs":{}}],"attrs":{}},{"type":"text","text":"的客戶端如事務管理器","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"TM","attrs":{}}],"attrs":{}},{"type":"text","text":"、資源管理器RM需要與事務協調者","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"TC","attrs":{}}],"attrs":{}},{"type":"text","text":"進行通信,那麼就需要通過註冊中心來獲取服務端的地址信息。","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Seata","attrs":{}}],"attrs":{}},{"type":"text","text":"註冊中心支持多個第三方註冊中心,如","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Consul","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Apollo","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Etcd3","attrs":{}}],"attrs":{}},{"type":"text","text":"等。我們來看下","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Seata","attrs":{}}],"attrs":{}},{"type":"text","text":"是怎麼使用SPI機制來實現對於多個註冊中心擴展支持的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先定義一個","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ConfigurationProvider","attrs":{}}],"attrs":{}},{"type":"text","text":"的接口,你看是不是嗅到了熟悉的味道,只要使用","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"SPI","attrs":{}}],"attrs":{}},{"type":"text","text":"那麼就需要首先把規矩給小弟們定好。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/21/21f72b2ce9d74b4dea4955d793e23d2d.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接着在對應的包","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"META-INF/services/","attrs":{}}],"attrs":{}},{"type":"text","text":"中定義具體實現類,如此處的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Consul","attrs":{}}],"attrs":{}},{"type":"text","text":"配置中心中定義了","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ConsulConfigurationProvider","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/28/283533a6c993eb45757dfe57c600c3a0.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以看到","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ConsulConfigurationProvider","attrs":{}}],"attrs":{}},{"type":"text","text":"實現了","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ConfigurationProvider","attrs":{}}],"attrs":{}},{"type":"text","text":"的接口。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e0/e0904f3ef79331b31f79aab130a15aa0.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我是慕楓,感謝各位小夥伴點贊、收藏和評論,文章持續跟新,我們下期再見!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微信搜索:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"慕楓技術筆記","attrs":{}},{"type":"text","text":",優質文章持續更新,我們有學習打卡的羣可以拉你進,一起努力衝擊大廠,另外有很多學習以及面試的材料提供給大家。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7f/7ff6f4cf7dc92ed189149727ace4dc43.gif","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章