1. 程式人生 > >Hudson外掛開發入門體驗

Hudson外掛開發入門體驗

持續整合(CI)將軟體專案流程的各個階段進行自動化部署,從build, deploy, test automation到coverage分析,全部實現自動完成,而不需要每天的手工操作。

在敏捷開發過程中,持續整合大大提高了團隊的工作效率,開發和測試人員可以專注於程式碼與測試用例的編寫,而不需要過多關注編譯和部署。每天夜晚進行持續整合的自動化部署,第二天可以馬上開始測試和分析前日的測試效果與程式碼覆蓋率,和敏捷開發的理念結合的恰到好處。

下面我就來介紹下如何開發一個Hudson的外掛。


首先你要有Maven 2和JDK1.6以上,這是必須的。然後在你的Maven 2的setting.xml 檔案中加入下列程式碼 

[html] view plaincopyprint?
  1. <pluginGroups>
  2. <pluginGroup>org.jvnet.hudson.tools</pluginGroup>
  3.  </pluginGroups>
  4.  <profiles>
  5. <profile>
  6.    <id>hudson</id>
  7.    <activation>
  8.      <activeByDefault/>
  9.    </activation>
  10.    <pluginRepositories>
  11.      <
    pluginRepository>
  12.        <id>m.g.o-public</id>
  13.        <url>http://maven.glassfish.org/content/groups/public/</url>
  14.      </pluginRepository>
  15.    </pluginRepositories>
  16.    <repositories>
  17.      <repository>
  18.        <id>m.g.o-public</id>
  19.        <url>http://maven.glassfish.org/content/groups/public/
    </url>
  20.      </repository>
  21.    </repositories>
  22.  </profile>
  23.  </profiles>
  24.  <activeProfiles>
  25. <activeProfile>hudson</activeProfile>
  26.  </activeProfiles>

   這樣會將你的Maven指向有著Hudson-related Maven plugins的倉庫,而且允許你使用Hudson Maven plugins的短名字來呼叫相關的外掛(例如:hpi:create 代替org.jvnet.hudson.tools:maven-hpi-plugin:1.23:create)。 

接著在CMD中輸入  [plain] view plaincopyprint?
  1. mvn hpi:create  

之後會問你一些如groupId和artifactId之類的問題,groupId填寫成你一般開發java程式碼的package資訊,例如com.webex.slim.hudsonplugin,artifactId則是你編寫此hudson外掛的名稱,例如buildslim。 

完成後計算機會自動的建立了一個專案,裡面有一些模板程式碼,可供你學習如何開始寫一個Hudson的外掛,後面的程式碼全部來自模版程式碼。如果你需要在Eclipse裡編輯外掛可以執行 

[plain] view plaincopyprint?
  1. mvn -DdownloadSources=true eclipse:eclipse  
然後你就可以在Eclipse中匯入這個專案並開始開發了。Eclipse匯入maven工程,需要安裝maven外掛,然後在工程裡選擇匯入已有專案即可。

執行前面的maven命令後,在我們的指定目錄下已經生成了一個Hudson 外掛的專案資料夾,這個目錄應該是~/artifactId/。在Eclipse中匯入這個專案,我們可以看見專案有如下的結構: 

[plain] view plaincopyprint?
  1. + src   
  2.     + main   
  3.         + java   
  4.              +  full.package.name   
  5.                     +- HelloWorldBuilder.java   
  6. +resources   
  7.              +  full.package.name   
  8.                     +- config.jelly   
  9.                     +- global.jelly   
  10.                 +- index.jelly   
  11.         + webapp   
  12.             +- help-globalConfig.html   
  13.             +- help-projectConfig.html   

 HelloWorldBuilder.java 

這個類就是具體實現某一擴充套件點的一個類,在這裡由於要擴充套件Builder這個擴充套件點,所以繼承了 Builder 這個類。在Hudson 中有很多不同種類的擴充套件點,比如Publisher、Recorder 等等。詳細的說明可以參考Hudson 的網站。 

建好工程後,已經有一些程式碼是maven自動生成的hudson外掛示例程式碼。


下面我來逐步分析這些程式碼 

[java] view plaincopyprint?
  1. @DataBoundConstructor
  2. public HelloWorldBuilder(String name) {     
  3.     this.name = name;     
  4. }     
  5. /**   
  6.  * We'll use this from the <tt>config.jelly</tt>.   
  7.  */
  8. public String getName() {     
  9.     return name;     
  10. }    

這段程式碼用於構造這個Bulider並且從相應的config.jelly中獲取相應的引數。Hudson使用了一種叫structured form submission的技術,使得可以使用這種方式活動相應的引數。 

[java] view plaincopyprint?
  1. publicboolean perform(Build build, Launcher launcher, BuildListener listener) {     
  2.      // this is where you 'build' the project   
  3.      // since this is a dummy, we just say 'hello world' and call that a build   
  4.      // this also shows how you can consult the global configuration of the builder   
  5.      if(DESCRIPTOR.useFrench())     
  6.          listener.getLogger().println("Bonjour, "+name+"!");     
  7.      else
  8.          listener.getLogger().println("Hello, "+name+"!");     
  9.     returntrue;     
  10. }     


方法perform()是個很重要的方法,當外掛執行的的時候這個方法會被呼叫。相應的業務邏輯也可以在這裡實現。比如這個perform()方法就實現了怎麼說 “Hello” 

接下來,在HelloBuilder 這個類裡面有一個叫 DescriptorImpl 的內部類,它繼承了Descriptor。在Hudson 的官方說明文件裡說Descriptor包含了一個配置例項的元資料。打個比方,我們在工程配置那裡對外掛進行了配置,這樣就相當於建立了一個插腳的例項,這時候就需要一個類來儲存外掛的配置資料,這個類就是Descriptor。 

  [java] view plaincopyprint?
  1. public String getDisplayName() {     
  2.     return"Say hello world";     
  3. }     
如上面的程式碼,可以在Descriptor的這個方法下設定外掛在工程配置頁面下現實的名字  [java] view plaincopyprint?
  1. publicboolean configure(StaplerRequest req, JSONObject o) throws FormException {     
  2.      // to persist global configuration information,   
  3.      // set that to properties and call save().   
  4.      useFrench = o.getBoolean("useFrench");     
  5.      save();     
  6.      returnsuper.configure(req);     
  7. }    
如同註釋屬所說,這個方法用於將全域性配置儲存到專案中 

注意點:

HUDSON_HOME:

Hudson需要一個位置來進行每次構建,保留相關的配置資訊,以及儲存測試的結果,這就是在部署好了Hudson環境以後,系統就會自動在當前使用者下新建一個.hudson,在linux下如:~/.hudson,我們有三種方式來改變這個路徑:

1.  在啟動servlet容器之前,設定:“HUDSON_HOME”環境變數,指向你需要設定的目錄

2.  在servlet容器之中,設定系統屬性

3.  設定一個JNDI的環境實體<env-entry>“HUDSON_HOME”指向您所想要設定的目錄

目前我們在glassfish中設定jvm-option的方式屬於第二種。

當我們設定好這個變數以後想要換個目錄,但又不想丟掉以前的配置怎麼辦,很簡單,關閉Hudson,將目錄A的內容拷貝的目錄B中去,然後重新設定“HUDSON_HOME”的值,然後重啟,你會發現之前你所作的所有配置都完好的保留了下來

1、Hudson-home的目錄結構:

HUDSON_HOME

 +- config.xml     (hudson的基本配置檔案,如:jdk的安裝路徑)

 +- *.xml          (其他系統相關的配置檔案,比如:旺旺外掛的全域性配置資訊)

 +- fingerprints   (儲存檔案版本跟蹤記錄資訊)

 +- plugins        (存放外掛)

 +- jobs

     +- [JOBNAME]      (任務名稱,對應頁面上的project name)

         +- config.xml     (任務配置檔案,類似於CC的config.xml中各個專案的配置)

         +- workspace      (SCM所用到的目錄,hudson所下載程式碼預設存放在這個目錄)

         +- builds

             +- [BUILD_ID]     (每一次構建的序號)

                 +- build.xml      (構建結果彙總)

                 +- log            (執行日誌)

                 +- changelog.xml  (SCM修改日誌)

小提示:如果你使用了e-mail來接受測試訊息,並且hudson的遷移設計到不同ip地址機器的遷移的話,可能需要去Hudson的主配置中修改一下Hudson的訪問地址

workspace:

剛才在hudson-home的目錄結構中已經看到了workspce,假設當前hudson-home為/home/hudson-home,那麼當我們在hudson上配置一個專案demo的時候,就會在新建一個目錄/home/hudson-home/demo,在第一次執行之前,在jobs下並沒有demo這個資料夾,只有當第一次執行以後才會在jobs目錄下建立demo目錄,當代碼順利從svn上下載下來時才會建立workspace資料夾,所有從svn下載下來的程式碼都會存放在這個目錄下。

1、相對路徑:

專案配置過程中,Hudson使用的是相對路徑,對於Hudson,在我們新建一個專案比如demo後,假設workspace的目錄結構為:

workspace

 +- demo

     +- pom.xml

     +- src

那麼測試報告的路徑就為demo/target/surefire-reports/*.xml,系統會自動去當前專案的workspace中去尋找這個路徑

mvn package  -- 完成程式碼開發之後執行,按照pom.xml 中的配置資訊將會打包為hpi 格式的外掛檔案,這個就是你最終可以拿來上傳給你的hudson 平臺的玩意

mvn hpi:run   -- 在本地的Jetty 中執行你的hudson 外掛,除錯專用,當然可以使用Debug 模式,執行之後,在本地訪問http://localhost:8080/ 即可見(注意不要佔用8080 埠)

mvnDebug hup:run ,debug除錯模式

下面貼出一個我自己寫的用於專案構建,自動編譯打包的Hudson外掛原始碼。

HelloWorldBuilder.java

[java] view plaincopyprint?
  1. package zygroup;  
  2. import hudson.FilePath;  
  3. import hudson.Launcher;  
  4. import hudson.Extension;  
  5. import hudson.Proc;  
  6. import hudson.util.FormValidation;  
  7. import hudson.model.AbstractBuild;  
  8. import hudson.model.BuildListener;  
  9. import hudson.model.AbstractProject;  
  10. import hudson.remoting.Channel;  
  11. import hudson.tasks.Builder;  
  12. import hudson.tasks.BuildStepDescriptor;  
  13. import net.sf.json.JSONObject;  
  14. import org.kohsuke.stapler.DataBoundConstructor;  
  15. import org.kohsuke.stapler.StaplerRequest;  
  16. import org.kohsuke.stapler.QueryParameter;  
  17. import javax.servlet.ServletException;  
  18. import java.io.IOException;  
  19. publicclass HelloWorldBuilder extends Builder {  
  20.     privatefinal String locate;  
  21.     privatefinal String cmd;  
  22.     // Fields in config.jelly must match the parameter names in the "DataBoundConstructor"
  23.     @DataBoundConstructor
  24.     public HelloWorldBuilder(String locate, String cmd) {  
  25.         this.locate = locate;  
  26.         this.cmd = cmd;  
  27.     }  
  28.     /** 
  29.      * We'll use this from the <tt>config.jelly</tt>. 
  30.      */
  31.     public String getLocate() {  
  32.         return locate;  
  33.     }  
  34.     public String getCmd() {  
  35.         return cmd;  
  36.     }  
  37.     @Override
  38.     publicboolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) {  
  39.         listener.getLogger().println("The SLiM build home is "+locate+".");  
  40.         listener.getLogger().println("The SLiM build command is "+cmd+".");  
  41.         try {  
  42.             FilePath path = new FilePath(Channel.current(),locate);  
  43.             Proc proc = launcher.launch(cmd, build.getEnvVars(), listener.getLogger(),path);  
  44.             int exitCode = proc.join();  
  45.             if (exitCode != 0returnfalse;  
  46.             returntrue;  
  47.           } catch (IOException e) {  
  48.             e.printStackTrace();  
  49.             listener.getLogger().println("IOException !");  
  50.             returnfalse;  
  51.           } catch (InterruptedException e) {  
  52.             e.printStackTrace();  
  53.             listener.getLogger().println("InterruptedException!");  
  54.             returnfalse;  
  55.           }  
  56.     }  
  57.     @Override
  58.     public DescriptorImpl getDescriptor() {  
  59.         return (DescriptorImpl)super.getDescriptor();  
  60.     }  
  61.     @Extension// this marker indicates Hudson that this is an implementation of an extension point.
  62.     publicstaticfinalclass DescriptorImpl extends BuildStepDescriptor<Builder> {  
  63.         public FormValidation doCheckName(@QueryParameter String value) throws IOException, ServletException {  
  64.             if(value.length()==0)  
  65.                 return FormValidation.error("Please set a name");  
  66.             if(value.length()<4)  
  67.                 return FormValidation.warning("Isn't the name too short?");  
  68.             return FormValidation.ok();  
  69.         }  
  70.         publicboolean isApplicable(Class<? extends AbstractProject> aClass) {  
  71.             // indicates that this builder can be used with all kinds of project types 
  72.             returntrue;  
  73.         }  
  74.         public String getDisplayName() {  
  75.             return"SLiM build";  
  76.         }  
  77.         @Override
  78.         publicboolean configure(StaplerRequest req, JSONObject formData) throws FormException {  
  79.             save();  
  80.             returnsuper.configure(req,formData);  
  81.         }  
  82.     }  
  83. }  
設定外掛相關的使用者輸入頁面的檔案config.jelly [html] view plaincopyprint?
  1. <j:jellyxmlns:j="jelly:core"xmlns:st="jelly:stapler"xmlns:d="jelly:define"xmlns:l="/lib/layout"xmlns:t="/lib/hudson"xmlns:f="/lib/form">
  2.   <!--  
  3.     This jelly script is used for per-project configuration.  
  4.     See global.jelly for a general discussion about jelly script.  
  5.   -->
  6.   <!--  
  7.     Creates a text field that shows the value of the "name" property.  
  8.     When submitted, it will be passed to the corresponding constructor parameter.  
  9.   -->
  10.   <f:entrytitle="Build Home"help="plugin/zyartifact/WEB-INF/classes/zygroup/HelloWorldBuilder/help-buildhome.html">
  11.     <f:textboxname="locate"type="text"value="${instance.locate}"/>
  12.   </f:entry>
  13.    <f:entrytitle="Build Command"help="plugin/zyartifact/WEB-INF/classes/zygroup/HelloWorldBuilder/help-cmd.html">
  14.     <f:textboxname="cmd"type="text"value="${instance.cmd}"/>
  15.   </f:entry>
  16. </j:jelly>

其中<f:entry>的help屬性指向了一個html檔案,位於程式碼中設定的位置下,可以寫入標準的html標記,用於在此輸入框右邊顯示幫助按鈕和點出幫助資訊。

該外掛的主要輸入內容是:

locate和cmd兩個字串,傳遞給build程式使用,成為locate和cmd兩個變數。用於使用者輸入構建程式碼的目錄和需要啟動構建的命令。

例如

/opt/CruiseControl/apache-ant-1.7.0/

ant antbuild

build程式得到這兩個變數後,就啟動shell並在locate目錄下執行cmd命令。這個功能在perform函式中實現。

[java] view plaincopyprint?
  1. publicboolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) {  
  2. /向hudson執行控制檯輸出日誌資訊  
  3.      listener.getLogger().println("The SLiM build home is "+locate+".");  
  4.      listener.getLogger().println("The SLiM build command is "+cmd+".");  
  5.      try {  
  6. //將locate字串轉化為hudson的FilePath型別
  7.         FilePath path = new FilePath(Channel.current(),locate);  
  8. //在path路徑下執行cmd命令
  9.          Proc proc = launcher.launch(cmd, build.getEnvVars(), listener.getLogger(),path);  
  10. //如果shell結果為失敗,則返回失敗
  11.          int exitCode = proc.join();  
  12.          if (exitCode != 0returnfalse;  
  13. //返回成功
  14.          returntrue;  
  15.        } catch (IOException e) {  
  16.     ......  
  17.        }  
  18.  }  

然後在windows的cmd或者linux的控制檯中該專案目錄下,鍵入mvn package,即可自動生成target目錄下的檔案,包括一個hpi檔案和jar檔案。

將hpi拷貝到hudson目錄的plugin目錄下,或者通過hudson的頁面上傳外掛,重啟hudson,即可使用。

這個外掛是一個build型別的外掛,會在hudson的job配置頁面,出現在build step下拉選單中,名字由HelloWorldBuilder.java的下面一個函式控制:

[java] view plaincopyprint?
  1. public String getDisplayName() {  
  2.     return"SLiM build";  
  3. }  

外掛在hudson已安裝外掛列表中顯示的名字,由該maven專案的poe.xml配置: [html] view plaincopyprint?
  1. <projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  2.     <modelVersion>4.0.0</modelVersion>
  3.     <parent>
  4.         <groupId>org.jvnet.hudson.plugins</groupId>
  5.         <artifactId>hudson-plugin-parent</artifactId>
  6.         <version>2.1.1</version><!-- which version of Hudson is this plugin built against? -->
  7.     </parent>
  8.     <groupId>zygroup</groupId>
  9.     <artifactId>zyartifact</artifactId>
  10.     <version>1.0-SNAPSHOT</version>
  11.     <packaging>hpi</packaging>
  12.     <name>SLiM build</name>
  13. </project>

這樣一個實現專案自動構建的簡單外掛就可以使用了^.^