偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Java動態(tài)模塊化運行原理與實踐

開發(fā) 后端
在Java模塊化編程中,我們可以使用服務(wù)進行Bundle間的通信,通過服務(wù)可以讓模塊系統(tǒng)動態(tài)化,這樣就能應(yīng)對在運行時服務(wù)的變化問題。

我們之前曾了解過面向Java EE 6平臺的上下文和依賴性注入OSGi依賴性管理,比如Bundle的訪問域等內(nèi)容。其實,標準Java代碼和模塊化Java代碼的區(qū)別之一就是依賴在運行時是如何綁定的。在本篇文章中,我們將詳細討論模塊化Java中的動態(tài)模塊化,包括對Bundle ClassPath、類的垃圾回收以及查找綁定等。

Bundle ClassPath

對于一個普通Java程序,只有一個classpath——啟動應(yīng)用程序所使用的那個。該路徑通常是在命令行中用-classpath選項指定的,或者通 過CLASSPATH 環(huán)境變量來設(shè)定。Java類裝載器在運行時解析類的時候會掃描此路徑,無論這一過程是靜態(tài)地(已編譯進代碼)還是動態(tài)地(使用反射及 class.forName())。然而,在運行時也可以使用多個類加載器;像Jetty和Tomcat這樣的Web應(yīng)用引擎都是使用多個類加載器,以便支持應(yīng)用熱部署。

在OSGi中,每個bundle都有其自己的類加載器。需要被其他bundle訪問的類則被委派(delegated)給這些其他bundle的類裝載器。因此,盡管在傳統(tǒng)應(yīng)用中,來自logging類庫、client和server JAR中的類都是由同一個類加載器加載的,但在OSGi模塊系統(tǒng)中,他們都是由自己的類加載器加載的。

結(jié)果是,一個VM中有可能有多個類加載器,其中可能存在名字相同的不同Class的對象。也就是說,在同一個VM中,一個叫做 com.infoq.example.App的類,其不同版本可以由com.infoq.example bundle的第1版和第2版同時輸出。Client bundle版本1使用該類的第1版,而client版本2使用該類的第2版。這在模塊化系統(tǒng)中相當(dāng)普遍;在同一個VM中,有些代碼可能需要裝載一個類庫 的老版本,同時更新點的代碼(在另一個bundle中)卻需要該類庫的新版本。好在OSGi為你管理起這種依賴傳遞,確保不再出現(xiàn)不兼容類引發(fā)的問題。

類的垃圾回收

每個類都有一個對其類裝載器的引用。因此如果想要從不同的bundle訪問這些類,不但要有對該類實例的引用,而且還要有對該類的類裝載器的引用。當(dāng)一個bundle持有另一個bundle的類時,它也會將該bundle固定在內(nèi)存中。在前篇文章的例子中,client被固定到該server上。

#T#在靜態(tài)世界里,無論你是否把自己的類固定到其他類(或類庫)都無所謂;因為不會有什么變化??墒?,在動態(tài)世界里,在運行時將類庫或工具替換成新版本就有可 能了。這聽起來可能有點復(fù)雜,但是在可熱部署應(yīng)用的Web應(yīng)用引擎早期就出現(xiàn)了(如Tomcat,最早發(fā)布于1999年)。每個Web應(yīng)用程序都綁定到 Servlet API的某個版本上,當(dāng)其停止時,裝載該Web應(yīng)用的類加載器也就廢棄掉了。當(dāng)Web應(yīng)用重新被部署時,又創(chuàng)建了一個新的類加載器,新版類就由其裝載。只要servlet引擎沒有保持對老版應(yīng)用的引用,這些類就像其他Java對象一樣被垃圾回收器回收了。

并不是所有的類庫都能意識到Java代碼中可能存在類泄漏的問題,就像是內(nèi)存泄漏。一個典型的例子就是Log4J的addAppender()調(diào)用,一旦其執(zhí)行了,將會把你的類綁定在Log4J bundle的生命周期上。即使你的bundle停止了,Log4J仍將維對appender的引用,并繼續(xù)發(fā)送日志事件(除非該bundle在停止時恰當(dāng)?shù)卣{(diào)用了removeAppender()方法)。

查找和綁定

為了成為動態(tài),我們需要有一個能查找服務(wù)的機制,而不是持久持有他們(以免bundle停止)。這是通過使用簡單Java接口和POJO來實現(xiàn)的,也就是大家所熟知的services(注意他們與WS-DeathStar或其他任何XML底層架構(gòu)都沒有關(guān)系;他們就是普通Java對象——Plain Old Java Objects)。

典型工廠實現(xiàn)方式是使用從properties文件中獲取的某種形式的類名,然后用Class.forName()來實例化相應(yīng)的類,OSGi則不同,它 維護了一個‘服務(wù)注冊器’,其實這是一個包含了類名和服務(wù)的映射列表。這樣,OSGi系統(tǒng)就可以使用 context.getService(getServiceReference("java.sql.Driver")),而不是 class.forName("com.example.JDBCDriver")來獲取一個JDBC驅(qū)動器。這就把client代碼解放出來了,它不需 知道任何特定客戶端實現(xiàn);相反,它可以在運行時綁定任何可用驅(qū)動程序。移植到不同的數(shù)據(jù)庫服務(wù)器也就非常簡單了,只需停止一個模塊并啟動一個新模 塊;client甚至不需要重新啟動,也不需要改變?nèi)魏闻渲谩?/p>

這樣做是因為client只需知道其所需的服務(wù)的API(基本上都是接口,盡管OSGi規(guī)范允許使用其他類)。在上述情況中,接口名是 java.sql.Driver;返回的接口實例是具體的數(shù)據(jù)庫實現(xiàn)(不必了解是哪些類,編碼在那里)。此外,如果服務(wù)不可用(數(shù)據(jù)庫不存在,或數(shù)據(jù)庫臨 時停掉了),那么這個方法會返回null以說明該服務(wù)不可用。

為了完全動態(tài),返回結(jié)果不應(yīng)被緩存。換句話說,每當(dāng)需要服務(wù)的時候,需要重新調(diào)用getService??蚣軙诘讓訄?zhí)行緩存操作,因此不存在太大的性能 問題。但重要的是,它允許數(shù)據(jù)庫服務(wù)在線被替換成新的服務(wù),如果沒有緩存代碼,那么下次調(diào)用時,client將透明地綁定到新服務(wù)上。

付諸實施

為了證明這一點,我們將創(chuàng)建一個用于縮寫URL的OSGi服務(wù)。其思路是服務(wù)接收一個長URL,如http://www.example.com/articles/modular-java-what-is-it,將其轉(zhuǎn)換為短點的URL,如http://tr.im/EyH1。該服務(wù)也可以被廣泛應(yīng)用在Twitter這樣的站點上,還可以用它來把長URL轉(zhuǎn)成短的這樣便簽背后也寫得下。甚至像《新科學(xué)家》和《Macworld》這樣的雜志也是用這些短URL來印刷媒體鏈接的。#p#

為了實現(xiàn)該服務(wù),我們需要:

◆一個縮寫服務(wù)的接口
◆一個注冊為縮寫實現(xiàn)的bundle
◆一個驗證用client

盡管并沒有禁止把這些東西都放在同一個bundle中,但是我們還是把他們分別放在不同的bundle里。(即便他們在一個bundle中,最好也讓bundle通過服務(wù)來通訊,就好像他們處于不同的bundle一樣;這樣他們就可以方便地與其他服務(wù)提供者進行集成。

把縮寫服務(wù)接口與其實現(xiàn)(或client)分開放在單獨bundle中是很重要的。該接口代表了client和server之間的‘共享代碼’,這樣,該 接口在每個bundle中都會加載。正因如此,每個bundle實際上都被固定到了該接口特定版本上,所有服務(wù)都有共同的生命周期,將接口放在單獨 bundle中(在整個OSGi VM生命周期中都在運行),我們的client就可以自由變化。如果我們把該接口放在某個服務(wù)實現(xiàn)的bundle中,那么該服務(wù)發(fā)生變化后我們就不能重新 連接到client上了。

shorten接口的manifest和實現(xiàn)如下:

  1. Bundle-ManifestVersion: 2 
  2. Bundle-Name: Shorten  
  3. Bundle-SymbolicName: com.infoq.shorten  
  4. Bundle-Version: 1.0.0 
  5. Export-Package: com.infoq.shorten  
  6. ---   
  7. package com.infoq.shorten;  
  8.  
  9. public interface IShorten {  
  10.     public String shorten(String url) throws IOException;  

上面的例子建立了一個擁有單一接口(com.infoq.shorten.IShorten)的bundle(com.infoq.shorten),并將其輸出給client。參數(shù)是一個URL,返回一個唯一的壓縮版URL。

和接口定義相比,實現(xiàn)就相對有趣一些了。盡管最近縮寫名稱的應(yīng)用開始多起來了,但是所有這些應(yīng)用的祖師爺都是 TinyURL.com。(具有諷刺意味的是,http://tinyurl.com實際上可以被壓縮的更短http://ow.ly/AvnC)。如今比較流行有:ow.ly、bit.ly、tr.im等等。這里并不是對這些服務(wù)全面介紹,也不是為其背書,我們的實現(xiàn)也可以使用其他同類服務(wù)。本文之所以使用TinyURL和Tr.im,是由于他們都可以匿名基于GET提交,易于實現(xiàn),除此之外沒有其他原因。

每種實現(xiàn)實際上都非常小;都以URL為參數(shù)(要縮寫的東西)并返回新的壓縮過的文本:

  1. package com.infoq.shorten.tinyurl;  
  2. import java.io.BufferedReader;  
  3. import java.io.InputStreamReader;  
  4. import java.net.URL;  
  5. import com.infoq.shorten.IShorten;  
  6.  
  7. public class TinyURL implements IShorten {  
  8.     private static final String lookup =   
  9.         "http://tinyurl.com/api-create.php?url=";  
  10.     public String shorten(String url) throws IOException {  
  11.         String line = new BufferedReader(  
  12.             new InputStreamReader(  
  13.                 new URL(lookup + url).openStream())).readLine();  
  14.         if(line == null)  
  15.             throw new IllegalArgumentException(   
  16.                 "Could not shorten " + url);  
  17.         return line;  
  18.     }  

Tr.im的實現(xiàn)類似,只需用http://api.tr.im/v1/trim_simple?url=替代lookup的值即可。這兩種實現(xiàn)的源代碼分別在com.infoq.shorten.tinyurl和com.infoq.shorten.trim bundle里。

那么,完成縮寫服務(wù)的實現(xiàn)后,我們?nèi)绾巫屍渌绦蛟L問它呢?為此,我們需要把實現(xiàn)注冊為OSGi框架的服務(wù)。BundleContext類的registerService(class,instance,properties)方法可以讓我們定義一個服務(wù)以供后用,該方法通常在bundle的start()調(diào)用期間被調(diào)用。如上篇文章所講,我們必須定義一個BundleActivator。實現(xiàn)該類后,我們還要把Bundle-Activator放在MANIFEST.MF里以便找到該實現(xiàn)。代碼如下:

  1. Manifest-Version: 1.0 
  2. Bundle-ManifestVersion: 2 
  3. Bundle-Name: TinyURL  
  4. Bundle-SymbolicName: com.infoq.shorten.tinyurl  
  5. Bundle-Version: 1.0.0 
  6. Import-Package: com.infoq.shorten,org.osgi.framework  
  7. Bundle-Activator: com.infoq.shorten.tinyurl.Activator  
  8. ---  
  9. package com.infoq.shorten.tinyurl;  
  10. import org.osgi.framework.BundleActivator;  
  11. import org.osgi.framework.BundleContext;  
  12. import com.infoq.shorten.IShorten;  
  13.  
  14. public class Activator implements BundleActivator {  
  15.     public void start(BundleContext context) {  
  16.         context.registerService(IShorten.class.getName(),  
  17.             new TinyURL(),null);  
  18.     }  
  19.     public void stop(BundleContext context) {  
  20.     }  

盡管registerService()方法接收一個字符串作為其第一個參數(shù),而且用"com.infoq.shorten.IShorten"也是可以的,但是最好還是用class.class.getName()這種形式,這樣如果你重構(gòu)了包或改變了類名,在編譯時就可發(fā)現(xiàn)問題。如果用字符串,進行了錯誤的重構(gòu),那么只有在運行時你才能知道問題所在。

registerService()的第二個參數(shù)是實例本身。之所以將其與第一個參數(shù)分開,是因為你可以將同一個服務(wù)實例輸出給多個服務(wù)接口(如果需要帶有版本的API,這就有用了,你可以進化接口了)。另外,一個bundle輸出同一類型的多個服務(wù)也是有可能的。

最后一個參數(shù)是服務(wù)屬性(service properties)。允許你給服務(wù)加上額外元數(shù)據(jù)注解,比如標注優(yōu)先級以表明該服務(wù)相對于其他服務(wù)的重要性,或者調(diào)用者關(guān)心的其他信息(比如功能描述和廠商)。

只要該bundle一啟動,縮寫服務(wù)就可用了。當(dāng)bundle停止,框架將自動取消服務(wù)注冊。如果我們想要自己取消注冊(比方說,對錯誤代碼和網(wǎng)絡(luò)接口不可用所作出的響應(yīng))也很容易(用context.unregisterService())。

使用服務(wù)

一旦服務(wù)起來并運行之后,我們就可以用client訪問它了。如果運行的是Equinox,你可以用services命令羅列所有已安裝的服務(wù),以及它們是由誰注冊的:

  1. {com.infoq.shorten.IShorten}={service.id=27}  
  2.   Registered by bundle: com.infoq.shorten.trim-1.0.0 [1]  
  3.   No bundles using service.  
  4. {com.infoq.shorten.IShorten}={service.id=28}  
  5.   Registered by bundle: com.infoq.shorten.tinyurl-1.0.0 [2]  
  6.   No bundles using service. 

在調(diào)用服務(wù)處理URL之前,client需要解析服務(wù)。我們需要獲得一個服務(wù)引用,它可以讓我們查看服務(wù)自身內(nèi)部的屬性,然后利用其來獲得我們感興趣的服務(wù)??墒?,我們需要能夠重復(fù)處理相同及不同的URL,以便我們可以把它集成到Equinox或Felix的shell里。實現(xiàn)如下:

  1. package com.infoq.shorten.command;  
  2. import org.osgi.framework.BundleContext;  
  3. import org.osgi.framework.ServiceReference;  
  4. import com.infoq.shorten.IShorten;  
  5.  
  6. public class ShortenCommand {  
  7.     protected BundleContext context;  
  8.     public ShortenCommand(BundleContext context) {  
  9.         this.context = context;  
  10.     }  
  11.     protected String shorten(String url) throws IllegalArgumentException, IOException {  
  12.         ServiceReference ref =  
  13.             context.getServiceReference(IShorten.class.getName());  
  14.         if(ref == null)  
  15.             return null;  
  16.         IShorten shorten = (IShorten) context.getService(ref);  
  17.         if(shorten == null)  
  18.             return null;  
  19.         return shorten.shorten(url);  
  20.     }  

當(dāng)shorten方法被調(diào)用時,上面這段程序?qū)⒉檎曳?wù)引用并獲得服務(wù)對象。然后我們可以把服務(wù)對象賦值給一個IShorten對象,并使用它與前面講到 的已注冊服務(wù)進行交互。注意這些都是在同一個VM中發(fā)生的;沒有遠程調(diào)用,沒有強制異常,沒有參數(shù)被序列化;只是一個POJO與另一個POJO對話。實際 上,這里與最開始class.forName()例子的唯一區(qū)別是:我們?nèi)绾潍@得shorten POJO。

為了在Equinox和Felix里面使用這一服務(wù),我們需要放一些樣板代碼進去。必須提一下,當(dāng)我們定義manifest時,我們可以在Felix和 Equinox命令行上聲明可選依賴,這樣,當(dāng)我們兩者中任何一個安裝之后,我們就可以運行了。(一個更好的解決方案是將其部署為單獨的bundles, 這樣我們可以去掉選項;但是如果bundle不存在,activator將會失敗,因此無法啟動)。Equinox和Felix特定命令的源代碼在com.infoq.shorten.command bundle中。

如果我們安裝了命令client bundle,我們將得到一個新命令,shorten,通過OSGi shell可以調(diào)用它。要運行該命令,需要先執(zhí)行java -jar equinox.jar -console -noExit或java -jar bin/felix.jar,然后安裝bundle,之后你就可以使用該命令了:

  1. java -jar org.eclipse.osgi_* -console -noExit  
  2. osgi> install file:///tmp/com.infoq.shorten-1.0.0.jar  
  3. Bundle id is 1 
  4. osgi> install file:///tmp/com.infoq.shorten.command-1.0.0.jar  
  5. Bundle id is 2 
  6. osgi> install file:///tmp/com.infoq.shorten.tinyurl-1.0.0.jar  
  7. Bundle id is 3 
  8. osgi> install file:///tmp/com.infoq.shorten.trim-1.0.0.jar  
  9. Bundle id is 4 
  10. osgi> start 1 2 3 4 
  11. osgi> shorten http://www.infoq.com  
  12. http://tinyurl.com/yr2jrn  
  13. osgi> stop 3 
  14. osgi> shorten http://www.infoq.com  
  15. http://tr.im/Eza8 

注意,在運行時TinyURL和Tr.im服務(wù)都是可用的,但是一次只能使用一種服務(wù)。可以設(shè)置一個服務(wù)級別(service ranking), 這是一個整數(shù),取值范圍在Integer.MIN_VALUE和Integer.MAX_VALUE之間,當(dāng)服務(wù)最初注冊時給 Constants.SERVICE_RANKING賦予相應(yīng)值。值越大表示級別越高,當(dāng)需要服務(wù)時,會返回最高級別的服務(wù)。如果沒有服務(wù)級別(默認值為 0),或者多個服務(wù)的服務(wù)級別相同,那么就使用自動分配的Constants.SERVICE_PID,可能是任意順序的一個服務(wù)。

另一個需注意的問題是:當(dāng)我們停止一個服務(wù)時,client會自動失敗轉(zhuǎn)移到列表中的下一個服務(wù)。每當(dāng)該命令執(zhí)行時,它會獲?。ó?dāng)前)服務(wù)來處理URL壓 縮需求。如果在運行期間服務(wù)提供程序發(fā)生了變化,不會影響命令的使用,只要有此需求時有服務(wù)在就成。(如果你停止了所有服務(wù)提供程序,服務(wù)查找將返回 null,這將會打印出相應(yīng)的錯誤信息——好的代碼應(yīng)該確保程序能夠預(yù)防返回服務(wù)引用為null的情況發(fā)生。)

服務(wù)跟蹤

除過每次查詢服務(wù)外,還可以用ServiceTracker來代替做這一工作。這就跳過了中間獲得ServiceReference的幾步,但是要求你在構(gòu)造之后調(diào)用open,以便開始跟蹤服務(wù)。

對于ServiceReference,可以調(diào)用getService()獲得服務(wù)實例。而waitForService()則在服務(wù)不可用時阻塞一段時間(根據(jù)指定的timeout。如果timeout為0,則永遠阻塞)。我們可以如下重新實現(xiàn)shorten命令:

  1. package com.infoq.shorten.command;  
  2.  
  3. import java.io.IOException;  
  4. import org.osgi.framework.BundleContext;  
  5. import org.osgi.util.tracker.ServiceTracker;  
  6. import com.infoq.shorten.IShorten;  
  7.  
  8. public class ShortenCommand {  
  9.     protected ServiceTracker tracker;  
  10.     public ShortenCommand(BundleContext context) {  
  11.         this.tracker = new ServiceTracker(context,  
  12.             IShorten.class.getName(),null);  
  13.         this.tracker.open();  
  14.     }  
  15.     protected String shorten(String url) throws IllegalArgumentException,  
  16.             IOException {  
  17.         try {  
  18.             IShorten shorten = (IShorten)  
  19.                 tracker.waitForService(1000);  
  20.             if (shorten == null)  
  21.                 return null;  
  22.             return shorten.shorten(url);  
  23.         } catch (InterruptedException e) {  
  24.             return null;  
  25.         }  
  26.     }  

使用Service Tracker的常見問題是在構(gòu)造后忘記了調(diào)用open()。除此之外,還必須在MANIFEST.MF內(nèi)部引入org.osgi.util.tracker包。

使用ServiceTracker來管理服務(wù)依賴通常被認為是管理關(guān)系的好方法。在沒有使用服務(wù)的情況下,查找已輸出的服務(wù)稍微有點復(fù)雜:比 如,ServiceReference在其被解析為一個服務(wù)之前突然變得不可用了。存在一個ServiceReference的原因是,相同實例能夠在多 個bundle間共享,而且它可以被用來基于某些標準(手工)過濾服務(wù)。而且,它還可以使用過濾器來限制可用服務(wù)的集合。

服務(wù)屬性和過濾器

當(dāng)一個服務(wù)注冊時,可以將服務(wù)屬性一起注冊。大多情況下屬性可以為null,但是也可以提供OSGi特定或關(guān)于URL的通用屬性。例如,我們想給服務(wù)分級 以便區(qū)分優(yōu)先級。我們可以注冊Constants.SERVICE_RANKING(代表優(yōu)先級的數(shù)值),作為最初注冊過程的一部分。我們可能還想放一些 client想知道的元數(shù)據(jù),比如服務(wù)的主頁在哪兒,該站點的條款鏈接。為達此目的,我們需要修改activator:

  1. public class Activator implements BundleActivator {  
  2.     public void start(BundleContext context) {  
  3.         Hashtable properties = new Hashtable();  
  4.         properties.put(Constants.SERVICE_RANKING, 10);  
  5.         properties.put(Constants.SERVICE_VENDOR, "http://tr.im");  
  6.         properties.put("home.page""http://tr.im");  
  7.         properties.put("FAQ""http://tr.im/website/faqs");  
  8.         context.registerService(IShorten.class.getName(),  
  9.             new Trim(), properties);  
  10.     }  
  11. ...  

服務(wù)級別自動由ServiceTracker及其他對象來管理,但也可以用特定條件來過濾。Filter是由LDAP風(fēng)格的過濾器改編而來的,其使用了一種前綴表示法(prefix notation)來 執(zhí)行多個過濾。雖然多數(shù)情況下你想提供類的名字(Constants.OBJECTCLASS),但你也可以對值進行檢驗(包括限制連續(xù)變量的取值范 圍)。Filter是通過BundleContext創(chuàng)建的;如果你想跟蹤實現(xiàn)了IShorten接口的服務(wù),并且定義一個FAQ,我們可以這樣做:

  1. ...  
  2. public class ShortenCommand  
  3.     public ShortenCommand(BundleContext context) {  
  4.         Filter filter = context.createFilter("(&" +  
  5.             "(objectClass=com.infoq.shorten.IShorten)" +  
  6.             "(FAQ=*))");  
  7.         this.tracker = new ServiceTracker(context,filter,null);  
  8.         this.tracker.open();  
  9.     }  
  10.     ...  

在定義服務(wù)時可以被過濾或可以設(shè)置的標準屬性包括:

◆service.ranking (Constants.SERVICE_RANKING) - 整數(shù),可以區(qū)分服務(wù)優(yōu)先級
◆service.id (Constants.SERVICE_ID) - 整數(shù),在服務(wù)被注冊時由框架自動設(shè)置
◆service.vendor (Constants.SERVICE_VENDOR) - 字符串,表明服務(wù)出自誰手
◆service.pid (Constants.SERVICE_PID) - 字符串,代表服務(wù)的PID(persistent identifier)
◆service.description (Constants.SERVICE_DESCRIPTION) - 服務(wù)的描述
◆objectClass (Constants.OBJECTCLASS) - 接口列表,服務(wù)被注冊在哪些接口下

過濾器語法在OSGi核心規(guī)范的 3.2.7節(jié) “Filter syntax”中有定義。最基本的,它允許如等于(=)、約等于(~=)、大于等于、小于等于以及子字符串比較等操作符。括號將過流器分組,并且可以結(jié)合 使用“&”、“|” 或“!”分別代表and、or和not。屬性名不是大小寫敏感的,值可能是(如果不用~=作比的話)。“*”是通配符,可用來支持子字符串匹配,比如 com.infoq.*.*。

總結(jié)

本文中,我們介紹了如何使用服務(wù)進行bundle間通信,以替代直接類引用的方法。服務(wù)可以讓模塊系統(tǒng)動態(tài)化,這樣就能應(yīng)對在運行時服務(wù)的變化問題。我們 還接觸到了服務(wù)級別、屬性及過濾器,并使用標準服務(wù)跟蹤器來更容易的訪問服務(wù)并跟蹤變化的服務(wù)。

責(zé)任編輯:佚名 來源: InfoQ
相關(guān)推薦

2017-05-18 11:43:41

Android模塊化軟件

2021-12-16 22:02:28

webpack原理模塊化

2010-01-21 09:27:30

模塊化的優(yōu)點NetBeans

2009-12-10 11:04:08

Java模塊化OSGiJigsaw

2016-12-14 14:50:26

CSS預(yù)處理語言模塊化實踐

2015-10-10 11:29:45

Java模塊化系統(tǒng)初探

2019-08-28 16:18:39

JavaScriptJS前端

2017-08-08 16:07:57

Android 模塊化架構(gòu)

2017-08-11 16:10:36

微信Android實踐

2014-04-27 10:16:31

QCon北京2014Andrew Bett

2018-03-21 21:31:28

Java9編程Java

2016-11-08 20:31:19

同方服務(wù)器模塊化

2025-05-12 08:45:00

模塊化FastAPI路由分發(fā)

2020-09-17 10:30:21

前端模塊化組件

2013-08-20 15:31:18

前端模塊化

2017-05-18 10:23:55

模塊化開發(fā)RequireJsJavascript

2022-03-11 13:01:27

前端模塊

2020-09-18 09:02:32

前端模塊化

2017-06-12 10:45:36

2022-02-17 16:22:41

高壓直流模塊化UPS
點贊
收藏

51CTO技術(shù)棧公眾號