插件開發環境搭建
- maven 3
- jdk 6 +
修改 maven 配置文件 settings.xml
<settings>
<pluginGroups>
<pluginGroup>org.jenkins-ci.tools</pluginGroup>
</pluginGroups>
<profiles>
<!-- Give access to Jenkins plugins -->
<profile>
<id>jenkins</id>
<activation>
<activeByDefault>true</activeByDefault> <!-- change this to false, if you don't like to have it on per default -->
</activation>
<repositories>
<repository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
<mirrors>
<mirror>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
<mirrorOf>m.g.o-public</mirrorOf>
</mirror>
</mirrors>
</settings>
創建一個空的 plugins 工程
- 默認為空工程
mvn archetype:generate -Dfilter=io.jenkins.archetypes:empty-plugin
-
hello_world
工程
mvn -U archetype:generate -Dfilter=io.jenkins.archetypes:
Choose archetype:
1: remote -> io.jenkins.archetypes:empty-plugin (Skeleton of a Jenkins plugin with a POM and an empty source tree.)
2: remote -> io.jenkins.archetypes:global-configuration-plugin (Skeleton of a Jenkins plugin with a POM and an example piece of global configuration.)
3: remote -> io.jenkins.archetypes:global-shared-library (Uses the Jenkins Pipeline Unit mock library to test the usage of a Global Shared Library)
4: remote -> io.jenkins.archetypes:hello-world-plugin (Skeleton of a Jenkins plugin with a POM and an example build step.)
5: remote -> io.jenkins.archetypes:scripted-pipeline (Uses the Jenkins Pipeline Unit mock library to test the logic inside a Pipeline script.)
# 4 HelloWorld
插件目錄結構
-
pom.xml
:maven 使用這個文件來構建插件,所有的插件都是基于 Plugin Parent Pom
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>3.43</version>
<relativePath />
</parent>
intellij idea:直接在 ide 中導入 pom 文件就能導入。
調試插件
- linux
export MAVEN_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000,suspend=n"
mvn hpi:run
- windows
set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000,suspend=n
mvn hpi:run
輸入命令過后可以打開瀏覽器,輸入:http://localhost:8080/jenkins, 就可以看見你的插件在 jenkins 中運行起來了,現在就可以開始進行調試了。
修改端口
mvn hpi:run -Djetty.port =8090
設置上下文路徑
mvn hpi:run -Dhpi.prefix=/jenkins
打包發布
mvn package
該命令會在 target 目錄創建出[插件名稱].hpi
文件,其他用戶可以直接將這個插件上傳安裝到 Jenkins 中使用(或者放到 $JENKINS_HOME/plugins
目錄中)。
Jenkins 插件 Demo HelloWorld
在之前我們使用
mvn -U archetype:generate -Dfilter=io.jenkins.archetypes:
創建插件目錄時,Jenkins在我們的項目中生成了一個 HelloWorldBuilder 的插件,這是一個官方示例,下面分析一下這個插件的示例源碼
public class HelloWorldBuilder extends Builder implements SimpleBuildStep {
}
首先創建一個類繼承于 Builder
,代表使用這個插件是一個構建插件(如果繼承于 Scm
,代表這個插件是一個源碼插件,例如 Git
,Svn
插件),然后實現 SimpleBuildStep
接口。
在 Jenkins 的插件中,每一個插件類中都必須要有一個 ==Descriptor
內部靜態類==,它代表一個類的 ’描述者‘,用于指明這是一個擴展點的實現,Jenkins 是通過這個描述者才能知道我們自己寫的插件。
每一個 ‘描述者’ 靜態類都需要被 @Extension
注解,Jenkins 內部會掃描 @Extenstion
注解來知道注冊了有哪些插件。
@Symbol("greet")
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
public FormValidation doCheckName(@QueryParameter String value, @QueryParameter boolean useFrench)
throws IOException, ServletException {
if (value.length() == 0)
return FormValidation.error("error");
if (value.length() < 4)
return FormValidation.warning("warning");
if (!useFrench && value.matches(".*[éáà?].*")) {
return FormValidation.warning("warning");
}
return FormValidation.ok();
}
@Override
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}
@Override
public String getDisplayName() {
return "Say hello world";
}
}
在 Desciptor
類中有兩個方法需要我們必須要進行重寫
isApplicable
@Override
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}
這個方法的返回值代表這個 Builder
在 Project 中是否可用,我們可以將我們的邏輯寫在其中,例如判斷一些參數,最后返回 true
或者 false
來決定這個 Builder
在此處是否可用。
getDisplayName
@Override
public String getDisplayName() {
return "Say hello world";
}
這個方法返回的是一個 String 類型的值,這個名稱會用在 web 界面上顯示的名稱。
如果我們在插件中需要獲取一些系統設置參數,我們可以在 Descriptor
中獲取一個參數對應 Descriptor
中的一個屬性,其中的 userFrench
屬性是一個全局配置,可以在系統設置里面看到這個屬性。
private boolean useFrench;
public DescriptorImpl() {
//...
load();
}
configure() 方法
在 Descirptor 構造函數中使用load()
進行加載全局配置,然后我們就可以在插件中獲取到配置信息。
@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
useFrench = formData.getBoolean("useFrench");
save();
return super.configure(req,formData);
}
當在全局配置修改屬性后,需要在configure()
方法中調用save()
將全局配置信息持久化到xml,我們可以在workspace的插件名.xml中看到持久化的數據。
peform() 方法
在每個插件的 perform()
方法中,是 perform
真正開始執行的地方,我們如果要在插件中完成什么事,代碼邏輯也是寫在 perform
方法中,perform
方法參數中 build
代表當前構建。
workspace
代表當前工作目錄,通過workspace
可以獲取到當前工作目錄的信息,并可以做些操作,如workspace.copyTo("/home")
。launcher
代表啟動進程,可以通過launcher
執行一些命令,如launcher.launch().stdout(listener).cmds("pwd").start();
。listener
代表一個監聽器,可以將運行的內容信息通過listener
輸出到前臺console output
。
@Override
public void perform(Run<?, ?> run, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException {
if (useFrench) {
listener.getLogger().println("Bonjour, " + name + "!");
} else {
listener.getLogger().println("Hello, " + name + "!");
}
}
如上面的代碼所示,在 perform
方法中我們通過 listener
打印了一行信息。在 web 界面上的控制臺可以看見如下內容。其中 Jeffrey
為系統設置中輸入的內容。
用戶自定義參數
在 Jenkins 插件中,如果我們需要一些自定義的參數信息,如構建時執行一些命令,命令的內容是由用戶輸入,這個時候需要一個變量來記錄用戶輸入的信息。
所以在 HelloWorkdBuilder 中定義一個屬性與用于輸入的信息相對應,如上面的 name
屬性。
public class HelloWorldBuilder extends Builder implements SimpleBuildStep {
private final String name;
....
}
這個屬性的值是在 job 的配置過程中輸入,由 Jenkins 從 web 前端界面傳遞過來的值,我們還需要在HelloWorldBuilder的構造方法中進行參數的注入
public class HelloWorldBuilder extends Builder implements SimpleBuildStep {
private final String name;
@DataBoundConstructor
public HelloWorldBuilder(String name) {
this.name = name;
}
類似于 Spring 的依賴注入,在這里 Jenkins 要求進行參數注入的構造方法需要用 @DataBoundConstructor
注解標注,以便 Jenkins 可以找到這個構造函數,并且調用這個構造函數,將 web 界面上配置的參數傳遞進HelloWorldBuilder,這樣就可以在 HelloWorldBuilder 中使用這個屬性了。
到此,這個插件的后臺代碼就已經搞定了,現在給大家講講怎么樣編寫這個前端配置的視圖。
Jenkins 中的視圖
Jenkins 使用 Jelly 來編寫視圖,Jelly 是一種基于 Java
技術和 XML
的腳本編制和處理引擎。Jelly 的特點是有許多基于 JSTL (JSP 標準標記庫,JSP Standard Tag Library)、Ant、Velocity 及其它眾多工具的可執行標記。Jelly 還支持 Jexl(Java 表達式語言,Java Expression Language),Jexl 是 JSTL 表達式語言的擴展版本。Jenkins 的界面繪制就是通過 Jelly 實現的。
在 Jenkins 中的視圖的類型有三種:
- global.jelly:全局的配置視圖
<?jelly escape-by-default='true'?>
<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:section title="Hello World Builder">
<f:entry title="French" field="useFrench"
description="Check if we should say hello in French">
<f:checkbox />
</f:entry>
</f:section>
</j:jelly>
- config.jelly:Job的配置視圖
<?jelly escape-by-default='true'?>
<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="Name" field="name">
<f:textbox />
</f:entry>
</j:jelly>
在定義一個屬性時,使用<f:entry>
標簽代表這是一個屬性,其中title
是指在界面上顯示的字段名,而field
是指這個屬性在 HelloWorldBuilder 中對應的屬性名,Jenkins 通過這個名稱來與 HelloWorldBuilder 中的屬性相對應,從而使用 @DataBoundConstructor
標注的構造函數將這些變量注入到 HelloWorldBuilder 類中。
- help-屬性名.html:幫助視圖 html 片段
<div>
Help file for fields are discovered through a file name convention. This file is
help for the "name" field. You can have <i>arbitrary</i> HTML here. You can write
this file as a Jelly script if you need a dynamic content (but if you do so, change
the extension to <tt>.jelly</tt>).
</div>
這是 Jenkins 中的三種視圖,上面也介紹了兩個簡單的控件 textbox 和 checkbox 的使用,更多的關于 Jelly 的視圖使用可以查看jelly官網。
Jenkins 數據持久化
之前在web界面上輸入了 name
,這個信息在下一次構建的時候仍然存在,說明 Jenkins 中需要使用數據持久化來將我們配置的信息保存下來,而 Jenkins 使用文件來存儲數據(所有數據都存儲在 $JENKINS_HOME
),有些數據,比如 console 輸出,會作為文本文件存儲;大多數的結構數據,如一個項目的配置或構建(build)記錄信息則會通過 XStream 持久化為一個 xml
文件,如下圖所示
而在需要信息的時候,Jenkins 又從 xml
文件中讀取到相應的數據,返回給應用程序。
總結總結
在本文,主要介紹了 Jenkins 的簡單使用,以及 Jenkins 的插件開發環境,以及 Jenkins 插件結構的一些介紹。本文主要還是做一個簡單入門介紹,如果想要了解更多的關于 Jenkins 的東西,還是需要去看 Jenkins 的官方wiki, 上面有詳細的關于每個擴展點已經 Jenkins 的 api 的使用介紹,同樣,你也可以下載 Jenkins 的源碼來查看內部的一些實現方式。在Github Jenkinci也有很多的關于 Jenkins 插件的源碼,我們可以通過源碼了解一些擴展點是怎樣使用,參照別人的源碼來寫出自己的插件。
參考