近期接觸到Hudson的外掛程式開發,覺得還是比較好玩的,但目前這方面的資料而非常之少,於是將自己一些學習資料簡單歸納了一下,算是拋磚引玉吧
一、關於Hudson(又名Jenkins)
簡單說,它就是一個純java實現開源的持續整合軟體,一般搭載在web容器上用,有單獨war包的形式,也有內嵌jetty伺服器的安裝包。在持續整合領域中相當出名,而其中最大的因素則源自其可伸縮的外掛程式機制和強大的外掛程式支援,目前已有超過400多款支援不同持續整合特性的免費可用外掛程式。Hudson的外掛程式機制允許開發人員通過定製來做很多事情,包括自訂構建步驟、結果的展示方式、通知方式、與SCM系統的整合、測試和分析等等。
二、外掛程式開發
Hudson是基於maven的項目,其外掛程式開發也離不開maven的支援,因此有必要稍微瞭解下maven是怎麼用的:http://maven.apache.org/ 。此外,hudson提供了hpi外掛程式來實現其外掛程式開發。The Hudson HPI (Hudson Plug-in Interface) tool, 是一個Maven外掛程式,可協助開發人員建立、構建、運行和調試Hudson外掛程式項目
安裝完maven之後便可以開始玩了:
1 建立項目
找一個乾淨的地方,執行一下:mvn hpi:create 此時maven會檢查當前是否安裝了hpi外掛程式(hudson外掛程式開發的maven外掛程式,全稱為hudson plugin Interface),如果沒有將先下載安裝;如果報錯提示 無法識別 hpi命令或別名,那是maven找不到外掛程式了,開啟maven的setting.xml檔案,添加maven外掛程式尋找路徑:
<pluginGroups> <pluginGroup>org.jenkins-ci.tools</pluginGroup></pluginGroups>
建立項目成功之後,一個helloworld的骨架項目結構如下:
pom.xml - Maven POM file which is used to build your pluginsrc/main/java - java源檔案src/main/resources - 外掛程式的Jelly 視圖檔案src/main/webapp - 外掛程式的靜態資源 such as images and HTML files.
這是一標準的maven項目結構,緊接著執行一下打包試試: mvn package,在target目錄下發現外掛程式打成了jar包,另外還有一個hpi檔案。而hpi檔案便是hudson的標準外掛程式格式,可以直接安裝到已啟動並執行hudson程式中(系統設定-外掛程式-進階-上傳外掛程式)
此後執行hpi:run 可以開啟一個test模式的hudson,其內建安裝了當前開發中的外掛程式,通過localhost:8080可以訪問。hpi:run 命令包含了幾個子task:啟動jetty伺服器,添加hudson為web項目、安裝當前外掛程式。
外掛程式的work子目錄成為了當前Hudson的Home目錄,work/plugins子目錄則包含了一些hpi檔案(對應於當前hudson中的外掛程式列表);仔細點可以發現當前的目錄中有一個hpl為尾碼的檔案,其對應了當前的helloworld外掛程式項目;這是一個簡單的文字檔,其內部描述了與當前項目構建相關的檔案(包括classes、jars和resources)每次執行hpi:run命令時,HPI工具都會產生該檔案,而Hudson解釋該檔案並直接載入該外掛程式(而不需要把外掛程式打成hpi的包)此種方式也方便於部署期間的調試。
2 擴充功能
產生的helloworld項目預設添加了一個Builder的擴充類(名為HelloWorldBuilder)。Hudson的擴充機制與Eclipse有些相似,也有擴充點和擴充的概念,擴充點即是一組介面,其允許第三方開發人員實現該介面(提供擴充實現)來增強系統的功能。下面的應用將圍繞HelloWorldBuilder進行說明:
一次構建過程通常包括: SCM checkout - check out出源碼 Pre-build - 先行編譯 Build wrapper -準備構建的環境,設定環境變數等 Builder runs - 執行構建,比如調用calling Ant, Make 等等 Recording - 記錄輸出,如測試結果 Notification - 通知成員
jenkins構建器的擴充點通過Builder介面聲明,在預設情況下,jenkins內建了Ant和Maven的builder擴充實現(建立一個job,可以添加ant build step...)
產生的HelloWorld類如下:
public class HelloWorldBuilder extends Builder { //構建的執行通過實現perform方法來進行自訂 public boolean perform(AbstractBuild<?> ab, Launcher launcher, BuildListener bl) throws InterruptedException, IOException;{ ..} /* Build參數是描述了當前任務的一次構建,通過它可以訪問到一些比較重要的模型對象如: 1 Project 當前項目的對象 2 workspace 構建的工作空間 3 Result 當前構建步驟的結果 Launcher 用於啟動構建 BuildListener 該介面用於檢查構建過程的狀態(開始、失敗、成功..) 通過它可以在構建過程中發送一些控制台資訊給Hudson */ perform方法的傳回值告訴jenkins當前步驟是否成功,如果失敗了Hudson將放棄後續的步驟。 此外有一個內部靜態類,該類通過@Extension聲明告訴Hudson,這是一個擴充實現 @Extension // This indicates to Jenkins that this is an implementation of an extension point. public static final class DescriptorImpl extends BuildStepDescriptor<Builder> { public boolean isApplicable(Class<? extends AbstractProject> aClass) { // 是否對所有項目類型可用 return true; } /** * builder的顯示名. */ public String getDisplayName() { return "Say hello world"; } }
}
關於構建方法(Perform)的一個實現範例:
List<Cause> buildStepCause = new ArrayList(); buildStepCause.add(new Cause() { public String getShortDescription() { return "Build Step started by Hello Builder"; } }); listener.started(buildStepCause); //向hudson控制台輸出日誌 ArgumentListBuilder args = new ArgumentListBuilder(); if (launcher.isUnix()) { args.add("/bin/ls"); args.add("-la"); } else { args.add("dir"); //Windows } String homeDir = System.getProperty("user.home"); args.add(homeDir); try { int r; //調用外部命令,cmds傳入命令和參數;stdout方法將標準輸出重新導向到listener的流中(輸出到hudson的web控制台),join等待完成並返回結果 //可以看到launcher是相當強大的.. r = launcher.launch().cmds(args).stdout(listener).join(); if (r != 0) { listener.finished(Result.FAILURE); return false; } } catch (IOException ioe) { ioe.printStackTrace(listener.fatalError("Execution" + args + "failed")); //列印異常,標記結果 listener.finished(Result.FAILURE); return false; } catch (InterruptedException ie) { ie.printStackTrace(listener.fatalError("Execution" + args + "failed")); listener.finished(Result.FAILURE); return false; } listener.finished(Result.SUCCESS);
3 添加配置
Jenkins使用了Jelly頁面渲染技術,這是一個基於XML的服務端頁面渲染引擎,其將基於Jelly的xml標籤轉換為對應的Html標籤並輸出到用戶端。模型對象的資訊通過Jexl運算式被傳遞到頁面上(相當於Jsp的JSTL)。jelly檔案以.jelly為尾碼,在hudson中使用類全名的形式來尋找模型類對應的jelly分頁檔,如名為org.sample.hudson.HelloWorldBuilder的類,其對應的分頁檔應該存在於resource目錄的以下位置中(以classpath為根)
org/sample/hudson/HelloWordBuilder
此外hudson通過固定的命名方式來確定分頁檔屬於局部配置還是全域配置:config.jelly提供局部配置;global.jelly提供全域配置
A 局部配置詳解
config.jelly 的內容將被包含在擴充功能的配置中
以HelloWorldBuilder為例,其擴充的是一個Hudson Job的構建步驟,那麼config.jelly 提供的便是該構建步驟對應的配置內容
範例說明:
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <f:entry title="名稱" field="name"> <f:textbox /> </f:entry></j:jelly>
entry 表示用於互動的html表單域,title將作為表單域label的值
textbox 表示簡單的渲染一個text
允許為表單域增加協助說明(在頁面上對應於文字框後面出現問號按鈕,一點擊可出現提示):
在同名目錄下建立help-{fileName}.html,在該檔案中添加協助內容;協助內容允許是動態,即可以從模型中拉取資訊進行顯示,這需要將html尾碼改為jelly,而檔案的形式大致如下:
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"> <div> Welcome to ${app.displayName}. //應用的Display名稱(一般就是Hudson) Enter your name in the Field. </div></j:jelly>
jelly尾碼的檔案在渲染時會交給jelly引擎執行,因而支援動態顯示能力。
jexl運算式替換模型資料的規則:${modelName.attrName} 調用對應模組的get**方法獲得值
關於內建模型對象的說明:
1 app Hudson應用程式物件 如上面的displayName例子2 it 當前UI所屬的模型對象,在上面的Builder擴充例子中則對應於HelloWorldBuilder對象 ${it.name} 對應於builder的getName()方法3 h 一個全域的工具類,提供靜態工具方法
通過Job配置介面儲存之後,hudson會建立builder對象,並將表單值通過構造器注入,構造器聲明如下:
@DataBoundConstructorpublic HelloWorldBuilder(String name) { this.name = name;}
而builder必須提供getName方法,這樣可將配置到config.xml中(hudson使用xml儲存配置資訊)在重新開啟job配置時可自動填值
關於表單值的校正,以文本域name為例:
jelly在渲染時自動增加了ajax校正的功能指令碼,於是文字框失去焦點時會往服務端發送校正請求:
GET /job/TestProject/descriptorByName/org.sample.hudson.HelloWorldBuilder/checkName?value=xy HTTP/1.1Host: localhost:8080User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:6.0.1) Gecko/20100101 Firefox/6.0.1Accept: text/javascript, text/html, application/xml, text/xml, */*
此後Hudson找到HelloWorldBuilder,尋找doCheckName方法(在Descriptor類中尋找),如果沒有找到則忽略該請求
否則返回檢查結果在前端展示。doCheckName方法的範例實現:
public FormValidation doCheckName(@QueryParameter String value) //@QueryParameter註解表示注入http請求參數 throws IOException, ServletException { if (value.length() == 0) { return FormValidation.error("Please set a name"); } if (value.length() < 4) { return FormValidation.warning("Isn't the name too short?"); } return FormValidation.ok();}
B 全域配置詳解
如上所述,global.jelly 為全域配置頁面,樣本:
<f:section title="Hello World Builder"> <f:entry title="French" description="Check if we should say hello in French" help="/plugin/javaone-sample/help-globalConfig.html"> <f:checkbox name="hello_world.useFrench" checked="${descriptor.useFrench()}" /> </f:entry></f:section>//在jenkins的系統設定中可以找到相應的配置段落。
其中${descriptor.useFrench()} 調用builder的(getDescriptor)得到Descriptor對象,調用其useFrench方法進行取值;
help聲明了協助內容文檔位置;
在每次儲存全域配置時,jenkins都會調用調用該descriptor對象,並調用其configure方法,可以實現該方法並提供自己的定製:
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { useFrench = formData.getBoolean("useFrench"); save(); return super.configure(req,formData); }//save方法用於將當前Descriptor所提供的配置持久化(通過get**方法)//load方法用於將持久化的資訊注入到當前Descriptor中(通過set**方法)因此為了使save和load能正常工作,需要提供配置項的get/set方法使用情境:在構造器方法中執行load,將全域配置諸如到當前對象中;設定檔通過運算式調用descriptor的get**方法顯示到前端;在儲存系統配置時,configure方法中將配置讀入當前對象,並持久化。
三、其他資料
Hudson的擴充點JavaDoc:http://wiki.jenkins-ci.org/display/JENKINS/Extension+points
Hudson外掛程式開發簡單介紹:https://wiki.jenkins-ci.org/display/~martino/2011/10/27/The+JenkinsPluginTotallySimpelGuide
實現報告發布擴充(Publisher)的介紹:http://www.theserverlabs.com/blog/2008/09/24/developing-custom-hudson-plugins-integrate-with-your-own-applications/
一個Html報告發布擴充的例子(基於Selenium的報告發布擴充):
https://github.com/jenkinsci/seleniumhtmlreport/blob/master/src/main/java/org/jvnet/hudson/plugins/seleniumhtmlreport/SeleniumHtmlReportPublisher.java
Hudson外掛程式大全介紹 -
http://wiki.hudson-ci.org/display/HUDSON/All+Plugins+by+Topic