1. 概述

Jenkins 是开源的持续集成服务器,支持为特定任务/环境创建自定义插件。本文将完整演示如何开发一个扩展插件,用于在构建输出中添加项目统计信息(类数量和代码行数)。

2. 环境搭建

Jenkins 官方提供了便捷的 Maven 原型,我们直接使用它初始化项目:

mvn archetype:generate -Dfilter=io.jenkins.archetypes:plugin

执行后会显示可选原型列表:

[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: 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:hello-world-plugin
  (Skeleton of a Jenkins plugin with a POM and an example build step.)

选择第一个选项(empty-plugin),然后在交互模式中定义 groupId/artifactId/package。完成后需要修改 pom.xml 中的占位符(如 <name>TODO Plugin</name>)。

3. Jenkins 插件设计

3.1 扩展点机制

Jenkins 提供了丰富的扩展点(Extension Points)。这些接口或抽象类定义了特定场景的契约,允许插件实现自定义功能。

典型扩展点示例:

  • BuildStep:定义构建步骤(如 "从 VCS 检出"、"编译"、"测试")
  • BuildWrapper:定义构建前/后操作
  • 第三方插件如 Email Extension 的 RecipientProvider:提供邮件收件人

⚠️ 注意:早期插件需继承 hudson.Plugin现在推荐使用扩展点机制

3.2 插件初始化

需要通过注解告知 Jenkins 如何实例化插件:

  1. 定义静态内部类并添加 @Extension 注解:

    class MyPlugin extends BuildWrapper {
     @Extension
     public static class DescriptorImpl 
       extends BuildWrapperDescriptor {
    
         @Override
         public boolean isApplicable(AbstractProject<?, ?> item) {
             return true;
         }
    
         @Override
         public String getDisplayName() {
             return "UI 中显示的名称";
         }
     }
    }
    
  2. 使用 @DataBoundConstructor 标记构造函数:

    @DataBoundConstructor
    public Maven(
    String targets,
    String name,
    String pom,
    String properties,
    String jvmOptions,
    boolean usePrivateRepository,
    SettingsProvider settings,
    GlobalSettingsProvider globalSettings,
    boolean injectBuildVariables) { ... }
    

构造函数参数会自动映射到 UI 配置界面(如 Maven 插件配置):

maven settings

✅ 也可用 @DataBoundSetter 注解 setter 方法实现参数绑定。

4. 插件实现

我们通过 BuildWrapper 在构建过程中收集项目统计信息:

class ProjectStatsBuildWrapper extends BuildWrapper {

    @DataBoundConstructor
    public ProjectStatsBuildWrapper() {}

    @Override
    public Environment setUp(
      AbstractBuild build,
      Launcher launcher,
      BuildListener listener) {}

    @Extension
    public static class DescriptorImpl extends BuildWrapperDescriptor {

        @Override
        public boolean isApplicable(AbstractProject<?, ?> item) {
            return true;
        }

        @Nonnull
        @Override
        public String getDisplayName() {
            return "构建时生成项目统计";
        }

    }
}

4.1 核心功能实现

定义统计实体类:

class ProjectStats {
    private int classesNumber;
    private int linesNumber;
    // 标准构造/getter
}

实现统计逻辑(递归扫描 Java 文件):

private ProjectStats buildStats(FilePath root)
  throws IOException, InterruptedException {
 
    int classesNumber = 0;
    int linesNumber = 0;
    Stack<FilePath> toProcess = new Stack<>();
    toProcess.push(root);
    while (!toProcess.isEmpty()) {
        FilePath path = toProcess.pop();
        if (path.isDirectory()) {
            toProcess.addAll(path.list());
        } else if (path.getName().endsWith(".java")) {
            classesNumber++;
            linesNumber += countLines(path);
        }
    }
    return new ProjectStats(classesNumber, linesNumber);
}

4.2 生成报告

创建 HTML 模板:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>$PROJECT_NAME$</title>
</head>
<body>
Project $PROJECT_NAME$:
<table border="1">
    <tr>
        <th>Classes number</th>
        <th>Lines number</th>
    </tr>
    <tr>
        <td>$CLASSES_NUMBER$</td>
        <td>$LINES_NUMBER$</td>
    </tr>
</table>
</body>
</html>

在构建完成后生成报告文件:

public class ProjectStatsBuildWrapper extends BuildWrapper {
    @Override
    public Environment setUp(
      AbstractBuild build,
      Launcher launcher,
      BuildListener listener) {
        return new Environment() {
 
            @Override
            public boolean tearDown(
              AbstractBuild build, BuildListener listener)
              throws IOException, InterruptedException {
 
                ProjectStats stats = buildStats(build.getWorkspace());
                String report = generateReport(
                  build.getProject().getDisplayName(),
                  stats);
                File artifactsDir = build.getArtifactsDir();
                String path = artifactsDir.getCanonicalPath() + REPORT_TEMPLATE_PATH;
                File reportFile = new File(path);
                // 将报告内容写入文件
            }
        };
    }
}

5. 使用指南

5.1 安装插件

构建插件并复制到 Jenkins:

mvn install
cp ./target/jenkins-hello-world.hpi ~/.jenkins/plugins/

重启 Jenkins 后验证安装:

  1. 访问 http://localhost:8080
  2. 进入 Manage JenkinsManage PluginsInstalled
  3. 确认插件列表中存在我们的插件

plugin enabled

5.2 配置任务

创建新任务并配置 Git 仓库(以 commons-lang 为例):

common lang git

启用我们的插件:

enable for project

5.3 查看结果

执行构建后,在构建产物中找到 stats.html

commons lang build 1

打开报告文件:

commons lang result

结果符合预期:1 个类,3 行代码。

6. 总结

本文完整演示了 Jenkins 插件开发流程,从环境搭建到最终验证。虽然未覆盖所有高级特性,但提供了核心设计思路和基础实现。完整代码请参考 GitHub 仓库


原始标题:Writing a Jenkins Plugin