1. 程式人生 > >淺析 Jenkins 外掛開發

淺析 Jenkins 外掛開發

Jenkins 概述

Jenkins,簡單的說就是一個開源的持續整合伺服器,是 Hudson 的繼續。Jenkins 提供了易於使用的持續整合系統,使開發者更專注於業務邏輯的實現。Jenkins 能實時監控整合過程中的問題,提供詳細的日誌資訊,還能以圖表的形式顯示專案構建的情況。

Jenkins 提供了豐富的管理和配置功能,如下圖所示,包括系統設定,外掛管理,系統資訊,系統日誌,負載統計,節點管理等功能。

圖 1. Jenkins 的系統管理
圖 1. Jenkins 的系統管理

Jenkins 架構

Stapler

Stapler 是一個將應用程式物件和 URL 裝訂在一起的 lib 庫,使編寫 web 應用程式更加方便。Stapler 的核心思想是自動為應用程式物件繫結 URL,並建立直觀的 URL 層次結構。

下圖顯示了 Stapler 的工作原理:

圖 2. Stapler 工作原理
圖 2. Stapler 工作原理

上圖左邊顯示了應用程式的程式碼資訊,右邊顯示了 URL 的層次結構。通過反射機制,Stapler 可以將 URL 繫結到應用程式物件。比如說,應用程式的根物件對應 URL 的根“/”。通過 getProject(“stapler”) 方法訪問的物件將分派給 URL “/project/stapler”。通過這種方式,應用程式物件模型就直接轉化為 URL 的層次結構, 如圖中紫色箭頭所示。

Jenkins 的類物件和 URL 繫結就是通過 Stapler 來實現的。Hudson 例項作為 root 物件繫結到 URL“/”,其餘部分則根據物件的可達性來繫結。例如 Hudson 有一個 getJob(String) 方法,那麼根據上圖的介紹,可以知道 URL“/job/foo/”將繫結到 Hudson.getJob(“foo”) 返回的物件。

持久化

Jenkins 使用檔案來儲存資料(所有資料都儲存在$JENKINS_HOME)。有些資料,比如 console 輸出,會作為文字檔案儲存;有些資料則會像 Java 配置檔案格式一樣儲存;大多數的結構資料,如一個專案的配置或構建(build)記錄資訊則會通過 XStream 持久化,實際如圖 3 所示。從圖中可以看到 Jenkins 把一個 Job 的所有構建記錄都通過 XStream 記錄下來。

圖 3. Jenkins 構建記錄
圖 3. Jenkins 構建記錄

外掛

Jenkins 的物件模型是可擴充套件的,通過 Jenkins 提供的可擴充套件點,我們可以開發外掛擴充套件 Jenkins 的功能。到目前為止,Jenkins 已經支援超過 600 個外掛,這些外掛支援的功能涵蓋了軟體配置管理 (SCM)、軟體測試、通知 (Notification)、報表等方面。

Jenkins 通過單獨的類載入器載入每個外掛以避免外掛之間產生衝突。外掛就像 Jenkins 內建的其他類一樣參與到系統的活動中。另外,外掛可以通過 XStream 持久化,可以由 Jelly 提供檢視技術,可以提供圖片等靜態資源,外掛中所有的功能可以無縫的加入到 Jenkins 內建的功能上。

Jenkins 原始碼除錯和淺析

原始碼除錯

要開發 Jenkins 外掛必然離不開 Jenkins 原始碼除錯。Jenkins 官方網站上提供了原始碼除錯方式。筆者嘗試過該方法,不過不幸的是一直沒有成功,如果有讀者成功過,歡迎指導。在這裡,筆者採取了一個變通的方式來除錯 Jenkins 原始碼。

  1. 檢出 jenkins-core 專案到本地,然後 import 到 Eclipse 中;
  2. 參考 Jenkins 官方外掛開發入門文件,在 Eclipse 上新建外掛專案;
  3. 在 jenkins-core 專案中新增斷點;
  4. 選擇新建的外掛專案右鍵,選擇“Debug Configurations”,在 Source 中新增第一步的 jenkins-core 專案,如圖 4 所示:
圖 4. Debug Configurations 中 Source 的配置
圖 4. Debug Configurations 中 Source 的配置

如此配置後,啟動外掛專案,這時 Eclipse 會啟動 Jenkins,之後就會進入第 3 步設定的斷點處,如下圖所示:

圖 5. Jenkins-core 除錯
圖 5. Jenkins-core 除錯

原始碼淺析

Jenkins 程式碼的入口是如圖 5 右邊所示的 WebAppMain 類,位於 hudson(package) 下面。

清單 1. WebAppMain.java
package hudson;
 ...
 public final class WebAppMain implements ServletContextListener {
 ...
 public void contextInitialized(ServletContextEvent event) {
 ...
 initThread = new Thread("hudson initialization thread") {
 @Override
 public void run() {
 boolean success = false;
 try {
 Jenkins instance = new Hudson(home, context);
 context.setAttribute(APP, instance);

 // at this point we are open for business and serving requests normally
 LOGGER.info("Jenkins is fully up and running");
 success = true;
 } catch (Error e) {
 LOGGER.log(Level.SEVERE, "Failed to initialize Jenkins",e);
 context.setAttribute(APP,new HudsonFailedToLoad(e));
 throw e;
 } catch (Exception e) {
 LOGGER.log(Level.SEVERE, "Failed to initialize Jenkins",e);
 context.setAttribute(APP,new HudsonFailedToLoad(e));
 } finally {
 Jenkins instance = Jenkins.getInstance();
 if(!success && instance!=null)
 instance.cleanUp();
 }
 }
 };
 initThread.start();
 ...
 }
 ...
 public void contextDestroyed(ServletContextEvent event) {
 terminated = true;
 Jenkins instance = Jenkins.getInstance();
 if(instance!=null)
 instance.cleanUp();
 Thread t = initThread;
 if (t!=null)
 t.interrupt();
 }
 ..
}

由以上清單可以看到,類 WebAppMain 實現了 ServletContextListener 介面。該介面的作用主要是監聽 ServletContext 物件的生命週期。當 Servlet 容器啟動或終止 Web 應用時,會觸發 ServletContextEvent 事件,該事件由 ServletContextListener 來處理。此外,ServletContextListener 介面還定義了兩個方法,contextInitialized 和 contextDestroyed。通過方法名,我們可以看到這兩個方法中一個是啟動時候呼叫 (contextInitialized),一個是終止的時候呼叫 (contextDestroyed)。

類中通過 contextInitialized 方法初始化了一個 Jenkins 物件。如清單 1 所示,在 Servlet 容器初始化的時候,Jenkins 物件會交由 WebAppMain 的 initTread 執行緒建立。

外掛分析

Jenkins 支援的外掛有很多 (請參考 Jenkins 外掛介紹,瞭解具體支援的外掛型別)。分析現有外掛原始碼是學習 Jenkins 外掛開發一種非常好的途徑,所以在介紹實際外掛開發之前,先來分析下筆者使用過的 Dynamic Jenkins Parameters 外掛(感興趣的讀者可以到 Jenkins 官網上下載這個外掛的原始碼)。

該外掛的主要作用是在 Jenkins 構建頁面上提供了一個動態引數。每次構建被觸發時,引數中配置的 Groovy 指令碼將被呼叫,動態生成引數值。該外掛支援兩種型別的引數:簡單的文字輸入引數和下拉選擇框引數。如果是簡單文字輸入框引數,則 Groovy 指令碼必須返回一個字串才能在構建頁面正確顯示。如果是下拉選擇框引數,則指令碼必須返回一個字串列表。

清單 2 列出了 Danymic Parameter 外掛中一個非常重要的類 DanaymicParameter.java. 從這個類可以看到,它繼承了 ParameterDefinition 類。ParameterDefinition 是 Jenkins 中的構建引數定義類,所有 Jenkins 構建引數外掛開發都需要繼承這個類。

清單 2. DynamicParameter.java
public class DynamicParameter extends ParameterDefinition {
 ...
 @Extension
 public static final class DescriptorImpl extends ParameterDescriptor {
 ...
 public ListBoxModel doFillValueItems(@QueryParameter String name) {
 ...
 }
 public ListBoxModel doFillDynamicValueItems(@QueryParameter String name, 
@QueryParameter String value) {
 ...
 }
 }
 ...
}

ParameterDefinition 類對應於 Jenkins 配置 Job 時的 Parameter 選項。因為 DynamicParameter 繼承了 ParameterDefinition,所以就會在 Jenkins 的 Job 配置頁面的 Add Parameter 下拉框中看到這個外掛,如下圖所示:

圖 6. Job 配置-Dynamic Parameter
圖 6. Job 配置-Dynamic Parameter

我們看到 DynamicParameter 類裡面有個內部類:DescriptorImpl。內部類裡面有如清單 2 所示的幾個方法 doFillValueItems 和 doFillDanymicValueItems。筆者在除錯的時候發現,第一個引數 Value 的改動會觸發第二個引數 DynamicValue 選項的變動,為什麼會這樣?為什麼 DescriptorImpl 內部類中的方法名必須是 doFill+引數名+Item 的形式?帶著這些問題,我們接下來看看 DynamicParameter 中的 index.jelly 檔案。

清單 3. Index.jelly
<f:entry title="${it.name} " description="${it.description}">
 <div name="parameter" description="${it.description}">
 <j:set var="instance" value="${it}" />
 <j:set var="descriptor" value="${it.descriptor}" />
 
 <input type="hidden" name="name" value="${it.name}" />
 <input type="hidden" name="secondName" value="${it.secondName}" />

 <f:select id="select1" field="value" default="" title="${it.name}" tooltip="${it.tips1}" /><br /> <br />
 
 ${it.secondName}<br /> <br />
 <f:select id="select2" field="dynamicValue" title="${it.secondName}" tooltip="${it.tips2}" /><br /> 
 
 </div>
</f:entry>

從這個檔案中可以看到,引數 value 和 dynamicValue 的型別是 f:select,所以我們去找 select.jelly。

清單 4. Select.jelly
${descriptor.calcFillSettings(field,attrs)} 
    <!-- this figures out the 'fillUrl' and 'fillDependsOn' attribute -->

從 select.jelly 中會發現呼叫 descriptor 的 calcFillSettings 方法,descriptor 其實就是 DynamicParameter 的內部類 DescriptorImpl。通過 DescriptorImpl 的抽象父類 Descriptor 可以找到對應的 calcFillSettings 方法,清單如下:

清單 5. Descriptor.java
public void calcFillSettings(String field, Map<String,Object> attributes) {
 String capitalizedFieldName = StringUtils.capitalize(field);
 String methodName = "doFill" + capitalizedFieldName + "Items";
 Method method = ReflectionUtils.getPublicMethodNamed(getClass(), methodName);
 if(method==null)
 throw new IllegalStateException(......);

 // build query parameter line by figuring out what should be submitted
 List<String> depends = buildFillDependencies(method, new ArrayList<String>());

 if (!depends.isEmpty())
 attributes.put("fillDependsOn",Util.join(depends," "));
 attributes.put("fillUrl", String.format("%s/%s/fill%sItems", 
getCurrentDescriptorByNameUrl(), getDescriptorUrl(), capitalizedFieldName));
 }

由清單 5 我們已經知道為什麼 DescriptorImpl 類中的方法名必須是 doFill+引數名+Items 的形式了。

外掛實戰

學習了 DanymicParameter 這個構建引數外掛後,接下來我們可以嘗試開發一個 Jenkins 外掛。

需求描述

該專案在構建中需要若干個引數(Jenkins 的帶參構建可以實現),其中有些引數存在依賴關係,比如說有四個引數,A、B、C 和 D。當引數 A 選擇某個值,比如 Options1 時,引數 B 的選項為 [1, 2, 3],同時引數 C 顯示,引數 D 隱藏;當引數 A 選擇 Option2 的時候,引數 B 的選項為 [4, 5, 6],同時引數 C 隱藏,引數 D 顯示。

外掛開發

通過調查可知,Jenkins 無法實現如上的需求,也找不到一種替代的方法。於是,我們不得不開發自己的外掛,實現對 Jenkins 的定製化。另外,我們也會發現不能簡單的通過上面提到的引數外掛的方式來實現這個需求。這個時候我們需要深入瞭解下 Jenkins 的構建原理。

開啟 Jenkins,新建一個測試 Job Test1。可以看到頁面中“立即構建”的 URL,如圖 7 的下部紫色區域所示:

圖 7. Job-構建
圖 7. Job-構建

通過上圖所示的 URL,我們可以找到對應的抽象類-AbstractProject(在 package hudson.model 下面)的 doBuild 方法,如下:

清單 6. AbstractProject.java
 public void doBuild( StaplerRequest req, StaplerResponse rsp, @QueryParameter 
TimeDuration delay ) throws IOException, ServletException {
 ...
 ParametersDefinitionProperty pp = getProperty(ParametersDefinitionProperty.class);
 if (pp != null && !req.getMethod().equals("POST")) {
 req.getView(pp, "index.jelly").forward(req, rsp);
 return;
 }
 ...
 if (pp != null) {
 pp._doBuild(req,rsp,delay);
 return;
 }
 ...
 }

通過清單 6,會發現圖 7 所示的 URL 會交由 ParametersDefinitionProperty 的 index.jelly 顯示。

檢視該 jelly 檔案發現它會遍歷每個 ParameterDefinition(通過上面章節的介紹可知,每個 ParameterDefinition 實際上就是一個構建引數),然後一一顯示。

我們回到實際需求,需求裡面要求控制構建引數,所以我們可以繼承 ParametersDefinitionProperty,並通過在 index.jelly 中加入 JS 的方式來達到引數之間的依賴關係。

可以參考示例程式碼,效果見下圖。

圖 8. Customize Parameters-配置
圖 8. Customize Parameters-配置

從圖 8 可以看到,我們開發了一個 Jenkins 構建外掛,於是在 Job 的配置頁面多了一個紅框中標記的配置項。

圖 9. Customize Parameters-引數項
圖 9. Customize Parameters-引數項

現在我們配置 4 個構建引數,分別是 PARAMONE, PARAMTWO, PARAMONEOne 和 PARAMONETwo。其中 PARAMONE 有三個選項:One, Two 和 Three;PARAMTWO 有六組選項:One:Apple, One:Banana, Two:Red, Two:Green, Three:Dog, Three:Cat。

當 PARAMONE 為 One 時,PARAMTWO 的選項為 Apple 和 Banana,同時 PARAMONEOne 顯示,PARAMONETwo 隱藏;當 PARAMONE 為 Two 時,PARAMTWO 選項為 Red 和 Green,同時 PARAMONEOne 隱藏,PARAMONETwo 顯示;當 PARAMONE 為 Three 時,PARAMTWO 選項為 Dog 和 Cat,同時 PARAMONEOne 和 PARAMONETwo 都隱藏。

圖 10. Customize Parameters-效果展示 1
圖 10. Customize Parameters-效果展示 1
圖 11. Customize Parameters-效果展示 2
圖 11. Customize Parameters-效果展示 2
圖 12. Customize Parameters-效果展示 3
圖 12. Customize Parameters-效果展示 3

遇到的問題

實際開發過程中遇到過不少問題,以下列幾個比較典型的,希望能給讀者一些參考。

外掛安裝後,在 Jenkins 頁面找不到

這個問題經常會遇到,筆者採取的做法是刪除 plugin 目錄下安裝的外掛,一般包括一個以外掛名命名的 jpi 檔案和資料夾。全部刪除後,回到 Eclipse,執行 mvn clean,清空打包過程中殘留的檔案,再重新打包安裝。這個時候就能在 Jenkins 上找到你的外掛了。

無法在構建過程中獲取引數值

這個問題主要發生在定製化的引數外掛中。當 Jenkins 構建 Job 頁面配置完自己開發的引數外掛後,發現有時候無法從構建指令碼獲取引數值。出現這種問題一般有兩個原因:

  1. 構建指令碼中沒有引入系統的環境變數。Jenkins 的所有構建引數在構建時都會被放入系統的環境變數中,所以構建指令碼需要引入系統環境變數,才能讀取構建引數。比如引數名為 MyParameter,則我們需要在指令碼中通過<property environment="env" />引入環境變數以後,然後才能通過 {env.MyParameter} 使用構建引數,或者是其他環境變數。
  2. 外掛開發時沒有把引數放入環境變數。引數外掛中定義的引數只要被加入環境變數,構建過程中通過 1 中的方法才能讀取引數值。具體實現請參考清單 7。
清單 6. AbstractProject.java
 public void doBuild( StaplerRequest req, StaplerResponse rsp, @QueryParameter 
TimeDuration delay ) throws IOException, ServletException {
 ...
 ParametersDefinitionProperty pp = getProperty(ParametersDefinitionProperty.class);
 if (pp != null && !req.getMethod().equals("POST")) {
 req.getView(pp, "index.jelly").forward(req, rsp);
 return;
 }
 ...
 if (pp != null) {
 pp._doBuild(req,rsp,delay);
 return;
 }
 ...
 }

構建引數無法驗證

Jenkins 雖然支援帶參構建,但是卻無法驗證所填引數的正確性,因此經常出現因為使用者填寫引數不正確而導致構建失敗的情況。為了提高使用者體驗感,同時減少構建平均時間,筆者所在的團隊需要對 Jenkins 進行定製,實現引數驗證功能。

當時對 Jenkins 還不熟,解決這個問題花了不少時間。最後通過研究 Jenkins 原始碼發現,其實 Jenkins 的構建功能也是一個外掛,既然如此,那我們可以定製一個構建外掛,在這個外掛的 index.jelly 中引入 JS 程式碼實現引數驗證。

瀏覽器相容

Jenkins 本身對 Chrome 和 Firefox 的相容性比較好,而對 IE 的相容性則較差。筆者在定製 Jenkins 的過程中,也深受瀏覽器相容問題困擾。

由於最初開發使用的是原生的 JS,沒有花多少精力處理瀏覽器相容問題,導致所有使用者只能使用某個瀏覽器,給使用者造成一定程度的不便。後來經過研究發現,Jenkins 本身使用的是 jQuery 框架。如此,我們在實際開發中,可以使用該框架,就不用自己花大量時間和精力用原生 JS 來解決瀏覽器問題。

總結

本文從 Jenkins 概述開始,介紹了 Jenkins 的架構,通過架構的描述,相信讀者對 Jenkins 有了基本的認識和理解。接著筆者介紹了 Jenkins 的除錯方法,簡單介紹了 Jenkins 程式碼的入口,讀者可以從這裡開始,深入瞭解 Jenkins 裡面的模組和內容。然後筆者通過分析 Dynamic Parameter 外掛的流程,結合筆者的實際專案,開發了外掛 Customze Parameters。最後列舉了幾個在 Jenkins 開發過程中遇到的比較典型的問題和處理辦法供讀者參考。Jenkins 外掛開發遠不止這些內容,希望在進一步的學習和應用中能繼續與大家分享。

最後,特別感謝汪清安。他在 IBM 實習期間,曾幫助我們團隊花時間研究 Jenkins 外掛開發。文中一些觀點和結論的得出,離不開他的辛勤努力。

相關推薦

淺析 Jenkins 外掛開發

Jenkins 概述 Jenkins,簡單的說就是一個開源的持續整合伺服器,是 Hudson 的繼續。Jenkins 提供了易於使用的持續整合系統,使開發者更專注於業務邏輯的實現。Jenkins 能實時監控整合過程中的問題,提供詳細的日誌資訊,還能以圖表的形式顯示專案構建的情況。 Jenkins 提供

Jenkins外掛開發進階篇之擴充套件外掛功能

之前寫過一篇文章是關於如何開發jenkins外掛,主要講述了開發jenkins外掛時需要準備的環境,如何新建一個jenkins外掛工程,以及對工程專案目錄結構的解析。本文是jenkins外掛開發的進階篇,主要講述如何擴充套件jenkins外掛的功能。如下圖所示:(1)Job任

Jenkins外掛開發(2):開發外掛

1.修改settings.xml檔案,新增以下內容 <settings><pluginGroups><pluginGroup>org.jenkins-ci.to

Jenkins外掛開發筆記(1):匯入Jenkins原始碼

環境配置 1.下載並配置JDK 2.下載並配置Maven 匯入eclipse專案 1.通過git獲取Jekins原始碼 2.修改${USER_HOME}/.m2/settings.xml檔案,新增以下內容 <settings><plu

jenkins外掛開發

為什麼要開發jenkins外掛:    Jenkins是持續整合執行、管理平臺(與hudson一樣,具體說明可以檢視jenkins的wiki)。jenkins本身提供了一套外掛的管理機制,這些外掛允許可插撥形式存在。jenkins外掛雖然能提供很多種外掛,但還是不能滿足我們

jenkins外掛開發過程中log4j包衝突問題解決過程

最近在做jenkins外掛,關於負載(job分配到節點)均衡問題,使用log4j做日誌,但是,在pom.xml中加入log4j依賴包,配置好log4j.properties,在需要輸出日誌的地方加入程

Jenkins外掛開發入門

Jenkins外掛開發指南 環境變數 為了能開發外掛,開發環境需安裝Maven和JDK 6.0以上版本 配置maven的settings.xml配置檔案 <settings>   <pluginGroups>     <pluginGroup

Jenkins外掛開發入門資源

1. 一篇不錯的外掛開發入門教程,介紹很詳細。 2. 一篇經典的外掛開發入門教程,英文,28頁,介紹很詳細。大部分團隊成員都有訪問許可權。 《Using Hudson's Plugin Development Framework to Build Your First H

Jenkins 外掛開發之旅:兩天內從 idea 到釋出(上篇)

本文首發於:Jenkins 中文社群 本文介紹了筆者首個 Jenkins 外掛開發的旅程, 包括從產生 idea 開始,然後經過

Jenkins 外掛開發之旅:兩天內從 idea 到釋出(下篇)

本文首發於:Jenkins 中文社群 本文分上下兩篇,上篇介紹了從產生 idea 到外掛開發完成的過程; 下篇將介紹將外掛託管到

火線教你如何開發Jenkins外掛

提到Jenkins,做測試工作的無論是小夥伴、大夥伴還是老司機都是比較熟悉的。網上大部分資料無非三種:Jenkins簡介、如何啟動Jenkins、如何安裝和使用Jenkins外掛。本文作為一個jenkins的進階:教你如何開發一個jenkins外掛。話不多說,讓我們直接切入

CentOS7服務器的搭建記錄(jenkins開發方向)

centos7 服務 jenkins 搭建 方向 http weibo 記錄 服務器 http://weibo.com/p/1005056376507156http://weibo.com/p/1005056196029141http://weibo.com/p/10050

phonegap3.4外掛開發入門例子

根據官方文件(3.4.0)的外掛開發指南: http://docs.phonegap.com/en/3.4.0/guide_hybrid_plugins_index.md.html#Plugin%20Development%20Guide http://docs.phonegap.com/e

openmediavault 4.1.3 外掛開發

參考網址:https://forum.openmediavault.... 建立應用GUI 建立應用目錄:/var/www/openmediavault/js/omv/module/admin/service/example 建立選單節點: Node.js ``` // require("js/

QGIS3和QGIS2外掛開發差異記錄

qgis2外掛開發的入門級教程 http://www.qgistutorials.com/zh_TW/docs/building_a_python_plugin.html 仿造該教程在qgis3當中進行開發,會碰到許多坑。這裡簡單記錄一下。 1. QGIS3 中,QFileD

chrome外掛開發(二) 入門篇(content script

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

VSCODE外掛開發:使用者輸入輸出

閱讀這篇文章之前,假設你已經具有開發helloworld的外掛的能力。 vscode.window 簡介 vscode.window 負責當前啟用視窗的輸入輸出,比如展示資訊,和使用者輸入等功能都是用vscode.window實現 程式碼輸出提示資訊 簡單的輸出提示資訊 使用vscode.windo

Jenkins外掛之 Docker-Plugin 將slave執行在docker容器中

Jenkins外掛名稱 Docker plugin This plugin integrates Jenkins with Docker This plugin allows slaves to be dynamically provisioned using Docker. 外掛文

VSCode外掛開發全攻略(七)WebView

更多文章請戳VSCode外掛開發全攻略系列目錄導航。 什麼是Webview 大家都知道,整個VSCode編輯器就是一張大的網頁,其實,我們還可以在Visual Studio Code中建立完全自定義的、可以間接和nodejs通訊的特殊網頁(通過一個acquireVsCodeApi特殊方法),這個網頁就叫W

Chrome瀏覽器外掛開發入門

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!