0%

jenkins 插件开发教程

环境准备

本人使用的是JDK8、maven3.6.3

maven的setting.xml配置文件需要添加如下配置:

<mirrors>
        <mirror>
            <id>repo.jenkins-ci.org</id>
            <url>https://repo.jenkins-ci.org/public/</url>
            <mirrorOf>m.g.o-public</mirrorOf>
        </mirror>
</mirrors>
    
<profile>
    <id>jenkins</id>
    <activation>
        <activeByDefault>true</activeByDefault>
    </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>  

该配置大致意思就是引入jenkins的 镜像仓库地址,方便代码生成时候maven去对应仓库地址下载需要的jar包

插件开发

jenkins插件开发非常简单,框架骨架都是通过maven的模板生产代码,到时候根据需要到对应地方填充我们自己的逻辑即可。

maven生成项目

执行如下命令:

mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate -Dfilter=io.jenkins.archetypes: -DarchetypeCatalog=file:///你的archetype-catalog.xml本地路径(archetype-catalog.xml可以百度下载)

该命令通过制定固定的maven插件maven-archetype-plugin版本为2.4,并且手动指定DarchetypeCatalog路径。因为笔者睬过一个坑,不指定maven-archetype-plugin版本的话默认使用的是3以上版本,然后命令执行会报错,手动改为2.4版本并指定DarchetypeCatalog本地路径才得以运行成功。日志如下:

[INFO] Scanning for projects...
    [INFO]
    [INFO] ------------------< org.apache.maven:standalone-pom >-------------------
    [INFO] Building Maven Stub Project (No POM) 1
    [INFO] --------------------------------[ pom ]---------------------------------
    [INFO]
    [INFO] >>> maven-archetype-plugin:2.4:generate (default-cli) > generate-sources @ standalone-pom >>>
    [INFO]
    [INFO] <<< maven-archetype-plugin:2.4:generate (default-cli) < generate-sources @ standalone-pom <<<
    [INFO]
    [INFO]
    [INFO] --- maven-archetype-plugin:2.4:generate (default-cli) @ standalone-pom ---
    [INFO] Generating project in Interactive mode
    [INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
    Choose archetype:
    1: file:///Users/Documents/archetype-catalog.xml -> io.jenkins.archetypes:empty-plugin (Skeleton of a Jenkins plugin with a POM and an empty source tree.)
    2: file:///Users/Documents/archetype-catalog.xml -> io.jenkins.archetypes:global-configuration-plugin (Skeleton of a Jenkins plugin with a POM and an example piece of global configuration.)
    3: file:///Users/Documents/archetype-catalog.xml -> io.jenkins.archetypes:hello-world-plugin (Skeleton of a Jenkins plugin with a POM and an example build step.)
    Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : **3**
    Choose io.jenkins.archetypes:hello-world-plugin version:
    1: 1.1
    2: 1.2
    Choose a number: 2:** 2**
    Downloading from hello-world-plugin-repo: file:///Users/zhangzhenglong/Documents/io/jenkins/archetypes/hello-world-plugin/1.2/hello-world-plugin-1.2.jar
    Downloading from nexus: http://nexus.dui88.com:8081/nexus/content/groups/public/io/jenkins/archetypes/hello-world-plugin/1.2/hello-world-plugin-1.2.jar
    Downloaded from nexus: http://nexus.dui88.com:8081/nexus/content/groups/public/io/jenkins/archetypes/hello-world-plugin/1.2/hello-world-plugin-1.2.jar (13 kB at 1.3 kB/s)
    Downloading from hello-world-plugin-repo: file:///Users/Documents/io/jenkins/archetypes/hello-world-plugin/1.2/hello-world-plugin-1.2.pom
    Downloading from nexus: http://nexus.dui88.com:8081/nexus/content/groups/public/io/jenkins/archetypes/hello-world-plugin/1.2/hello-world-plugin-1.2.pom
    Downloaded from nexus: http://nexus.dui88.com:8081/nexus/content/groups/public/io/jenkins/archetypes/hello-world-plugin/1.2/hello-world-plugin-1.2.pom (737 B at 135 B/s)
    [INFO] Using property: groupId = unused
    Define value for property 'artifactId': : **buildInformer**
    Define value for property 'version':  1.0-SNAPSHOT: : **1.0.0**
    [INFO] Using property: package = org.jenkinsci.plugins.sample
    Confirm properties configuration:
    groupId: unused
    artifactId: buildInformer
    version: 1.0.0
    package: org.jenkinsci.plugins.sample
     Y: : **y**
    [INFO] ----------------------------------------------------------------------------
    [INFO] Using following parameters for creating project from Archetype: hello-world-plugin:1.2
    [INFO] ----------------------------------------------------------------------------
    [INFO] Parameter: groupId, Value: unused
    [INFO] Parameter: artifactId, Value: buildInformer
    [INFO] Parameter: version, Value: 1.0.0
    [INFO] Parameter: package, Value: org.jenkinsci.plugins.sample
    [INFO] Parameter: packageInPathFormat, Value: org/jenkinsci/plugins/sample
    [INFO] Parameter: package, Value: org.jenkinsci.plugins.sample
    [INFO] Parameter: groupId, Value: unused
    [INFO] Parameter: artifactId, Value: buildInformer
    [INFO] Parameter: version, Value: 1.0.0
    [INFO] project created from Archetype in dir: /Users/Documents/code/jenkins/build-informer/buildInformer
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time:  01:24 min
    [INFO] Finished at: 2020-10-10T13:50:04+08:00

    jenkins提供给了我们五种模板 :

  • 空插件(Jenkins插件的框架)
  • 全局配置插件(具有POM和全局配置示例的Jenkins插件的框架)
  • 全局共享库
  • hello-world-plugin(带有POM的Jenkins插件的骨架和一个示例构建步骤)
  • 脚本管道
    我们选择了第二种。然后就是输入ArtifactId 和创建Maven项目一样GroupId呢这里就不能设置了,再然后呢就是设置 version 指定插锁创建插件的版本,这里还是使用默认的,直接 Enter 就行,最后确认 信息输入 y 回车,最后就生成了一个基本的jenkins插件项目了!

插件项目

项目目录格式如下:

resources下的目录结构必须要和java下的目录结构一样。例如:src.main.java下有cn.com.xxx.jenkinsBuildInformer 包中有 BuildInformer.java 文件,src/main/resources 下有cn.com.xxx.jenkinsBuildInformer.BuildInformer 目录与之对应,这是Jenkins的规则,如果存在BuildInformer.java就必须存在BuildInformer目录与之对应,目录路径必须同BuildInformer.java包名一致。

然后我们再来看BuildInformer目录,他下面有三类文件,config.jelly、jelly、*.html ,config.jelly就是整个的表单界面。在里面配置你需要的表单内容即可。

我这边写的是一个http的后置通知,可以在打包完成后把打包结果实时通知给调用打包的服务器。避免调用方使用轮训的方法拉取结果。

BuildInformer代码如下:

 /**
 * @author 青葱
 */
public class BuildInformer extends Notifier {

    private final String appName;

    private final String env;

    private final String serverUrl;

    private PrintStream logger;

    /**
     * Fields in config.jelly must names in "DataBoundConstructor"
     */
    @DataBoundConstructor
    public BuildInformer(String appName, String env, String serverUrl) {
        this.appName = appName;
        this.env = env;
        this.serverUrl = serverUrl;
    }

    @Override
    public BuildStepMonitor getRequiredMonitorService() {
        return BuildStepMonitor.NONE;
    }

    @Override
    public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws UnsupportedEncodingException {

        logger = listener.getLogger();

        Jenkins.getInstance();
        sendAsync(generateMessageURL(build.getResult()));
        return true;
    }

    /**
     * 构建发送通知消息url
     *
     * @return
     */
    private String generateMessageURL(Result result) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(this.serverUrl)
                .append("appName=").append(this.appName)
                .append("&env=").append(this.env)
                .append("result=").append(result.toString());
        return stringBuffer.toString();
    }

    /**
     * 发送消息
     *
     * @param url
     */
    protected void send(String url) {
        logger.println("Sendurl: " + url);
        HttpURLConnection connection = null;
        InputStream is = null;
        String resultData = "";
        try {
            URL targetUrl = new URL(url);
            connection = (HttpURLConnection) targetUrl.openConnection();
            connection.setConnectTimeout(10 * 1000);
            connection.setReadTimeout(10 * 1000);
            connection.connect();
            is = connection.getInputStream();
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader bufferReader = new BufferedReader(isr);
            String inputLine = "";
            while ((inputLine = bufferReader.readLine()) != null) {
                resultData += inputLine + "\n";
            }
            logger.println("response: " + resultData);
        } catch (Exception e) {
            logger.println("http error." + e);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                connection.disconnect();
            }
        }
        logger.println("Send url finish");
    }

    protected void sendAsync(String url) {
        RequestConfig requestConfig = RequestConfig.custom()
                .setSocketTimeout(100000)
                .setConnectTimeout(100000).build();
        CloseableHttpAsyncClient httpclient = HttpAsyncClients.custom().
                setDefaultRequestConfig(requestConfig)
                .build();
        try {
            httpclient.start();
            final HttpGet request = new HttpGet(url);
            httpclient.execute(request, new FutureCallback<HttpResponse>() {

                @Override
                public void completed(final HttpResponse response) {
                    logger.println(request.getRequestLine() + "->" + response.getStatusLine());
                }

                @Override
                public void failed(final Exception ex) {
                    logger.println(request.getRequestLine() + "->" + ex);
                }

                @Override
                public void cancelled() {
                    logger.println(request.getRequestLine() + " cancelled");
                }
            });
        } catch (Exception e) {
            logger.println("http error." + e);
        } finally {
            try {
                httpclient.close();
            } catch (Exception e) {
            }
        }
        logger.println("send Done");
    }

    @Override
    public DescriptorImpl getDescriptor() {
        return (DescriptorImpl) super.getDescriptor();
    }

    public String getAppName() {
        return appName;
    }

    public String getEnv() {
        return env;
    }

    public String getServerUrl() {
        return serverUrl;
    }

    @Extension
    public static final class DescriptorImpl extends
            BuildStepDescriptor<Publisher> {

        public DescriptorImpl() {
            load();
        }

        private boolean isNumeric(String str) {
            Pattern pattern = Pattern.compile("[0-9]*");
            Matcher isNum = pattern.matcher(str);
            if (!isNum.matches()) {
                return false;
            }
            return true;
        }


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

        /**
         * jenkins中显示名称
         *
         * @return
         */
        @Override
        public String getDisplayName() {
            return "构建通知";
        }

        @Override
        public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
            save();
            return super.configure(req, formData);
        }

    }
}  

调试

开发完成执行mvn hpi:run 会打开一个本地的带当前开发插件的jenkins服务器,地址为http://localhost:8080/jenkins,可以在该地址上看插件效果。
如果需要断点调试,可以运行mvnDebug hpi:run,此命令会在8000端口建立监听,然后可以在IDE中配置对应的Run/Debug Configuration,配置完后运行Debug就会自动触发插件编译和运行流程。

上传插件

jenkins如果需要运行该插件,可以登陆jenkins后台,插件管理->高级->上传插件 上传我们打包完成的.hpi包