人気の自動化プラットフォームをカスタマイズして「Hallo」と表示させる

著者:Arjan Tijms

2020116

※本記事はBuild your own Jenkins plugins with Guice, SezPoz, Stapler, and Jellyの翻訳記事です。

本記事をPDFでダウンロード


Java開発者が毎日使っている多くのツールは、プラグインで拡張できます。プラグインとは、プログラムによる細かい機能の集まりがツールと密接に統合されて、追加機能を提供するものです。皆さんは、自分でプラグインを書いて生産性を高めることができるという事実について深く考えずに、多くの既製プラグインを使っているのかもしれません。

Java開発者が使っている数々のツールはJavaで書かれているので、お気に入りの言語や親しんでいるツールを使ってプラグインを書くことができます。そのうえ多くの場合、プラグインを書くという経験は、プラグインを書くシステムについての理解を深めることにつながります。

独自のMavenプラグインの書き方」では、Mavenビルド・ツール用のプラグインの作り方について説明しました。本記事では、特に人気の高い自動化サーバーであるJenkinsのプラグインに迫ります。

Jenkinsの拡張は、Mavenの拡張よりも複雑です。Mavenのプラグインのほぼすべては同じ1つの種類ですが、Jenkinsのプラグインには複数の型があります。この点については、後ほど詳しく説明します。

Jenkinsのプラグインは、最低でも2つのクラスで構成されます。

  • いわゆる記述子:実際のプラグインのインスタンスを生成するためのファクトリとして、プラグインと機能のメタデータを保持する
  • プラグインの実装クラスの実体

記述子

記述子は、Jenkinsが検出してインデックスに登録するクラスです。記述子には、@Extensionまたは@OptionalExtensionというアノテーションを付加する必要があります。記述子には、さまざまな種類のサブクラスも存在します。たとえば、Builderプラグインには、BuildStepDescriptorを使用する必要があります。名前からわかるように、この記述子はビルドに機能を提供するプラグインのためのものです。

記述子には、他にもさまざまなものがあります。たとえば、GlobalConfigurationはシステム構成ページに機能を提供するために使用します。

MavenのMojoと同じように、DescriptorはJSR 330(「Javaの依存性注入」)に基づいたマネージドBeanで、JSR 250(「Javaプラットフォーム向け共通アノテーション」)の@PostConstructを追加でサポートしています。

Jenkinsでは、依存性注入(DI)コンテナのGuice(JSR 330のリファレンス実装)と、SezPozというJSR 269(「プラガブル・アノテーション処理API」)準拠のアノテーション・インデクサを組み合わせて使用します。

SezPozでは、JSR 269のAbstractProcessorを使ってコンパイル時にアノテーションをスキャンし、検出されたアノテーションの情報をJARのMETA-INF/annotationsフォルダ内のインデックス・ファイルに書き込みます。その際、1つのアノテーションにつき1つのファイルが生成されます。たとえば、hudson.Extensionアノテーションの情報は、すべてMETA-INF/annotations/hudson.Extensionというファイルに格納されます。この仕組みは、すべてのアノテーションに対して1つのインデックスを作成するJandex(DI コンテナのWeldで使用されます)などのアノテーション・インデクサとは対照的です。

Jenkinsでは、実行時に簡単なGuiceモジュール(com.google.inject.Module)実装を作成します。そして、クラスパス上にあるすべてのインデックス・ファイルから@Extensionアノテーションと@OptionalExtensionアノテーションが付加されたすべてのインスタンスを取得し、FaultTolerantScopeという内部スコープを使ってそれらのインスタンスをバインドします。ここでのバインドという言葉は、Guiceでキーを使ってインスタンスを検索できるようにすることを指します。内部スコープが使われるのは、動作しないプラグインによってJenkinsの起動が妨げられないようにするためです。

Jenkinsで作成するのは、このメイン・モジュールだけではありません。@Extensionアノテーションが付加され、SezPozのインデックスに登録されたcom.google.inject.Moduleの実装のそれぞれについて、追加のGuiceモジュールを作成します。JenkinsではそれらのModuleインスタンスを直接使用します。これは高度な機能の場合に行われる処理であり、通常のプラグインで必要となるものではありません。Jenkinsでは、実行時にプラグインをアップデートする処理もサポートしています。Guice自体では既存のバインドを直接変更することを許可していませんが、子コンテナと呼ばれるものを使えば、模擬的にそれを実現できます。Jenkinsで使用しているのは、まさにその仕組みです。

Jenkinsでは、JSR 250をサポートするために、Springの規則に従って、クラス階層に登場する順番(スーパークラスが先、サブクラスが後)で@PostConstructアノテーションが付加されたメソッドを呼び出します。

草創期の歴史:なぜHK2ではなくGuiceなのか

Jenkinsにおいて、有名な依存性注入カーネルであるHK2ではなくGuiceを使って依存性注入を行っているのはなぜかと不思議に思う方もいるかもしれません。Guiceと同様に、HK2でもJSR 330を実装しています。HK2でもっとも特徴的なのは、有名な開発者である川口耕介氏によって主に作成されたことです。川口氏は、現在Jenkinsになっているものの開発を始めた人物でもあります。Jenkinsには、至るところに川口氏の名前が含まれています(文字どおり、かなりの数のJavaパッケージ名にkohsukeが使われています)。それではなぜ、「自分の」HK2が使われていないのでしょうか。

その答えは必ずしも明確ではありません。事実としては、Sun Microsystemsで当時Hudsonと呼ばれていたJenkinsの開発が始まったのは2004年の夏で、当初はノンマネージドなプラグイン・システムが搭載されていました。同じSunHK2の開発が始まったのは2007年ですが、すぐに実環境で使用できる状態ではなかったのかもしれません。

川口氏は、2009年初頭にSezPozによるプラグイン自動検出メカニズムの追加を始めましたが、その際、PlexusMavenで使用されていたレガシーDIコンテナ)を組み込むことについて述べていました。HK2がようやく実環境で使用できるようになったとき、川口氏はすでにSunを退社していました。

その後の2010年、川口氏は拡張機能にGuiceを使い始めました。そして、川口氏がJenkinsのロードマップを話題にした際も、HK2は発言の追伸として登場しただけでした。後にGuiceHK2について尋ねられたとき(ただし、Jenkins関係の話題ではありませんでした)も、川口氏は「今のところ、HK2を使う理由はない」と回答していました

興味深いことに、Sonatypeのチームは、このときすでに商用バージョンのHudsonJSR 330を組み込む大量の作業を終えていました。その際、同様にGuiceを使用してMavenで同じ作業を行った経験を生かしていました。やがてこの作業は、Smoothieフレームワークの最初の(そして唯一の)コミットによって、2011年初頭にオープンソース化されました。

プラグインの実装クラス

プラグインの実装クラスの大部分は、BuilderNotifierなど、前述の主なプラグインの型から継承します。実装クラスそのものは、Guiceで処理されるわけではありません。デフォルトでは、Descriptorによってそのインスタンスが作成されてから、Staplerと呼ばれるフレームワークによってその注入が行われます。

Staplerは、Jenkins自体よりも前に、川口氏が手がけたもっとも古いフレームワークの1つです。Staplerには、URLを対象にした式言語のような機能が搭載されています。したがって、#{foo.bar.kaz}というコードを書けば、”foo”という名前のベース・オブジェクトに対してgetBar()が呼び出され、その結果に対してgetKaz()が呼び出されます。Staplerでは、同様の呼出し階層を実現するために、URL /foo/bar/kazを使います。

ここで説明する、プラグインの実装において特に重要なのは、プラグイン構成から生成されるJSONペイロードの値をStaplerがプラグイン・クラスに注入することです。これを行うために、Staplerではコンストラクタなどのパラメータ名を使用します。Javaではパラメータ名にアクセスする仕組みが提供されていますが、残念ながらデフォルトでは使用できません。そのため、StaplerではSezPozと似たような仕組みを使っています。具体的に言えば、コンパイル時のアノテーション処理を使い、パラメータ名を別個の*.staplerファイルに取り込んでいます。このファイルは、クラス・ファイルと一緒にJARに格納されます。たとえば、org/omnifaces/example/HelloWorld.classに対しては、org/omnifaces/example/HelloWorld.staplerファイルが生成されます。

Staplerにある程度自動的に行わせることもできたと思われますが、この処理に使うコンストラクタには、@org.kohsuke.stapler.DataBoundConstructorアノテーションを付加する必要があります。デフォルトのコンストラクタや引数のないコンストラクタしか存在しない場合も同様です。JSONペイロードの値は、まずコンストラクタに渡されます。残った値がある場合は、フィールド注入またはsetter注入が使われます。これも自動では行われません。対象とするフィールドやsetterメソッドには、@org.kohsuke.stapler.DataBoundSetterアノテーションを付加する必要があります。

さらに、Descriptorと同じく、プラグインの実装クラスでもJSR 250の@Postconstructアノテーションをサポートしています。このアノテーションを付加したメソッドは、コンストラクタ注入と、フィールド注入またはsetter注入が行われた後で呼び出されます。

Hello world

以上の内容を実際に行うと、どのような形になるでしょうか。次に示す簡単なHelloWorldプラグインをご覧ください。

package org.omnifaces.example;

import hudson.Extension;
import hudson.model.AbstractProject;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;

@Extension
public class HelloWorldDescriptor extends BuildStepDescriptor<Builder> {

    public HelloWorldDescriptor() {
        super(HelloWorld.class);
    }

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

これは最小限の記述子の1つです。データをスキャンしてGuiceに渡すために必要な@Extensionアノテーションに注目してください。また、BuildStepDescriptorクラスを拡張する必要があります。現実的に言えば、パラメータとして使うのは常にBuilder型とする必要があります(実際の要件はBuildStep型とDescribable型ですが、Builderはどちらもきちんと満たしています)。

ここで満たすべき要件はあと2つあります。実装するプラグインのクラスをコンストラクタで設定することと、isApplicable()メソッドを実装することです。ここではtrueを返しています。これは、このBuilderプラグインを任意のタイプのプロジェクト(FreestyleやMavenなど)で使用できるようにするという意味になります。

次に、実際のプラグイン実装を確認します。

package org.omnifaces.example;

import org.kohsuke.stapler.DataBoundConstructor;

import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.tasks.Builder;

public class HelloWorld extends Builder {
    
    @DataBoundConstructor
    public HelloWorld() {
    }

    @Override
    public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) {
        listener.getLogger().println("Hello, world!”);
        return true;
    }
}

Builderクラスを継承していることに注目してください。この型は、Descriptorのジェネリック・パラメータとして使用したものと同じです。ここに明示的なコンストラクタは必要ないものの、Staplerの要件を満たすコンストラクタを定義しなければなりません。つまり、@DataBoundConstructorアノテーションを付加したコンストラクタが必要になります。

実際の処理はperform()メソッドで行うことができます。このプラグインは、次のPOMファイルで構成するMavenプロジェクトを使ってビルドすることができます。

<?xml version="1.0" encoding="UTF-8"?>
<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>
  
            <parent>
      <groupId>org.jenkins-ci.plugins</groupId>
      <artifactId>plugin</artifactId>
      <version>4.9</version>
      <relativePath />
  </parent>

  <groupId>org.omnifaces.example</groupId>
  <artifactId>hello-jenkins-plugin</artifactId>
  <version>1.0</version>
  <packaging>hpi</packaging>

  <properties>
      <java.level>8</java.level>
  </properties>

  <repositories>
      <repository>
          <id>repo.jenkins-ci.org</id> 
          <url>https://repo.jenkins-ci.org/releases/</url>
      </repository>
  </repositories>
  
  <pluginRepositories>
      <pluginRepository>
          <id>repo.jenkins-ci.org</id>
          <url>https://repo.jenkins-ci.org/releases/</url>
      </pluginRepository>
  </pluginRepositories>
</project>

おわかりのように、このPOMファイルではHPIという特別な種類のパッケージを使用しています。HPIは、Hudsonプラグイン・インタフェース(Hudson Plug-in Interfaceを略したもので、おなじみの.warにとてもよく似た形式です。ここで使ったであるorg.jenkins-ci.plugins:pluginが、この形式の作成処理を進めます。この親にはかなり癖があります。非常に特殊なプロセスを使って大量のMavenプラグインを実行するからです。その中には、テストの生成や生成したテストの実行まで含まれています。さらに、唯一の必須プロパティのjava.levelを使って、Mavenの強制ルールやJDK APIのチェックも行っています。ここで注意すべき点は、この親がすべてに「1.」というプリフィックスを付けることです。そのため、たとえば「8」は「1.8」になります。

Jenkinsは主に、Maven Centralの代わりに専用のリポジトリを使います。そのため、POMファイルにそのリポジトリを追加する必要があります(もちろん、settings.xmlファイルを使って外部から設定しても構いません)。

HelloWorldプラグイン用に生成された.hpiファイルには、以下の内容が含まれるはずです。

META-INF/MANIFEST.MF
WEB-INF/lib/hello-jenkins-plugin.jar
WEB-INF/licenses.xml

OSGiモジュールの場合と同様に、ここで実際に重要な意味を持つのがMANIFEST.MFファイルです。このファイルは、プラグイン・クラスの名前が含まれていた古いバージョンのJenkinsプラグインのときほどの重要性はありませんが、それでも最低限必要なJenkinsバージョンについての重要な情報が含まれています。

Manifest-Version: 1.0
Build-Jdk: 11.0.8
Extension-Name: hello-jenkins-plugin
Specification-Title: The Jenkins Plugins Parent POM Project
Implementation-Title: hello-jenkins-plugin
Implementation-Version: 1.0
Group-Id: org.omnifaces.example
Short-Name: hello-jenkins-plugin
Long-Name: hello-jenkins-plugin
Minimum-Java-Version: 1.8
Plugin-Version: 1.0
Hudson-Version: 2.204
Jenkins-Version: 2.204
Plugin-Developers: 
Plugin-ScmUrl: https://github.com/jenkinsci/plugin-pom/hello-jenkins-plugin

プラグインをインストールしようとするとき、Jenkinsではこのバージョンをチェックします。おわかりのように、親が4.9の場合、デフォルトは少し古めのLTSリリース2.204に設定されます。Mavenプロパティjenkins.versionを使うと、このバージョンを明示的に上書きすることができます。次に例を示します。

<jenkins.version>2.222.3</jenkins.version>.

.hpiファイルに含まれる.jarファイルを見てみると、以下の内容が含まれていることがわかります。

META-INF/annotations/hudson.Extension
META-INF/annotations/hudson.Extension.txt
org/omnifaces/example/HelloWorldDescriptor.class
org/omnifaces/example/HelloWorld.class
org/omnifaces/example/HelloWorld.stapler

META-INF/annotationsの中には、@hudson.Extensionアノテーションのインデックス・ファイル(1つはバイナリ・オブジェクト・ストリーム形式、もう1つはもう少し人間が読みやすい形式)があります。さらに、プラグイン・コードの2つの.classファイルと、コンストラクタのパラメータ名を含む.staplerファイルがあります。先ほどの例のとおり、パラメータのないコンストラクタを使ったので、.staplerファイルのエントリはもちろん空になります。

このプラグインを実行するには、ブラウザでhttp://localhost:8080/pluginManager/advanced(管理者アクセスが可能なローカルホストにJenkinsをインストールしたものとします)を開き、「Upload Plugin」→「Browse」をクリックして、先ほどのプロジェクトをビルドしたターゲット・フォルダに移動します。その際の様子を1に示します。

Navigating to the correct Jenkins folder to find the .hpi file

1適切なJenkinsフォルダに移動し.hpiファイルを見つけて選択

Upload」をクリックし、プラグインがインストールされるまで待ちます。その後、http://localhost:8080/view/all/newJobに移動してFreestyleジョブを作成します。すべてうまくいけば、2に示すように、Buildセクションのビルド手順としてHelloWorldプラグインが表示されます。

The build step in Jenkins for the HelloWorld plugin

2Jenkinsに含まれる、HelloWorldプラグインのビルド手順

HelloWorld」を選択してプロジェクト構成を保存します。「Build now」をクリックしてビルドを実行し、http://localhost:8080/job/plugin-test/1/consoleに移動してログを調べます。3の出力に「Hello, world!」が表示されていることからわかるように、プラグインは動作しました。

The plugin ran successfully and said “Hello, world!”

3プラグインの実行が成功して出力された「Hello, world!

Jenkinsプラグインのデバッグ

すばらしいプラグインを開発することは重要ですが、どこかでデバッグを行った方がよいでしょう。最適な形でデバッグを行うには、jenkins.versionプロパティで設定されたJenkinsのバージョンと、プラグインをデバッグするJenkinsのバージョンとが正確に一致しているのが最善であることに注意してください。

これを行う方法の1つは、Jenkinsをデバッグ・モードで起動することです。デバッグ・モードで起動する正確な方法は、Jenkinsをどのようにインストールしたかによってやや異なる部分もあります。Linuxでディストリビューションのパッケージ・マネージャを使ってインストールしている場合は、/etc/default/Jenkinsまたは/etc/sysconfig/Jenkinsというファイルが存在することが一般的です。このファイルは、Java引数を追加する際に使用できます。次の例をご覧ください。

agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=9009

これを前述のJenkinsファイルのJAVA_ARGSに追加します。ファイル全体は次のようになります。

# Jenkins自動化サーバーのデフォルト
 
# 簡略化するため、initスクリプトから取得したもの
NAME=jenkins
 
# Javaに渡す引数
 
# Xサーバーが存在する場合でも、グラフなどが動作するようにする
JAVA_ARGS=”-Djava.awt.headless=true -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=9009″
 
#JAVA_ARGS=”-Xmx256m”
 
# JenkinsにIPv4アドレスをリスニングさせる
#JAVA_ARGS=”-Djava.net.preferIPv4Stack=true”
 
PIDFILE=/var/run/$NAME/$NAME.pid
 
# 起動ユーザーとグループ(デフォルトはJenkins)
JENKINS_USER=$NAME
JENKINS_GROUP=$NAME
 
# Jenkins WARファイルの場所
JENKINS_WAR=/usr/share/$NAME/$NAME.war

Linuxでsudo service jenkins restartまたはsudo systemctl restart Jenkinsなどと入力してJenkinsを再起動すると、EclipseなどのIDEをアタッチすることができます。ブレークポイントを設定してジョブを再起動すると、4のような表示になります。

Setting a breakpoint in the Jenkins plugin using Eclipse

4Eclipseを使用してJenkinsプラグインにブレークポイントを設定

デバッグを行うことにより、プラグインを書いているシステムでどのようにコードが呼び出されているかについての理解を深めることもできます。Eclipse(やその他ほとんどのIDE)では、コール・スタックのステップを容易に戻すことができます。先ほど紹介した親のPOMファイルには、ターゲットとなるJenkinsバージョンの部品表(BOM)ファイルが含まれているので、Mavenプロジェクトで公開されているソース・コードの大部分を取得し、IDEで表示することができます。5をご覧ください。

The source code available to the Maven project

5Mavenプロジェクトで公開されているソース・コード

記述子と実装クラスの結合

Jenkinsではインナー・クラスを多用します。おそらく、その頻度は世の中に存在するほとんどのJavaコードベースを上回っているでしょう。そのため、Jenkinsプラグインには記述子をインナー・クラスにするための特別な規則があることも不思議ではありません。

しかし、これは少しばかり奇妙に思えます。というのも、インナー・クラスがスキャンとインデックス作成の対象になっていて、アウター・クラスをインスタンス化するというのは、おそらく皆さんが想定することではないからです。

反面、この規則のおかげで、コンストラクタでクラス名の指定を省略できます。記述子では、アウター・クラスから自動的に名前を取得します。

記述子と実装クラスを組み合わせると、プラグインの実装は次のようになります。

public class HelloWorld extends Builder {
    
    @DataBoundConstructor
    public HelloWorld() {
    }

    @Override
    public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) {
        listener.getLogger().println("Hello, world!");
        return true;
    }
    
    @Extension
    public static class HelloWorldDescriptor extends BuildStepDescriptor<Builder> {
        @Override
        public boolean isApplicable(Class<? extends AbstractProject> jobType) {
            return true;
        }
    }
}

記述子への注入

JSR 330の注入とJSR 250の@PostConstruct、そしてプラグインのファクトリとして動作するDescriptorの実例を示すため、次のように少しばかりデモのコードを変更します。

@Extension
public class HelloWorldDescriptor extends BuildStepDescriptor<Builder> {
    
    @Inject
    private Jenkins jenkins;

    private String name;
    
    public HelloWorldDescriptor() {
        super(HelloWorld.class);
    }
    
    @PostConstruct
    public void init() {
        name = jenkins.getAssignedLabels()
                      .stream()
                      .map(e -> e.toString())
                      .findFirst()
                      .orElse("unknown");
    }
    
    @Override
    public Builder newInstance(StaplerRequest req, JSONObject formData) throws FormException {
        HelloWorld helloWorld = (HelloWorld) super.newInstance(req, formData);
        helloWorld.setName(name);
        
        return helloWorld;
    }

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

上記のように変更して、HelloWorldDescriptorに現在のJenkinsインスタンスを注入しました。注入後に呼び出される@PostConstructメソッドでは、プロジェクトが構成されているノードに割り当てられた最初のラベルを取得しました。さらに、ラベルのコレクションが空である可能性も考慮し、結果をインスタンス変数nameに格納しました。

続いてコードはnewInstance()メソッドをオーバーライドし、そこで通常どおりスーパーメソッドにインスタンスを作成させています。このインスタンスを作成するのがDescriptorの役割です。先ほど計算した名前を設定した後で、インスタンスを返却してJenkinsが使用できるようにしています。

プラグインの実装クラスを、次のように少しだけ更新します。

public class HelloWorld extends Builder {
    
    private String name;
    
    @DataBoundConstructor
    public HelloWorld() {
    }

    @Override
    public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) {
        listener.getLogger().println("Hello, " + name);
        return true;
    }
    
    public void setName(String name) {
        this.name = name;
    }

}

.hpiアーカイブを再インストールしてジョブを再実行すると、ノードのラベルにmainを割り当てていた場合なら、今回はHello, mainと表示されるはずです。

プラグインの実装の構成と注入

プラグインの実装に関する先ほどの説明では、プラグイン構成から生成されるJSONペイロードの値をStaplerがプラグイン・クラスに注入すると述べました。この構成がどこから来るのかについて、さらに詳しく確認してみましょう。

Jenkinsプラグインのすばらしい機能の1つは、プラグイン自体のUIフラグメントをバンドルして構成を行う点です。Jenkinsでは、構成キーが何であるかをこのフラグメントから導出することができます。そのため、UIフラグメントは構成パラメータの宣言としても、ユーザーが値を指定できる実際のUIのマークアップとしても機能します。

Jenkinsでは、このUIフラグメントにJellyと呼ばれるApacheのフレームワークを使用しています。このフレームワークの説明には、「JavaおよびXMLベースのスクリプト・エンジン」とあります。Jellyはシンプルでありながら強力なスクリプト・エンジンで、JavaServer Pages Standard Tag Library(JSTL)、Velocity、Declarative Velocity Style Language(DVSL)、Ant、Cocoonの優れた考え方が集約されています。Jellyが開発され、実質的に完成したのは2002年から2003年初めにかけてですが、その後に多くのアップデートが行われることはありませんでした。

Jellyのフラグメントは、構成対象のプラグイン・クラスの完全修飾名に基づいて名付けられたディレクトリを用いて、config.jellyという名前でJARに挿入されます。

greetingという名前のコンストラクタ・パラメータを受け取るように、プラグイン・クラスを更新します。

package org.omnifaces.example;
// …
public class HelloWorld extends Builder {
    
    private String greeting;
    private String name;
    
    @DataBoundConstructor
    public HelloWorld(String greeting) {
        this.greeting = greeting;
    }

    @Override
    public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) {
        listener.getLogger().println(greeting + ", " + name);
        return true;
    }
    
    public void setName(String name) {
        this.name = name;
    }
}

このクラスの完全修飾名はorg.omnifaces.example.HelloWorldなので、Mavenプロジェクトのsrc/main/resourcesorg/omnifaces/example/HelloWorldディレクトリを作成し、その中にindex.jellyというファイルを作成する必要があります。ファイルの内容は次のコードです。

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
    <f:entry title="Greeting" field="greeting">
        <f:textbox default="Hello" />
    </f:entry>
 </j:jelly>

このフラグメントでは、greetingという構成パラメータ(field属性で記述しています)を定義しています。これで、値を入力するテキスト・ボックスがユーザーに表示されます。テキスト・ボックスには、デフォルト値としてHelloが表示されます。

これを再ビルドすると、次の内容の.jarファイルを含む.hpiアーカイブが生成されます。

META-INF/annotations/hudson.Extension  
  META-INF/annotations/hudson.Extension.txt  
  org/omnifaces/example/HelloWorld/config.jelly  
  org/omnifaces/example/HelloWorld.stapler  
  org/omnifaces/example/HelloWorld.class 
  org/omnifaces/example/HelloWorldDescriptor.class

コンストラクタ・パラメータを使ったので、今回のHelloWorld.staplerファイルにはその名前が含まれています。内容は次のようになります。

constructor=greeting

この状態で、先ほど作成したJenkinsジョブの構成ページを開くと、レンダリングされたJellyフラグメントの結果を即座に確認することができます。6をご覧ください。

A dialog box the user sees

6ユーザーに表示されるダイアログ・ボックス

Halloと表示されるようにテキストを変更してジョブを再ビルドすると、ジョブから今度はHallo, mainが出力されます。この点から、構成と注入が動作したことがわかります。デバッガを使うと、JSONペイロードを確認することができます。この例では、次のようになります。

{   “greeting":"Hallo",
    “stapler-class":"org.omnifaces.example.HelloWorld",
    “$class":"org.omnifaces.example.HelloWorld"}

まとめ

Jenkinsプラグインの作成は難しすぎるものではありませんが、一度にさまざまなフレームワークを使う必要があります。本記事では、Jenkins自体のAPIとともに、SezPoz、Guice、Stapler、Jellyを使いました。Jenkinsプラグインに関連する2つのメイン・クラスが別々に管理および注入されることも見てきました。

これらすべての内容を見ると、最初はとまどってしまうかもしれません。しかし、前述のフレームワークとの関わりを、いくつかの規則とアノテーションの使用にとどめることができます。今回説明したように、提供されるMavenの親POMファイルを使う場合は、特にそれが当てはまります。この親POMファイルには非常に癖があることも見てきました。つまり、処理方法が少しばかり異なる点が難題かもしれないということです。

Builder型のHelloWorldプラグインも、いくつかの種類を確認しました。今回のデモは表面をなぞっただけです。というのも、作成したプラグインはあまり役に立つものではありません。プラグインのBuildStepでは役立つことを何も行いませんでした。また、プラグインの型は、本記事で触れたもの以外にも多数存在します。

構成UIに機能を提供するためのUIテンプレートとして使用したJellyについても、簡潔に確認しました。しかし、非常に基本的な点を除き、Jellyの構文やその他の機能の説明は行いませんでした。

まだ取り上げるべきことは数多くありますが、本記事が優れた出発地点となったことを願っています。

さらに詳しく

 

Arjan Tijms

 

Arjan Tijms:JSF(JSR 372)およびSecurity API(JSR 375)で専門家グループのメンバーを務めた。現在はJakarta Security、Jakarta Authentication、Jakarta Authorization、Jakarta Faces、Jakarta Expression Languageなど、多くのJakartaプロジェクトでプロジェクト・リードを担当。2015年のDuke’s Choice Awardを受賞した、JSF用の人気ライブラリOmniFacesの共同作成者で、2冊の書籍『The Definitive Guide to JSF』と『Pro CDI 2 in Java EE 8』の著者でもある。オランダのライデン大学でコンピュータ・サイエンスの理学修士号を取得している。Twitterのフォローは@arjan_tijmsから。