Jenkins系列:如何进行Jenkins插件开发

Jenkins介绍

Jenkins 是一个开源项目,提供了一种易于使用的持续集成系统,使开发者从繁杂的集成中解脱出来,专注于更为重要的业务逻辑实现上。同时 Jenkins 能实施监控集成中存在的错误,提供详细的日志文件和提醒功能,还能用图表的形式形象地展示项目构建的趋势和稳定性。

在前面我们看见jenkins可以支持git, svn, maven等很多功能,这些都是Jenkins的插件,jenkins本身不提供很多功能,我们可以通过使用插件来满足我们的使用,接下来就介绍一下插件的原理以及我们怎么通过写一个自己的插件来满足我们的需求。

jenkins有很多的扩展点ExtensitonPoint),它是Jenkins系统的某个方面的接口或抽象类。这些接口定义了需要实现的方法;而Jenkins插件需要实现这些方法,也可以叫做在此扩展点之上进行扩展Jenkins。有关扩展点的详细信息,请参阅Jenkins 官方ExtentionPoints文档。通过这些扩展点我们可以写插件来实现自己的需求。
下面是一些常用的扩展点:

  • Scm:代表源码管理的一个步骤,如下面的Git,Subversion就是扩展的Scm;

      143046_q48O_2470917.png

  • Trigger:代表一个构建的触发,当满足一个什么样的条件时触发这个项目开始构建。比较常用的触发就是当代码变更时触发,如果我们需要实现一些比较复杂的触发逻辑,就需要扩展Trigger这个扩展点;

          143505_jkQG_2470917.png

  • Builder: 代表构建的一个步骤,如下图中在构建过程中,我们可以增加一个构建步骤,而每一个选项都是对应一个Builder,在每一个Builder中都有自己不同的功能。如Execute shell,这就是一个ShellBuilder,意味着在构建过程中会执行一个shell命令;

           143347_ZG6B_2470917.png

  • Publisher:Publisher代表一个项目构建完成后需要执行的步骤,如选项中的E-Mail Notifaction就是一个Publisher插件,选择这个选项后,当项目构建完成,就会使用email来通知用户,假如想要在项目构建完成后将构建目标产物发送到服务器上,则可以扩展此扩展点。

         143657_tYB4_2470917.png

上面简单描述了一下插件和扩展点,接着我们可以搭建一个插件的开发环境。

Jenkins插件开发

这边我们创建一个名叫“jenkins-plugin-hello-world”的maven项目,具体如果创建maven项目,我这边不再详细描述。目录结构如下:

153520_73cJ_2470917.png

pom.xml

<project xmlns="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">
  <modelVersion>4.0.0</modelVersion>
  <name>jenkins-plugin-hello-world</name>
  
 <parent>
        <groupId>org.jenkins-ci.plugins</groupId>
        <artifactId>plugin</artifactId>
        <!-- Baseline Jenkins version you use to build and test the plugin. 
Users must have this version or newer to run. -->
        <version>1.565.3</version>
        <relativePath/>
  </parent>
  
  <groupId>com.nd.component.java</groupId>
  <version>1.0.0-SNAPSHOT</version>
  <artifactId>jenkins-plugin-hello-world</artifactId>
  <packaging>hpi</packaging>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

</project>

HelloWorldBuilder.java

package com.lian.builder;

import hudson.Extension;
import hudson.Launcher;
import hudson.model.BuildListener;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Descriptor;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;

import java.io.IOException;
import java.io.Serializable;

import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;

public class HelloWorldBuilder extends Builder implements Serializable {

    /**	
     * Member Description
     */
    private static final long serialVersionUID = 1L;
    
    /* job模板配置的属性值,即config.jelly中的print属性值 */
    private boolean print;

    // Fields in config.jelly must names in "DataBoundConstructor"
    @DataBoundConstructor
    public HelloWorldBuilder(boolean print) {
        this.print = print;
    }
    
    // We'll use this from the <tt>config.jelly</tt>.
    public boolean isPrint() {
        return print;
    }

    @Override
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, final BuildListener listener) 
            throws IOException, InterruptedException {
        
        listener.getLogger().println("[INFO] isPrint: " + print);
        
        if (print) {
            listener.getLogger().println("[INFO] " + getDescriptor().getContent());
        }
        
        return true;
    }
    
    @Override
    public DescriptorImpl getDescriptor() {
        return (DescriptorImpl) super.getDescriptor();
    }

    @Extension
    public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
        
        /* 系统配置里面该插件的全局配置属性,即global.jelly中的content属性值 */
        private String content;
        
        public String getContent() {
            return content;
        }

        public DescriptorImpl() {
            load();// 加载全局配置
        }

        @Override
        public boolean configure(StaplerRequest req, net.sf.json.JSONObject json) 
throws Descriptor.FormException {
            this.content = json.getString("content");
            save();// 将全局配置信息持久化到xml
            return super.configure(req, json);
        }
        
        @Override
        @SuppressWarnings("rawtypes")
        public boolean isApplicable(Class<? extends AbstractProject> aClass) {
            return true;
        }

        @Override
        public String getDisplayName() {
            return "Hello World";// 插件显示名称
        }
    }
    
}

首先创建一个类继承于Builder,代表使用这个插件是一个构建插件(如果继承于Scm,代表这个插件是一个源码插件,例如Git,Svn插件),然后实现Serializable接口(在进行文件操作时,需要序列化)。

在Jenkins的插件中,每一个插件类中都必须要有一个Descriptor内部静态类,它代表一个类的“描述者”,用于指明这是一个扩展点的实现,Jenkins是通过这个描述者才能知道我们自己写的插件。每一个“描述者”静态类都需要被@Extension注解,Jenkins内部会扫描@Extenstion注解来知道注册了有哪些插件。

在Desciptor类中有两个方法需要我们必须要进行重写,即

@Override
@SuppressWarnings("rawtypes")
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
    return true;
}

这个方法的返回值代表这个Builder在Project中是否可用,我们可以将我们的逻辑写在其中,例如判断一些参数,最后返回true或者false来决定这个Builder在此处是否可用;

@Override
public String getDisplayName() {
    return "Hello World";// 插件显示名称
}

这个方法返回的是一个String类型的值,这个名称会用在web界面上显示的名称。

154144_BTOj_2470917.png

如果我们在插件中需要获取一些系统设置参数,我们可以在Descriptor中获取一个参数对应Descriptor中的一个属性,其中的content属性是一个全局配置;

/* 系统配置里面该插件的全局配置属性,即global.jelly中的content属性值 */
private String content;
        
public String getContent() {
    return content;
}

public DescriptorImpl() {
    load();// 加载全局配置
}

然后可以在系统设置里面看到这个属性。

154240_GzLv_2470917.png

在Descirptor构造函数中使用load()进行加载全局配置,然后我们就可以在插件中获取到配置信息

@Override
public boolean configure(StaplerRequest req, net.sf.json.JSONObject json) 
throws Descriptor.FormException {
     this.content = json.getString("content");
     save();// 将全局配置信息持久化到xml
     return super.configure(req, json);
}

当在全局配置修改属性后,需要在configure()方法中调用save()将全局配置信息持久化到xml,我们可以在workspace的插件名.xml中看到持久化的数据。

在每个插件的perform()方法中,是perform真正开始执行的地方,我们如果要在插件中完成什么事,代码逻辑也是写在perform方法中,perform方法参数中build代表当前构建,workspace代表当前工作目录,通过workspace可以获取到当前工作目录的信息,并可以做些文件操作;launcher代表启动进程,可以通过launcher执行一些命令;listener代表一个监听器,可以将运行的内容信息通过listener输出到前台console output。

@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, final BuildListener listener) 
            throws IOException, InterruptedException {
        
        listener.getLogger().println("[INFO] isPrint: " + print);
        
        if (print) {
            listener.getLogger().println("[INFO] " + getDescriptor().getContent());
        }
        
        return true;
}

如上面的代码所示,在perform方法中我们通过listener打印了一行”[INFO] isPrint: “ + print,print是一个变量,这个变量的值从哪里来下面我会介绍一下给大家。

155656_axtt_2470917.png

在jenkins插件中,如果我们需要一些自定义的参数信息,如构建时执行一些命令,命令的内容是由用户输入,这个时候需要一个变量来记录用户输入的信息;所以在HelloWorkdBuilder中定义一个属性与用于输入的信息相对应,如上面的print属性

public class HelloWorldBuilder extends Builder implements Serializable {

    /**	
     * Member Description
     */
    private static final long serialVersionUID = 1L;
    
    /* job模板配置的属性值,即config.jelly中的print属性值 */
    private boolean print;

    // Fields in config.jelly must names in "DataBoundConstructor"
    @DataBoundConstructor
    public HelloWorldBuilder(boolean print) {
        this.print = print;
    }
    
    // We'll use this from the <tt>config.jelly</tt>.
    public boolean isPrint() {
        return print;
    }


}

155046_FWao_2470917.png

这个属性的值是在job的配置过程中输入,由Jenkins从web前端界面传递过来的值,我们还需要在HelloWorldBuilder的构造方法中进行参数的注入。

类似于Spring的依赖注入,在这里Jenkins要求进行参数注入的构造方法需要用@DataBoundConstructor注解标注,以便Jenkins可以找到这个构造函数,并且调用这个构造函数,将web界面上配置的参数传递进HelloWorldBuilder,这样就可以在HelloWorldBuilder中使用这个属性了。

到此,这个插件的后台代码就已经搞定了,可以打包发布了。

执行命令打包:

mvn clean package -DskipTests

得到jenkins-plugin-hello-world.hpi,进行发布:

系统管理--》插件管理--》高级--》上传插件

161449_Z2O0_2470917.png

重启jenkins(可以参考https://my.oschina.net/lienson/blog/1514503),即可生效插件。

现在给大家讲讲怎么样编写这个前端配置的视图。

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">
  <!--
    This Jelly script is used to produce the global configuration option.

    Jenkins uses a set of tag libraries to provide uniformity in forms.
    To determine where this tag is defined, first check the namespace URI,
    and then look under $JENKINS/views/. For example, <f:section> is defined
    in $JENKINS/views/lib/form/section.jelly.

    It's also often useful to just check other similar scripts to see what
    tags they use. Views are always organized according to its owner class,
    so it should be straightforward to find them.
  -->
  <f:section title="Hello World插件全局配置">
  	  <f:entry title="内容" field="content" description="要讲的信息">
			<f:textbox />
	  </f:entry>
  </f:section>
</j:jelly>
  • config.jellyJob的配置视图
<?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="job配置">
	    <f:entry title="是否打印" field="print" description="默认不打印,false">
	      <f:checkbox />
	    </f:entry>
	</f:section>
</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官网

想要更详细了解Jelly用法,可以参考http://blog.csdn.net/kittyboy0001/article/details/18710161

总结

在本文,主要介绍了Jenkins的简单使用,以及Jenkins的插件开发环境,以及Jenkins插件结构的一些介绍。本文主要还是做一个简单入门介绍,如果想要了解更多的关于Jenkins的东西,还是需要去看Jenkins的官方wiki, 上面有详细的关于每个扩展点已经Jenkins的api的使用介绍,同样,你也可以下载Jenkins的源码来查看内部的一些实现方式。

Github Jenkinci也有很多的关于Jenkins插件的源码,我们可以通过源码了解一些扩展点是怎样使用,参照别人的源码来写出自己的插件。

参考

http://www.jianshu.com/p/8c05b6191d2f

 

转载于:https://my.oschina.net/lienson/blog/1531341

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值