※本記事は、Arjan Tijmsによる”How to write your own Maven plugins“を翻訳したものです。
意外と簡単に作れる、Apache Mavenビルド・ツールのカスタム・プラグイン
著者:Arjan Tijms
2020年8月3日
多くのJava開発者ツールは、プラグインで拡張できます。プラグインとは、細かい機能の集まりがツールと密接に統合されたもので、便利な働きをします。その3つの代表例は、Eclipse IDE、Jenkins自動化サーバー、Mavenビルド・ツールです。多くの場合、コード作成者は既製のプラグインを使っています。しかし、プラグインを作ることも容易です。Java開発者が使っている数々のツールと同様に、プラグインもJavaで書かれています。
そのうえ、プラグインを書くという経験によって、プラグインを組み込むシステムについての理解を深めることもできます。それでは、(ほぼ)すべての人に愛されているビルド管理ツール、Apache Mavenの仕組みについて、プラグインを書くことを通して見ていくことにします。
基本事項
ほぼすべてのMavenプラグインは、Maven Plain Old Java Object (Mojo)と呼ばれる1つのJavaクラスが中核となっています。この用語は、おなじみの用語であるPlain Old Java Object(Pojo)をもじったものです。Mojoが実際にPojoでもあるかという問題には、議論の余地があります。というのも、Pojoにはインタフェースの実装に関する要件があるからです。
MavenのMojoは、マネージド・オブジェクトです。具体的に言えば、Javaの依存性注入(DI)(JSR 330)によって管理され、2012年3月に登場したEclipse Sisuと呼ばれるモジュール式のコンテナに対応しています。
さらに詳しく見てみます。Sisu自体は、JSR 330の軽量実装としてよく知られたGuiceをベースに構築されています。Guiceは、JavaにおけるDIのパイオニアに名を連ねるBob Lee氏が作成したDIコンテナです。Sisuには、クラスパスのスキャン、自動バインディング、Guiceへのオートワイヤリングが追加されています。そのためSisuは、それらのことも自動的に行われる、コンテキストと依存性の注入(CDI)にやや近づいたものとなっています。SisuのCDIとプレーンなGuiceとの違いは、Guiceではバインディング(インタフェースに注入するための好みの実装をマッチングすること)をプログラムで行わなければならない点です。
巨視的に見れば、Mavenのプラグインの仕組みを実現するSisuは、それ自体が実質的にGuiceのプラグインです。
注意深い読者ならお気づきかもしれませんが、Sisuは2012年に登場したものであるにもかかわらず、Mavenは2004年7月から存在しています。Mavenでは当初、Plexusと呼ばれる、制御の反転(Inversion-of-Control、IoC)コンテナを使っていました。Plexusは実質的に独立したスタンドアロンのIoCコンテナで、必ずしもMavenに結びつけられたものではありませんでした。ただし、Mavenと同じ人々によって作られていました。またPlexusは、Guiceへの切り替え(後述)まで、Maven以外で採用されたことはほとんどありませんでした。そのため、ほぼMaven専用のIoCコンテナとなっていました。
類似するものがオープンソースに存在するのであれば、このような専用のコンテナをメンテナンスしなければならないというのは合理性に乏しいと言えます。そこでチームは、2010年のMaven 3の登場に合わせて、いくつかの独自拡張を組み込んだGuiceに切り替えることを決めました。Maven 3.0(2010年10月)では、まず内部的にのみGuiceを使い、既存のPlexusベースのプラグインがすべて動作し続けるように、互換性レイヤーを組み込んでいました。Guiceと、Mavenに貢献された拡張機能(このときまでにSisuと呼ばれるようになっており、Eclipseに移行していました)がプラグインとして一般公開されたのは、2013年7月になってMaven 3.1が登場してからでした。
Plexusとシームレスな互換性レイヤーは長い間存在していたため、Mavenプラグインでは今でもよくPlexusを見かけることがあります。
歴史についてひととおり説明しました。次はMavenプラグインをビルドしてみます。
「hello world」プラグインのビルド
まずは、次の「hello world」プラグインから始めます。
package org.omnifaces.mojo.group;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Mojo;
@Mojo(name = "hello")
public class HelloMojo implements org.apache.maven.plugin.Mojo {
private Log log;
@Override
public void execute() {
log.info("Hello, world");
}
@Override
public void setLog(Log log) {
this.log = log;
}
@Override
public Log getLog() {
return log;
}
}
次に示すのは、このプラグインをビルドするための最小限に近いpom.xmlです。
<?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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.omnifaces.example</groupId>
<artifactId>hello-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<prerequisites>
<maven>3.5</maven>
</prerequisites>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.plugin.skipErrorNoDescriptorsFound>true</maven.plugin.skipErrorNoDescriptorsFound>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>3.6.3</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
これとJARプロジェクトの主な違いは、パッケージングの種類がmaven-pluginであり、Mavenの最小バージョンを設定したprerequisites要素が必要な点です。さらに、必須プロパティmaven.plugin.skipErrorNoDescriptorsFoundもあります。このプロパティは常にfalseに設定する必要があります。ここでは、2つの依存性を使っています。@Mojoアノテーションを提供するmaven-plugin-annotationsと、Mojoインタフェース(および例外、抽象実装、今回使っているLog型などの関連する型)とSisuを提供するmaven-plugin-apiです。Sisuによって、JSR 330(とJSR 250)のアノテーションが使えるようになります。同時に、Plexusに関係したいくつかのクラスも取り込まれます。
maven-pluginパッケージングで主に行っていることは、ライフサイクルへのmaven-plugin-plugin(繰り返しは意図的なもので、誤植ではありません)の追加です。それによってMETA-INF/maven/plugin.xmlファイルが生成されます。これはOpen Services Gateway initiative(OSGi)プロジェクトとやや似ています。OSGiでは、「bundle」パッケージングでバンドル・プラグインが追加され、それによって特殊なMETA-INF/MANIFEST.MFが生成されます。
生成されるplugin.xmlファイルは次のようになります。
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by maven-plugin-tools 3.2 -->
<plugin>
<name>hello-maven-plugin</name>
<description></description>
<groupId>org.omnifaces.example</groupId>
<artifactId>hello-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<goalPrefix>hello</goalPrefix>
<isolatedRealm>false</isolatedRealm>
<inheritedByDefault>true</inheritedByDefault>
<mojos>
<mojo>
<goal>hello</goal>
<requiresDirectInvocation>false</requiresDirectInvocation>
<requiresProject>true</requiresProject>
<requiresReports>false</requiresReports>
<aggregator>false</aggregator>
<requiresOnline>false</requiresOnline>
<inheritedByDefault>true</inheritedByDefault>
<implementation>org.omnifaces.mojo.group.HelloMojo</implementation>
<language>java</language>
<instantiationStrategy>per-lookup</instantiationStrategy>
<executionStrategy>once-per-session</executionStrategy>
<threadSafe>false</threadSafe>
<parameters/>
</mojo>
</mojos>
<dependencies/>
</plugin>
<mojo>要素は、大部分が@Mojoアノテーションに対応しており、デフォルト属性の多くが明示的に定義されていることがわかります。
コマンドラインに次のように入力し、カスタム・プラグインを実行します。
mvn org.omnifaces.example:hello-maven-plugin:hello
次のような結果になるはずです。
[INFO] --------------< org.omnifaces.example:hello-maven-plugin >-------------- [INFO] Building hello-maven-plugin 1.0-SNAPSHOT [INFO] ----------------------------[ maven-plugin ]---------------------------- [INFO] [INFO] --- hello-maven-plugin:1.0-SNAPSHOT:hello (default-cli) @ hello-maven-plugin --- [INFO] Hello, world
この方法ではコマンドラインに打ち込む内容が多いため、プラグイン・グループとゴール・プリフィックスと呼ばれるものを使って入力内容を短縮する方法が用意されています。この仕組みにより、特定番号のグループIDの場合、グループIDを省略し、完全なアーティファクトIDの代わりに短いゴール・プリフィックスを使うことができます。Mavenでチェックされるプラグイン・グループは、settings.xmlファイル(~/.m2/settings.xmlなど)で指定できます。
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<pluginGroups>
<pluginGroup>org.omnifaces.example</pluginGroup>
</pluginGroups>
</settings>
この設定を行うことで、次のようにしてプラグインを起動できるようになります。
mvn hello:hello
Mavenプラグインのデバッグ
カスタム・プラグインをビルドしているならば、デバッグできなければなりません。その方法がすぐにはわからないこともあるかもしれません。いずれにせよ、開発者がただ単にIDEからMojoを起動するわけではありません。何らかの方法で実際のmvnコマンドに接続する必要があります。ありがたいことに、Mavenには最初からこの方法が用意されています。それがmvnDebugコマンドです。このコマンドにより、ポートにリモート・デバッガをアタッチすることができるように、起動直後に実行が一時停止されます。次の出力に示すように、このポートは、デフォルトでは8000です。
mvnDebug hello:hello Preparing to execute Maven in debug mode Listening for transport dt_socket at address: 8000
IDEからこのポートに接続できます。その場合、図1のようになります(ここではEclipseを使っています)。

図1:ポートに接続した状態
デバッグを行うことにより、システムでどのようにコードが呼び出されているかについての理解を深めることもできます。Eclipse(やその他多くのIDE)では、コール・スタックのステップを容易に戻すことができます。プラグインは、正しいソースにアタッチする必要があります。今回の場合は、org.apache.maven:maven-core依存性にDefaultBuildPluginManagerが含まれており、執筆時点においてコマンドラインで使われているMavenバージョンは3.6.3です。
Eclipseから一番簡単にソースにアクセスする方法は、提供される依存性org.apache.maven:maven-core dependency:3.6.3をプロジェクトのpom.xmlに追加することです。これを行った後、Mojoを呼び出したコードを確認してみます(図2参照)。

図2:Mojoを呼び出すコード
依存性の注入
「hello world」サンプルを拡張し、先ほどのMojoに依存性を注入してみます。ここでは、ユーザーに入力を求めるコンポーネントを使います。ところで、プロジェクトのビルドに加わるプラグインの場合は通常、ユーザーから入力を求めるのはよい考えではありません。ビルドはほぼ常に自動で実行される必要があり、通常は再現性も求められるからです。ただし、自動化が関心事ではない場合、ユーザーとやり取りすることに主眼を置いたMavenプラグインも存在します。
この例のコンポーネントは、org.codehaus.plexus:plexus-interactivity-api:1.0への依存性をPOMに追加して取得することができます。拡張したコードを次に示します。
@Mojo(name = "hello")
public class HelloMojo implements org.apache.maven.plugin.Mojo {
@Inject
private Prompter prompter;
private Log log;
@Override
public void execute() {
try {
String name = prompter.prompt("What's your name?");
log.info("Hello, " + name);
} catch (PrompterException e) {
new MojoExecutionException("Something went wrong", e);
}
}
@Override
public void setLog(Log log) {
this.log = log;
}
@Override
public Log getLog() {
return log;
}
}
コマンドmvn hello:helloでこれを実行すると、入力を求めるプロンプトがユーザーに表示されます。
[INFO] Building hello-maven-plugin 1.0-SNAPSHOT [INFO] ----------------------------[ maven-plugin ]---------------------------- [INFO] [INFO] --- hello-maven-plugin:1.0-SNAPSHOT:hello (default-cli) @ hello-maven-plugin --- What's your name?: Arjan [INFO] Hello, Arjan [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS
なお、依存性の名前ですでに示されていたとおり、このPrompterがPlexusコンポーネントであることに注意してください。Sisuが2013年に公開され、それから7年が経過したにもかかわらず、かなりの数のMaven用コンポーネントがPlexusコンポーネントです。
POMの操作
Mavenプラグインの中には、ユーザーがプロジェクトのPOMファイルを操作できるインタラクティブなプラグインで構成されているものがあります。その代表的な2つの例が、アーティファクトのバージョンをPOMで管理したい場合に使うversionsと、プロジェクトのpom.xmlファイルをクリーンアップするtidyです。
こういったタスクに、何らかのスタンドアロン・ツールではなくMavenプラグインを使うメリットは、MavenですでにPOMファイルが解析されており、POMファイルが「コンテキスト内」にあることです。つまり、親子関係、依存性の解決、パラメータなど、Mavenで実際に認識されていることに基づいてタスクが処理されます。
それでは、プロジェクトのグループIDを変更するプラグインを作成してみます。Maven APIには、プロジェクト構造を参照する機能はほぼそろっていますが、POMファイルに変更を書き込む機能はあまり含まれていません。そこで、ここではversionsプラグインを依存性として使い、そのコードの一部を再利用することにします。
Mojoの基本構造は次のようになります。
@Mojo(name = "setGroup", requiresProject = true, requiresDirectInvocation = true, aggregator = true, threadSafe = true)
public class SetGroupMojo extends AbstractVersionsUpdaterMojo {
@Parameter(property = "newGroupId")
private String newGroupId;
@Parameter(property = "groupId", defaultValue = "${project.groupId}")
private String groupId;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (project.getOriginalModel().getGroupId() == null) {
throw new MojoExecutionException("Project GroupId is inherited from parent.");
}
try {
for (File pomFile : collectPomFiles(getMavenProject(), getReactorModels(project, getLog()))) {
process(pomFile);
}
} catch (IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}
}
ここには、いくつかの新しい要素が登場しています。このプラグインではプロジェクトを扱っているため、@Mojoアノテーションでそのようなプロジェクトを定義する必要があります。さらに、プラグインを(コマンドラインで)直接起動することと、pom.xmlファイル経由でライフサイクルにアタッチしないことも必要です。
@Parameterアノテーションを付加したフィールドには、パラメータを注入できます。このパラメータは、pom.xmlファイルに含まれる、プラグインの構成セクションか、コマンドラインの-D定義プロパティで指定できます。具体的には、Mojoは注入を処理するJSR 330コンテナで管理されますが、@ParameterアノテーションはJSR 330とは関係ありません。たとえば、似たような仕組みを提供するEclipse MicroProfile Configでは、JSR 330の@Injectアノテーションや@Qualifierアノテーションを使用します。ここで特筆すべきもう1つのことは、Mojoでさまざまなものを注入する際によく使われるパターンです。具体的には、@Parameterアノテーションを付加したフィールドのdefaultValue属性の式が使用されます。たとえば、次のようにして設定を注入することがよくあります。
@Parameter(defaultValue = "${settings}", readonly = true )
private Settings settings;
この場合、現在のプロジェクトのグループIDが注入されますが、-Dパラメータで上書きすることができます。
前述のSetGroupMojo Mojoでは、リアクタ・マップ(相対モジュール・パスをキーとしたマップ)を使ってすべてのモジュールを収集してから、MavenProjectクラスの助けを借りてそれらのモジュールを絶対パスに変換しています。
private Set<File> collectPomFiles(MavenProject project, Map<String, Model> reactor) {
return
reactor.entrySet().stream()
.filter(e -> getGroupId(e.getValue()) != null)
.filter(e -> getArtifactId(e.getValue()) != null)
.map(e -> getModuleFile(project, e.getKey()))
.collect(toSet());
}
private File getModuleFile(MavenProject project, String relativeModulePath) {
File moduleDir = new File(project.getBasedir(), relativeModulePath);
if (project.getBasedir().equals(moduleDir)) {
return project.getFile();
}
if (moduleDir.isDirectory()) {
return new File(moduleDir, "pom.xml");
}
return moduleDir;
}
次に、ベース・クラスで提供されるちょっとした魔法によって、各POMファイルが新しいグループIDで更新されます。
@Override
protected synchronized void update(ModifiedPomXMLEventReader pom) throws MojoExecutionException, MojoFailureException, XMLStreamException {
try {
Model model = getRawModel(pom);
Parent parent = model.getParent();
// 親の更新
if (parent != null && groupId.equals(parent.getGroupId())) {
setProjectValue(pom, "/project/parent/groupId", newGroupId);
}
// プロジェクトの更新
if (groupId.equals(getGroupId(model))) {
setProjectValue(pom, "/project/groupId", newGroupId);
}
} catch (IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}
もちろん、これは小さな例です。説明を簡潔にするため、もっと強力なグループID変更プラグインならば行うであろうさまざまなことは省略しています。たとえば、今回のコードではプラグイン・セクションの更新は行いませんでした(プロジェクトがそれ自体のモジュールを参照している場合、この更新は便利です)。また、foo.bar.kaz内のfoo.barの名前を変更するなど、サブグループについて何らかの処理を行えるようにする必要もあるでしょう。これらの点は、演習として読者に委ねたいと思います。
まとめ
Mavenのカスタム・プラグインを書き始めるのは、比較的簡単です。しかし、Mavenには豊かな歴史があるため、最初のうちはさまざまな注入オプションが何を表すのかが少しわかりにくいかもしれません。
本記事では、2種類の「hello world」プラグインを取り上げました。1つはテキストを表示するだけのもの、もう1つはユーザーに入力を求めるものでした。また、プロジェクトのPOMファイルを操作するプラグインの例も紹介しました。
当然ながらこれは、Mavenプラグインでできることの表面をなぞっただけにすぎません。カスタムJavaコードを使い、Mavenの広範なAPIや利用可能なコンポーネントを使用すれば、可能性は無限大です。
![]() |
Arjan TijmsArjan 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から。 |

