结合提供者模式解析Jenkins源码国际化的实现
關鍵字:提供者模式,設計模式,github,gerrit,源碼學習,jenkins,國際化,maven高級,maven插件
本篇文章的源碼展示部分由于長度問題不會全部粘貼展示,或許只是直接提及,需要了解的朋友請fork in github,文中會給出源碼地址。
源碼的研究策略
從這篇文章開始,陸續要展開一些源碼分析的內容,既然確立了這個目標,就要尋找研究源碼的策略,經過各方面的取經和自己的總結,接下來我將采取的策略為:
- 從最早的release版本開始,任何偉大而復雜的工程可能都源自于“helloworld”,從最初的版本(如果你能找到的話)開始看,可能會降低很多難度,隨著工程的不斷升級,根據release歷史,可以跟蹤到每次大的升級更新內容。
- 采用github。作為世界最大的源碼庫,github使用非常方便,并且我也在上面有很多自己的repo。可以直接fork官方源碼然后加入自己的調試研究過程,可以記錄下每一次的更新與變化,我想這也是github除了保存自己的代碼以外最為重要的功能之一。
- 本地工程運行,按圖索驥,編譯調試,理解設計模式的一些常見命名方式。
- 結合官方手冊(注意要與當前源碼release版本相一致)get started, API,使用UML,分析核心功能模塊的實現。
- 修改源碼工程,添加自己的注釋,增加自己的代碼,以支持模擬業務場景。同時要保存自己的github提交歷史,這也是學習過程的記錄。
在以下文章分析過程中,我會通過這種格式來記錄每一個我突發奇想的可以用來實驗Jenkins源碼的業務需求,這些需求會在未來繼續研究源碼的文章中進行實現。
搭建源碼開發環境
一、github本地配置修改
由于本地存在其他git庫的配置并且他們集成了gerrit,所以如果我想在本地配置一套github的開發環境,必須要做些改變。如果你的機器是純凈的,大可不必有此顧慮,直接配置成全局變量即可。
git配置文件
git的默認配置是在用戶home目錄下的.gitconfig文件,這個文件我是不可以修改的,否則會影響現有庫的使用。而在每個git工程中還有.git目錄,這下面的config就是該項目的本地Git配置,相當于復寫home目錄下的.gitconfig文件,home目錄下的對應的是global配置,項目本地的對應的是local配置。
gerrit
- 代碼審核服務器,一種免費、開放源代碼的代碼審查軟件,使用網頁界面。
- 同一個團隊的軟件程序員,可以相互審閱彼此修改后的程序代碼,決定是否能夠提交,退回或者繼續修改。
- 通過鉤子hooks/commit-msg程序,將每次推送的提交附屬上唯一的Change-Id,從而轉化為一個一個的代碼審核任務。
- 代碼審核工作流,完全在網頁上面操作,其中涉及到comments,Code-Review,Verified,submit,merge等操作。
- gerrit同時也是一個git的版本庫,一般用于維護項目的主干分支,各開發者可以將本地庫與其進行pull,merge等操作。
本地git配置文件修改
1.刪除hooks
目標確定為git工程下的.git目錄,首先刪除其中的hooks文件夾(hooks默認為空,如果安裝了gerrit,每次clone時會同步下載hooks/commit-msg鉤子程序),要知道gerrit的集成主要就靠這個鉤子,這個鉤子的作用就是每次在你提交代碼時,默認附屬上一串Change-Id,這樣一來就將你的每一次提交建立了一個主鍵,通過這個主鍵去review,merge等
2.配置用戶名和郵件
在git工程下直接配置上git config user. name 和user.email即可使用當前配置而不是用戶目錄下的.gitconfig的默認配置。請參考Setting your username in Git
3.git提供ssh和http兩種交互方式
這里采用http的方式,它可以繞過防火墻和網絡代理,很方便,但是每次與遠端庫交互的時候都要驗證賬戶和密碼。請參考remote url method
- ssh
ssh的方式要在遠端庫中配置上本地的id_rsa.pub,從而實現免密認證。
- http
http的話,直接使用credential.helper store來存儲用戶名密碼,可避免日后必須始終輸入賬號密碼的麻煩。具體操作如下:
evsward@lwbsPC:~/work/github/mainbase$ git config credential.helper store evsward@lwbsPC:~/work/github/mainbase$ git push origin master Username for 'https://github.com': evsward Password for 'https://evsward@github.com': Everything up-to-date evsward@lwbsPC:~/work/github/mainbase$ git push origin master Everything up-to-datehttp修改存儲密碼的方式以上方式會在根目錄下建立一個.git-credentials的文件明文存儲密碼。雖然可以指定該文件的訪問權限,我仍然覺得很不安全,所以采用另外一種方式——存儲于緩存。請參考Caching your GitHub password in Git,延長默認緩存時間從15分鐘改為1小時。如下方式執行以后,會在用戶根目錄下生成一個文件夾.git-credential-cache,里面存儲一個socket的設備文件,用于緩存用戶名密碼,通常手段無法讀取這個文件,采取緩存用戶名密碼的方式比起上一種直接存儲的方式要安全一些。(注意:當你的系統仍需連接其他git庫的時候,參數不要使用global,全部設置為local即默認)另外,同一個github下的不同項目只要存儲過一次賬號密碼以后,任何項目在其本地執行
git config credential.helper 'cache --timeout=3600'
不必初始化存入密碼,即可立即免密使用,因為同一個github賬戶下的項目訪問時的賬戶密碼是相同的,默認都是從用戶根目錄下的.git-credential-cache去讀取,因此,同一個github賬戶初始化過程只需要一次即可。當然了,超過了我們設定的緩存時限1個小時,就需要重新輸入了。下面是具體操作方式:
evsward@lwbsPC:~/work/github/mainbase$ git config credential.helper 'cache --timeout=3600' evsward@lwbsPC:~/work/github/mainbase$ git push origin master Username for 'https://github.com': evsward Password for 'https://evsward@github.com': Everything up-to-date evsward@lwbsPC:~/work/github/mainbase$ git push origin master Everything up-to-dategit修改歷史提交記錄
一般來說是直接reset + commitId,然后git push -f <remote> <branch>到遠程庫直接刪除commitId以后的所有提交歷史,請參考git如何修改已提交的commit
二、Jenkins項目源碼
1.首先fork Jenkins源碼到自己的賬戶,并下載到本地。
2.同步更新,Configuring a remote for a fork -> Syncing a fork
Jenkins 業務構想之一:監控Jenkins 源代碼,如果有任何更新,則fetch到本地,然后同步推送至我的github庫。
3.開始檢查jenkins 的release版本,找到第一個發布在github上的release版本1.312,可惜的是這個歷史版本因為太古老只留下了zip的下載方式,直接下載下來,jenkins-1.312.zip。
4.github網頁端新建一個repo起名為jenkins-1.312,將這個空項目clone到本地,然后導入前面下載的jenkins-1.312.zip解壓出來的文件。
5.注意新clone下來的github項目一定要先刪除hooks,配置好user. name,email以及credential,然后push到github遠端。
6.eclipse通過檢測pom文件將jenkins1.312以maven項目導入。
三、Maven構建源碼工程
本文就細細地將研究過程中遇到的所有可記錄的知識點都寫下來。
配置Maven
1.去Maven下載一個zip包,我下載的是Maven3.5.2
2.解壓縮,打開conf/setting.xml,修改localRepository到你預設的本地Maven資源庫。
3.修改mirror,添加阿里云maven庫
4.在eclipse中配置上剛剛下載并修改好的maven地址,同時別忘記更改user-setting。
5.linux下配置maven環境變量(Windows的配置這里不再贅述),在用戶根目錄下打開.profile,增加export MAVEN_HOME=/home/CORPUSERS/evsward/work/apache-maven-3.5.2,并將$MAVEN_HOME/bin添加到PATH中去。
6.terminal下輸入mvn -v測試。
開始構建
eclipse中直接使用clean project來觸發maven重構工程,但是發生錯誤,我們剛配置的阿里云的maven庫似乎連接不上,我按圖索驥,使用瀏覽器對該url路徑進行了檢查,確定了這個文件確實是存在于阿里云上面的。
下面我在terminal中,定位到項目路徑下,使用命令去測試mvn install(安裝artifacts,compile是編譯工程代碼,package是為現有工程打包并上傳到maven庫),錯誤仍舊是那樣。所以目前的問題是瀏覽器可以訪問,但是terminal和eclipse無法訪問。
我又嘗試了在terminal中直接wget,仍然是好使的,我將vpn配發的proxy路徑配置到$MAVEN_HOME/conf/setting.xml中以后,開始工作了!
<proxies><proxy><id>A</id><active>true</active><protocol>http</protocol><username>evsward</username><password>xxxxxxx</password><host>proxy.xxxx.net</host><port>8080</port></proxy><proxy><id>B</id><active>true</active><protocol>https</protocol><username>evsward</username><password>xxxxxxx</password><host>proxy.xxxxxx.net</host><port>8080</port></proxy></proxies>阿里云的Maven庫還是非常全的!
我們先terminal本地install一下,最終Maven安裝artifacts結果如下:
[INFO] ------------------------------------------------------------------------ [INFO] Reactor Summary: [INFO] [INFO] Hudson main module ................................. SUCCESS [04:48 min] [INFO] Hudson remoting layer .............................. SUCCESS [01:58 min] [INFO] Hudson CLI ......................................... SUCCESS [02:31 min] [INFO] Hudson core ........................................ FAILURE [05:33 min] [INFO] Hudson Maven PluginManager interceptor ............. SKIPPED [INFO] Hudson Maven CLI agent ............................. SKIPPED [INFO] Maven Integration plugin ........................... SKIPPED [INFO] Hudson war ......................................... SKIPPED [INFO] Test harness for Hudson and plugins ................ SKIPPED [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 22:49 min [INFO] Finished at: 2017-11-22T17:12:58+08:00 [INFO] Final Memory: 30M/164M [INFO] ------------------------------------------------------------------------總共耗時近23分鐘,只有一項Hudson core編譯失敗了,其他均成功了。
Jenkins 業務構想之二:每次的源碼更新,本地要自動執行mvn install去編譯,這樣就為我們真正的開發節省了很多時間。
現在去查看一下我們的repo目錄:
evsward@lwbsPC:~/work/maven-repo$ ls ant commons-digester geronimo-spec net antlr commons-discovery httpunit org aopalliance commons-el javanettasks oro args4j commons-fileupload javax plexus asm commons-httpclient jaxen qdox avalon-framework commons-io jdom slide backport-util-concurrent commons-jelly jfree stax ch commons-lang jline velocity classworlds commons-logging jtidy xalan com commons-pool junit xerces commons-beanutils commons-validator log4j xml-apis commons-cli de logkit xom commons-codec dom4j mx4j xpp3 commons-collections doxia nekohtmlevsward@lwbsPC:~/work/maven-repo$ du -sh 115M可以看到,原來空空如也的本地repo已經被填入了115M的不同的依賴包,這些都是從之前我們配置的mirror——阿里云下載過來的。
下面我們轉戰到IDE,刷新一下項目,工程在Maven的幫助下自動進入安裝階段。
- 失敗一次
可惜最終還是沒有build成功,報錯信息顯示有些依賴包在阿里云上面無法找到,看來阿里云還是不夠全啊。
- 失敗二次
于是我將conf/setting.xml中的mirror內容注釋掉了,重新運行mvn package從maven中央庫下載,build又開始工作了!(之前加的mirror不是當時無法download的根源問題,根源問題已解決,是proxy的問題)
- 失敗N次
失敗已經持續了10個小時,轉去翻官方文檔。
重新出發
由于沒有依據官方文檔,自己在摸索中構建導致了很多問題,無法順利構建成功,這一次依據官方文檔,Build Jenkins,我來嘗試follow一下。第一個改變就是我們丟棄了jenkins-1.312版本,直接使用jenkin最新版本,這是因為最新版本的文檔和代碼都是非常齊全,適合我們分析與研究。嫌麻煩的同學不用擔心,我會將所有的構建步驟貼在下面。
1.構建準備
使用jdk7+,maven3
2.環境變量配置
alias jdk7='export JAVA_HOME=/home/CORPUSERS/evsward/work/java/jdk1.7.0_80_x64 ; export PATH=$JAVA_HOME/bin:$PATH'3.maven 基礎構建
$ cd jenkins $ mvn -Plight-test install4.找到作者的github
If you want simply to have the jenkins.war as fast as possible (without test execution), run:mvn clean install -pl war -am -DskipTestsThe WAR file will be in war/target/jenkins.war (you can play with it) You can deactivate test-harness execution with -Dskip-test-harness最終,terminal build成功!
[INFO] ------------------------------------------------------------------------ [INFO] Reactor Summary: [INFO] [INFO] Jenkins main module ................................ SUCCESS [ 0.888 s] [INFO] Jenkins cli ........................................ SUCCESS [ 12.555 s] [INFO] Jenkins core ....................................... SUCCESS [01:31 min] [INFO] Jenkins war ........................................ SUCCESS [01:09 min] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 02:54 min [INFO] Finished at: 2017-11-23T13:13:35+08:00 [INFO] Final Memory: 86M/488M [INFO] ------------------------------------------------------------------------在目錄war/target/jenkins.war中已經在本地成功生成了jenkins.war包。但是環境依然有問題,有很多紅叉在項目里面。
localizer
打開整個jenkins工程,感覺亂七八糟頭有點大,不知道從何開始研究,本地跑起來剛剛生成的war包沒問題,但是調試起來還有很多障礙,我們就先從這些紅叉叉開始研究吧。localizer也是由kohsuke(Hudson&Jenkins的作者)寫的一個屬性文件本地化工具。先來介紹一下它的功能,它可以將屬性文件*.properties按照國際化語言設定規則轉成一個常量類文件,可以直接在其他類中調用。我們把jenkins.war包解壓縮,找到cli-2.92-SNAPSHOT.war,繼續解壓縮,進入到cli-2.92-SNAPSHOT/hudson/cli/client目錄下,可以發現:
Messages_bg.properties Messages_es.properties Messages.properties Messages.class Messages_fr.properties Messages_pt_BR.properties Messages_da.properties Messages_it.properties Messages_zh_TW.properties Messages_de.properties Messages_ja.properties這里面除了我們編寫的各個國家地區的語言屬性文件,還有一個Message.class并不是我們寫的,而是Maven生成的,這非常方便,因為屬性文件作為靜態文件,并不是類需要動態編譯,所以常量類文件可以完全被屬性文件取代,同時又能擁有常量類文件的調用便攜性。下面我們來分析一下這個工具。
1.首先去kohsuke的github庫中下載該項目
每次下載都要執行以下操作(這僅針對于我的環境):
2.導入項目到eclipse中去
查看核心類ResourceBundleHolder類,可以看到上面的holder.format方法:
其中get(LocaleProvider.getLocale())方法在類的上方也給出了。
Jenkins業務構想之三:開發一個處理國際語言本地化的工具系統
每種語言都有一個文件后綴,例如漢語是zh,這里是全世界關于這個語言后綴的列表。取其中ISO 639-1的值。
源碼相關知識,對象的軟引用SoftReference,弱引用WeakReference。弱引用HashMap:WeakHashMap。強引用也是類加載器引用a classloader refernce
3.介紹一種緩存的使用方法
private static final Map<Class<?>, WeakReference<ResourceBundleHolder>> cache =new WeakHashMap<Class<?>, WeakReference<ResourceBundleHolder>> ();public synchronized static ResourceBundleHolder get(Class<?> clazz) {WeakReference<ResourceBundleHolder> entry = cache.get(clazz);if (entry != null) {ResourceBundleHolder rbh = entry.get();if (rbh != null) return rbh;}ResourceBundleHolder rbh = new ResourceBundleHolder(clazz);cache.put(clazz, new WeakReference<ResourceBundleHolder>(rbh));return rbh;}分析一波:
- 這個緩存cache的類型是WeakHashMap,即弱引用的哈希Map,并且它的key為Class類,值為弱引用的ResourceBundleHolder對象。這種緩存的定義就決定了它在垃圾回收器需要的時候可以隨時自動清除針對此對象的所有弱引用。
- 我們來看下面的get(Class)方法,首先它是上了鎖的,這是必須的,因為要避免緩存同時被多線程操作造成內部數據混亂的結果。然后,它也是static的,所以這個方法是屬于類的而不是對象的,比起對象屬性,它的作用域更加廣,也保證了緩存的唯一性。
- 另外,這個緩存在系統資源緊張的時候會被隨時清理,就會出現你去按key查找卻查不到值的情況??催M去方法體,先獲得key為某Class的值,判斷其是否為空,不為空則取出,為空則說明要么是該key從來沒被存入過,要么是被垃圾回收器清理掉了,無論哪種情況,我們再存入一遍即可。注意以該Class為key的值,就是ResourceBundleHolder的弱引用對象。(所以構造器ResourceBundleHolder(Class o)雖然被丟棄,但內部仍要使用。
4.本地化文件的集合容器
private transient final Map<Locale,ResourceBundle> bundles = new ConcurrentHashMap<Locale,ResourceBundle>();java 的transient關鍵字為我們提供了便利,你只需要實現Serilizable接口,將不需要序列化的屬性前添加關鍵字transient,序列化對象的時候,這個屬性就不會序列化到指定的目的地中。
ConcurrentHashMap 是支持并發的HashMap。Locale和ResourceBundle都是Jdk中的關于國際化的類。
下面是ResourceBundleHolder最核心的方法get(Locale locale),
public ResourceBundle get(Locale locale) {ResourceBundle rb = bundles.get(locale);// 根據地區情況獲取其資源包if (rb != null)return rb;synchronized (this) {// 如果未獲取到locale對應的資源包,則為其上鎖并創建資源包rb = bundles.get(locale);if (rb != null)return rb;// 進鎖以后再查一遍。還是沒有則繼續Locale next = getBaseLocale(locale);// 擴大一級搜索范圍String s = locale.toString();// 這個owner就是Message類,拼串以后就是例如Message_zh.properties,getResource查找帶有給定名稱的資源。URL res = owner.getResource(owner.getSimpleName() + (s.length() > 0 ? '_' + s : "") + ".properties");if (res != null) {// 找到對應屬性文件try {URLConnection uc = res.openConnection();uc.setUseCaches(false);InputStream is = uc.getInputStream();// ResourceBundleImpl是自己實現的一個可根據 InputStream// 創建屬性資源包。構造器是繼承實現ResourceBundleImpl bundle = new ResourceBundleImpl(is);is.close();rb = bundle;if (next != null)// 多線程操作,當涉及事務操作的時候要先做檢查// ResourceBundle類可以根據parent屬性找到相近的屬性文件,而不是查不到就直接返回null.bundle.setParent(get(next));bundles.put(locale, bundle);} catch (IOException e) {MissingResourceException x = new MissingResourceException("Unable to load resource " + res,owner.getName(), null);x.initCause(e);throw x;}} else {if (next != null)bundles.put(locale, rb = get(next));elsethrow new MissingResourceException("No resource was found for " + owner.getName(), owner.getName(),null);}}return rb;}總結一下ResourceBundleHolder類,就是它是一個序列化的本地化資源數據的緩存,緩存中存儲了多個key為類,值為該類為owner的ResourceBundleHolder類的鍵值對。而每個ResourceBundleHolder對象會維護一個不序列化且外部不可修改的成員屬性二級緩存Map,該Map會存儲每次查詢過的本地化文件數據,如果沒有則會新插入數據。在插入新數據時,要根據本地化參數Locale去查找相近的屬性文件,然后將該文件存入資源包作為前面說的那個Map的值。
5.Message類
按圖索驥,下面來分析上面提到的那個由Maven自動生成的Message類,我們將它反編譯看一下:
這個類是直接使用我們剛剛分析過的ResourceBundleHolder類,其中還調用到了Localizable類,下面來分析一下Localizable類,然后再繞回來繼續分析它。
6.Localizable類
Localizable類就是一個針對本地資源包國家地區數據的一個封裝類,該類有一個ResourceBundleHolder的私有成員對象。然后比較重要的就是它的toString(Locale locale)方法:
這里面調用到了我們分析ResourceBundleHolder類中的核心方法get(Locale),也即通過地區條件Locale查找對應的屬性文件。這里有個"key",代表的是屬性文件內部的數據的key(屬性文件內部數據結構也是key-value)。
7.最終目的
最終目的就是在資源包中找到屬性文件,然后在該文件中找到key為"CLI.VersionMismatch"的值,用參數args內容通過MessageFormat.format替換掉值里面的占位符。
綜上分析,Message類的
return new Localizable(holder, "CLI.VersionMismatch", new Object[0]);返回的即是包含上面toString需要的三個參數的Localizable對象,當在Message類的更外部調用的時候,會讓這個對象toString輸出,而
public String toString() {return toString(LocaleProvider.getLocale());}所以就調用回了toString(Locale locale)方法,最終實現了我們剛才說的最終目的。
而LocaleProvider.getLocale()就是一個緩存存儲的就是當前本地化數據,如語言、國家地區等。
public static final LocaleProvider DEFAULT = new LocaleProvider() {public Locale get() {return Locale.getDefault();}};一步步跟進去到jdk的Locale類中,找到對應方法:
private static Locale initDefault() {String language, region, script, country, variant;language = AccessController.doPrivileged(new GetPropertyAction("user.language", "en"));// for compatibility, check for old user.region propertyregion = AccessController.doPrivileged(new GetPropertyAction("user.region"));if (region != null) {// region can be of form country, country_variant, or _variantint i = region.indexOf('_');if (i >= 0) {country = region.substring(0, i);variant = region.substring(i + 1);} else {country = region;variant = "";}script = "";} else {script = AccessController.doPrivileged(new GetPropertyAction("user.script", ""));country = AccessController.doPrivileged(new GetPropertyAction("user.country", ""));variant = AccessController.doPrivileged(new GetPropertyAction("user.variant", ""));}return getInstance(language, script, country, variant, null);}以上的方法通過本地方法(native method)獲取機器的語言,國家地區等信息。
提供者模式
首先展示一下上面localizer的類圖,localizer就使用到了提供者模式,因為我們看到了LocaleProvider,我們通過它的類圖來研究和學習提供者模式。
以上LocaleProvider的DEFAULT屬性為LocaleProvider本身的匿名內部類,這里可以再次重申
繼承了抽象類的類必須實現其抽象方法。
提供者模式并非一個全新的主意,它主要從流行的策略模式發展而來??焖贋g覽下策略模式是個不錯的想法。
提供者模式是由.net2.0提出的,雖然語言與java不同,但是設計模式是跨語言的。有了提供者模式,很多時候可以用它來代替策略模式,他們的角色也是非常類似的。
- 角色
- provider類,用于統籌管理具體provider對象,是一個抽象類
- XXXProvider類,繼承自Provider,是具體的provide類,有自己的方法實現
通過與策略模式對比,我們可以發現LocaleProvider很神奇,有點與策略模式相類似但又不太一樣,下面具體分析,
所以結論是什么?LocaleProvider類將策略模式中的Context類和Strategy類合并了起來。最終所有的模式其實都匯聚到這一個類中,然而這并非不符合“單一指責原則”,因為LocaleProvider類的職責自始至終都是一個,那就是決定Locale對象。
以后如果遇到這種情況,我們也可以使用這種模式創建我們的Provider,決定(服務?)某個類的對象。
Message.java
Message.java是整個localizer包的出口。它的功能是:
以上兩種方式的內部實現分別為
MessageFormat.format(get(LocaleProvider.getLocale()).getString(key),args);
MessageFormat.format(holder.get(locale).getString(key),(Object[])args);
這兩種實現基本是一致,只是調用方式稍有不同,他們均可以實現按照語言本地化的方式調用字符串,并用參數替換占位符,格式化該字符串的功能。
但是我們發現有意思的是,如果你是第一次下載下來localizer的源碼,會發現并不存在這個Message.java的文件。經過分析才知道,該類文件是通過Maven的插件自動創建的。
maven-localizer-plugin
這個Maven插件也屬于localizer包的一部分,它的功能就一個:自動創建上面提到的那個Message.java類文件。
首先先來思考,這個Message.java的類文件(也就是上面提及的KeyName類)如此重要,是外部調用的接口,為什么要自動生成?
原因很簡單,因為太麻煩。我們定義屬性文件的時候,基本已經把所有的數據按照key-value的形式寫入,同時又創建了多個相同結構,不同翻譯版本的value的地區語言屬性文件。Message類文件需要按照屬性文件內部的key來生成對應的方法,這個過程就是復制粘貼還容易出錯的工作量很大的枯燥的工程,因此,通過插件去讀取這些屬性文件然后自動生成是比較好的選擇。
下面針對Maven如何創建一個插件來另開一個章節仔細介紹。
Maven插件
Maven本身只是提供了一個執行環境,所有的具體操作包括打包、單元測試、代碼檢查、版本規則等等都是通過Maven插件完成的。為了讓Maven完成不同的任務,我們要為它配置各種插件。
archetype:generate:從archetype創建一個Maven項目
首先Archetype也是Maven的一個插件。
創建一個新的Maven工程,我們需要用到Maven的archetype,archetype是一個模板工具包,定義了一類項目的基本架構。
- Maven通過不同的archetype為程序員提供了創建Maven項目的模板,同時也可以根據已有的Maven項目生成自己團隊使用的參數化模板。
- 通過archetype,開發人員可以很方便的復用一類項目的最佳實現架構到自己的項目中去。
- 在一個Maven項目中,開發人員可以通過archetype提供的范例快速入門并了解該項目的結構與特點。
Maven的Archetype包含:
- maven-archetype-plugin: Archetype插件。通過該插件,開發者可以在Maven中使用Archetype。它主要有兩個goal:
- archetype:generate:從archetype 中創建一個Maven項目。
- archetype:create-from-project:從已有的項目中生成archetype。
- archetype-packaging:用于描述Archetype的生命周期與構建項目軟件包。
- archetype-models:用于描述類和引用。
- archetype-common:核心類
- archetype-test:用于測試Maven archetype的內部組件。
下面利用Archetype具體創建一個Maven項目,這里使用命令行的方式,IDE只是集成了這些功能,最終仍舊是轉化成命令行的方式,所以理解了命令行操作,IDE的操作也就直接掌握了。
- 執行mvn archetype:generate,終端會顯示開始下載很多archetype,最終穩定在一個讓你輸入一個編號的界面。這個編號有個默認的1082,對應的是maven archetype quickstart。如果直接回車則默認選擇該quickstart的archetype為你構建一個Maven項目?;剀囈院髸屇氵x擇一個quickstart的版本,默認是最近穩定版。繼續回車會讓你默認輸入
按照上面傻瓜式的輸入,就創建了一個完整的Maven工程,我們將其導入eclipse,然后觀察它的目錄結構??梢园l現src/main/java和src/test/java已經成為了source folder,其中也包含例子程序,并且該項目也引用了jdk,mavne默認加了一個junit的依賴。
pom.xml
最后主要內容為查看該項目的pom文件。
非常簡潔,只有一個junit的依賴,其他的都是常見的屬性信息字段。那么問題是剛剛講過的Maven的其他強大的功能所依賴的那些插件在哪里定義的呢?
實際上,我們項目中的pom文件是繼承于一個Super Pom,我們在該項目目錄下的終端里輸入
mvn help:effective-pom
就會展示一個完整的包含其super pom內容的pom文件,完整的pom文件太長了,就不展示在這里了,核心思想就是我們項目中的pom文件是繼承一個super pom的,所以項目內的pom可以僅關注于本業務的依賴定義即可,Maven默認的功能插件支持在super pom中都會默認幫你配置好。
archetype:create-from-project:從已有的項目中生成archetype
在上面通過archetype生成了Maven工程以后,我們對其進行一個針對我們組內開發需求,加入依賴包,創建示例程序等,抽象出來一個我們自己的maven項目構建模板。然后在項目根目錄終端在中輸入:
mvn archetype:create-from-project
執行完以上命令以后,就可以在target/generated-sources/archetype目錄下生成一個archetype目錄,進去這個目錄,然后mvn install就可以將該archetype安裝到本地倉庫,如果要共享到組內,則可以使用mvn deploy安裝到nexus等公共倉庫。非常方便。
創建一個自己的maven插件
學習了以上maven archetype的知識,我們要通過archetype創建一個自定義的maven插件開發工程,archetype選擇maven-archetype-mojo。然后按照上面講過的內容將該Maven工程創建成功。然后我們來觀察這個項目的結構和內容,
- pom.xml文件中的packaging字段的值為maven-plugin,這與我們其他的maven項目不同,其他的項目可能是jar,war,hpi(Jenkins插件安裝包)等。
- 示例程序中,我們發現了一個Mojo結尾的類,這里我們可以轉到 maven-localizer-plugin,可以看到GeneratorMojo,它繼承自org.apache.maven.plugin.AbstractMojo。它的類注解有兩個新東西:
可以發現goal字段的generate對應的就是GeneratorMojo的注解@goal generate,這是為查找插件使用的。
這個注解定義了插件在Maven的哪一個生命周期中運行。Maven構建的生命周期,以下通過一個表格來展示。
| validate | 檢查工程配置是否正確,完成構建過程的所有必要信息是否能夠獲取到。 |
| initialize | 初始化構建狀態,例如設置屬性。 |
| generate-sources | 生成編譯階段需要包含的任何源碼文件。 |
| process-sources | 處理源代碼,例如,過濾任何值(filter any value)。 |
| generate-resources | 生成工程包中需要包含的資源文件。 |
| process-resources | 拷貝和處理資源文件到目的目錄中,為打包階段做準備。 |
| compile | 編譯工程源碼。 |
| process-classes | 處理編譯生成的文件,例如 Java Class 字節碼的加強和優化。 |
| generate-test-sources | 生成編譯階段需要包含的任何測試源代碼。 |
| process-test-sources | 處理測試源代碼,例如,過濾任何值(filter any values)。 |
| test-compile | 編譯測試源代碼到測試目的目錄。 |
| process-test-classes | 處理測試代碼文件編譯后生成的文件。 |
| test | 使用適當的單元測試框架(例如JUnit)運行測試。 |
| prepare-package | 在真正打包之前,為準備打包執行任何必要的操作。 |
| package | 獲取編譯后的代碼,并按照可發布的格式進行打包,例如 JAR、WAR 或者 EAR 文件。 |
| pre-integration-test | 在集成測試執行之前,執行所需的操作。例如,設置所需的環境變量。 |
| integration-test | 處理和部署必須的工程包到集成測試能夠運行的環境中。 |
| post-integration-test | 在集成測試被執行后執行必要的操作。例如,清理環境。 |
| verify | 運行檢查操作來驗證工程包是有效的,并滿足質量要求。 |
| install | 安裝工程包到本地倉庫中,該倉庫可以作為本地其他工程的依賴。 |
| deploy | 拷貝最終的工程包到遠程倉庫中,以共享給其他開發人員和工程。 |
所以該注解定義了maven-localizer-plugin插件的執行時間是在generate-sources階段,也就是在生成工程包中需要包含的資源文件的階段,會將Message.java生成。
- MavenProject屬性
GeneratorMojo類包含一個MavenProject的對象屬性,該屬性并未賦值,它可以在插件運行時通過@parameter expression="${project}"將maven項目注入(Maven自己的IoC容器Plexus)到該屬性對象中去。在使用MavenProject類時,要在pom中加入依賴
<dependency> <groupId>org.apache.maven</groupId> <artifactId>maven-project</artifactId> <version>2.2.1</version> </dependency>即可使用該類。
- execute方法
繼續研究GeneratorMojo類,它實現了AbstractMojo類以后,就會默認必須實現一個execute方法。這個方法就是該插件功能的核心實現。回到我們的Maven插件開發項目中去,簡單編寫execute的內容,最終我們的測試Mojo類的完整內容如下:
然后對整個Maven項目執行mvn clean install
build success以后執行mvn com.evsward:test-maven-plugin:0.0.1-SNAPSHOT:evswardtest
輸出內容如下:
改善插件goal的命令
mvn com.evsward:test-maven-plugin:0.0.1-SNAPSHOT:evswardtest
這個命令實在是太長很麻煩,不像我們之前執行的mvn install等,因此我們要針對我們的命令進行改善,這就需要使用別名的方式代替冗長的命令,有兩點要求:
- 插件工程的命名規則必須是xxx-maven-plugin或者maven-xxx-plugin,我們的工程是test-maven-plugin,已經滿足了這個命名規則。(經過嘗試這一條并不應驗)
- Maven默認搜索插件只會在org.apache.maven.plugins和org.codehaus.mojo兩個groupId下搜索,我們要讓它也來搜索我們自己的groupId,就要在Maven的setting.xml中加入
所以最終命令執行
mvn test-maven-plugin:evswardtest
即可。
GeneratorMojo的execute方法
GeneratorMojo除了注入project屬性以外,還通過@parameter注入了outputDirectory,fileMask,outputEncoding,keyPattern,generatorClass,strictTypes,accessModifierAnnotations,他們分別都是maven build過程中的一些屬性內容。
// packaging 方式為pom的跳過 String pkg = project.getPackaging(); if(pkg!=null && pkg.equals("pom"))return;execute方法首先要確認packaging方式,如果是pom方式則不處理。
下面則是一系列java io相關的文件寫入工作,文件過濾器FileFilter可以搜索屬性文件或結尾包含"_xx"的文件,將他們通過一系列處理最終調用ClassGenerator的build方法完成寫入工作。
TODO: java io 方面具體的深入研究請關注我即將發布的文章。
下面是maven-localizer-plugin插件中涉及類生成工作的類圖。
本文總結
通過本文的研究,我們深入學習了:
- Maven的配置使用,模板架構,工程創建,插件開發,部署等高級使用方法。這部分源碼地址在test-maven-plugin
- Jenkins源碼中所有涉及屬性文件的操作工具localizer以及其開發的maven-localizer-plugin插件,并完全研究了localizer的源碼
- 通過研究localizer源碼,我們復習了設計模式中的策略模式,同時也學習了新型的提供者模式。
- 最后也是本文的初衷,涉及Jenkins源碼部分,我們僅是完成了對其國際化工具的實現,這對于整套源碼來講只是冰山一角,之后會隨著越加深入而展開更多的Jenkins源碼研究課題。
轉載于:https://www.cnblogs.com/Evsward/p/localizer.html
總結
以上是生活随笔為你收集整理的结合提供者模式解析Jenkins源码国际化的实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 采用集成的Windows验证和使用Sql
- 下一篇: 4028: [HEOI2015]公约数数