RSS2.0

Maven pom.xml のバージョン番号を Java プログラムから取得する

LWJGFont ver1.1 の開発で、LWJGFont のバージョンを埋め込んだ別アーティファクト用の pom.xml を動的に生成しなければならない、という場面がありました。私は Java プロジェクトの管理に Apache Maven3 を使っているので、LWJGFont 自体のバージョン番号は LWJGFont プロジェクト自身の pom.xml に記載されています。ところがバージョン番号自体を扱いたいのは LWJGFont のロジックだったので、状況としては、Java プログラムから自身の Maven プロジェクトの pom.xml に書いてあるバージョン情報を取得する、ということになります。

pom.xml.png

開発中にいろいろ調べてみたのですが、簡単にとれるということではないにしろ、実現方法はいくつかありました。それぞれ違った特徴があるのでどれを採用するかはプロジェクトの置かれた状況によっても違ってくるでしょうが、LWJGFont での採用理由も含め、備忘録がてらに書き残しておこうと思います。

Maven プロジェクトのバージョンを Java から取得する方法

・java.lang.Package.getImplementationVersion() を利用する
・maven-antrun-plugin でファイルにバージョンを書き出す
・exec-maven-plugin でファイルにバージョンを書き出す

java.lang.Package.getImplementationVersion() を利用する

Java 自体の機能として、java.langPackage には、パッケージが実装する仕様のバージョンを返す getSpecificationVersion() と、実装自体のバージョンを返す getImplementationVersion() というメソッドが用意されています。
ただし、Java の文法としてこれらのバージョンを記載することが必須でないことからもわかるとおり、必ずしもこのメソッドでバージョン情報が取れるわけではありません。jar ファイルに含まれる META-INF/MANIFEST.MF にはこの仕様のバージョンと実装のバージョンを記載する項目があり、これを記載した META-INF/MANIFEST.MF を含む jar から読み込んだパッケージは、バージョン情報を取得することができます。

maven-jar-plugin を使って jar の MANIFEST.MF にバージョンを記載することができます。
pom.xml の plugins タグ内に maven-jar-plugin を追加し、下記のように、addDefaultImplementationEntries タグと addDefaultSpecificationEntries タグに true を設定しておきます。
<build>
  <plugins>
    ...
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <configuration>
        <archive>
          <manifest>
            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
          </manifest>
        </archive>
      </configuration>
    </plugin>
    ...
  <plugins>
<build>
この pom.xml で jar ファイルをビルドすると、MANIFEST.MF には以下のように、仕様のバージョンと実装のバージョンが追加されるようになります。
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: momokan
Build-Jdk: 1.7.0_07
Specification-Title: sample
Specification-Version: 1.0-SNAPSHOT
Implementation-Title: sample
Implementation-Version: 1.0-SNAPSHOT
Implementation-Vendor-Id: sample

あとは、Java 側で以下のように、Package.getImplementationVersion() を呼び出せば、Maven プロジェクトの pom.xml に記載されている自身のバージョンを取得することができます。
package sample;

public class Main {

	public static void main(String[] args) {
		System.out.println(Main.class.getPackage().getImplementationVersion());
	}

}

ただし、この方法は利用するパッケージが jar ファイルに含まれている場合にのみ利用できるようになります。例えば Eclipse 上で動作させると、クラスは Eclipse 自身によってコンパイルされてそのまま読み込まれるので、バージョンを取得することができません。
成果物を必ず jar で配布し、かつ実行時のみ取得できればいいという条件下ではこの方法が有効となります。

maven-antrun-plugin でファイルにバージョンを書き出す

実行時だけでなく、開発時にも Maven プロジェクトのバージョンを取得したいとなると、一度 pom.xml のバージョンをファイルに書き出し、Java 側でそのファイルを読み込むという手段を思いつくと思います。pom.xml 内では変数 ${project.version} に自身のプロジェクトのバージョンが設定されているので、この変数の値をなんらかの手段でファイルに書き出せれば、バージョン情報を Java プログラムと共有することができます。

Ant であればこのような処理は簡単に対応することができます。しかし残念ながら Maven には直接ファイル操作をするような機能はありません。Maven 自体の設計思想が Ant とは異なり、各フェーズに必要となる操作をプラグインの積み重ねとして管理する、というフレームワークとしての役割を定義するものに終始しているため、Maven 単体でファイル操作のような局所的な処理を実現することまでは考えられていないのです。決して Maven の力不足ということではなく、そのような局所的な処理は各 Maven プラグインに任されているということです。

それでも細かい所まで何でもできる Ant のタスクを使いたいというユーザーのために、Maven には Ant と連携するための Maven AntRun Plugin が用意されています。
これは Maven AntRun Plugin が実行されるフェーズにおいて、Ant から build.xml の特定のターゲットを実行することができます。

まずは pom.xml に下記のように maven-antrun-plugin を設定します。
<build>
  <plugins>
    ...
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-antrun-plugin</artifactId>
      <executions>
        <execution>
          <phase>initialize</phase>
          <goals>
            <goal>run</goal>
          </goals>
          <configuration>
            <tasks>
              <ant target="make-version-file" />
            </tasks>
          </configuration>
        </execution>
      </executions>
    </plugin>
    ...
  <plugins>
<build>
実行フェーズを initialize としているのは、mvn compile だけでなく Eclipse と連携する際に実行する mvn eclipse:eclipse でも、最新の pom.xml のバージョンを取得するためです。tasks 内の ant に実行する build.xml のターゲットを設定します。

Maven から呼び出される build.xml がこちら。
<project basedir=".">
  <target name="make-version-file">
    <xmlproperty file="pom.xml" prefix="pom.xml" />
    <echo message="PROJECT.VERSION=${pom.xml.project.version}" file="src/main/resources/sample/version.properties" />
  </target>
</project>
xmlproperty タグで pom.xml をそのまま読み込めるので、Ant 側からは pom.xml のすべての値にアクセスできます。
Maven プロジェクトのバージョンには ${pom.xml.project.version} でアクセスできますが、${pom.xml.project.dependencies.dependency.groupId} と書けば、依存しているアーティファクトのグループ ID にアクセスできます。
上記 build.xml については Ant の範疇になるので詳細は割愛しますが、echo タスクによって src/main/resources/sample/version.properties にバージョン情報を書き出しています。この version.properties を Java から読み込みます。

Java 側からは以下のようなプログラムで、Ant で書き出した version.properties を読み込みます。
package sample;

import java.io.IOException;
import java.util.Properties;

public class Main {

	public static void main(String[] args) throws IOException {
		Properties	properties = new Properties();

		properties.load(Main.class.getResourceAsStream("version.properties"));

		System.out.println(properties.get("PROJECT.VERSION"));
	}

}
version.properties の読み込みに java.lang.Class.getResourceAsStream() を使っているのは、jar に固められた場合にも jar の中から読み込めるようにするためです。
Maven から build.xml を使うというと身構えてしまいがちですが、思いのほか簡単に連携することができます。

なお、書き出される version.properties 自体は随時ファイルの内容が変わってくるはずなので、ソースコードのバージョン管理システムからは無視するファイルとして設定しておくことをおすすめします。Subversion では svn:ignore 属性に、Git では .gitignore に version.properties を設定しておいてください。


さて、LWJGFont ではこの方法は採用していません。理由は完全に個人的な嗜好の域なのですが、Maven と Ant の二刀流は最後の手段としたかったためです。Ant を使うならすべて Ant ですませようという方向になりかねませんし、build.xml と pom.xml があるとトップディレクトリを見ただけではビルド方法がわからないためです。pom.xml があったら誰でも何も考えずに mvn package する、というのがオープンソースプロジェクトとしてのわかりやすさなんじゃないかなという思いがあります。

exec-maven-plugin でファイルにバージョンを書き出す

基本的なアプローチはさきほど触れた maven-antrun-plugin を使う方法と同じなのですが、Ant は使わずに exec-maven-plugin を使います。maven-antrun-plugin プラグインは、OS のコマンドを実行するプラグインです。実行するコマンドの引数には pom.xml から ${project.version} を渡せるので、それをファイルに書き出すのは OS コマンドを利用しようという方針になります。

pom.xml には以下のように exec-maven-plugin を設定します。
<build>
  <plugins>
    ...
    <plugin>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>exec-maven-plugin</artifactId>
      <executions>
        <execution>
          <phase>initialize</phase>
          <goals>
            <goal>exec</goal>
          </goals>
        </execution>
      </executions>
      <configuration>
        <executable>java</executable>
        <arguments>
          <argument>-cp</argument>
          <argument>lib</argument>
          <argument>sample.VersionStamp</argument>
          <argument>${project.version}</argument>
        </arguments>
      </configuration>
    </plugin>
    ...
  <plugins>
<build>
実行フェーズは先ほどと同じく initialize フェーズです。
実行するコマンドが executable タグの設定値ですが、ここで java コマンドを指定し、arguments タグ内で java コマンドから実行するクラスやクラスパス、${project.version} 値を指定しています。
ここで echo コマンドを指定すれば話は早いのですが、OS コマンドの標準出力をファイルに出力することができませんでした。マニュアルを見る限り、maven-antrun-plugin にはプラグインの設定として outputFile タグが用意されていて、標準出力と標準エラーをファイルに書き出せるようなのですが、これがうまく動いてくれませんでした。そこで、実行時引数として渡された ${project.version} 値をファイルを書き出す単純な Java プログラムを作り、java コマンドから呼び出す方法をとりました。

maven-antrun-plugin プラグインから呼び出されるクラスは以下のとおりです。
package sample;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;

public class VersionStamp {

	public static void main(String[] args) throws IOException {
		Properties		properties = new Properties();
		FileOutputStream	out = null;

		properties.put("PROJECT.VERSION", args[0]);

		try {
			out = new FileOutputStream("src/main/resources/sample/version.properties");
			properties.store(out, "VersionStamp");
		} finally {
			if (out != null) {
				try {
					out.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

}
これを単独でコンパイルし、できあがったクラスファイルを lib/sample/ 内に配置します。
$ javac src/main/java/sample/VersionStamp.java
$ mkdir -p lib/sample
$ mv src/main/java/sample/VersionStamp.class lib/sample/
これで mvn コマンドを実行すれば、version.properties が作成されます。
version.properties の仕様は maven-antrun-plugin プラグインの例と同じなので、sample.Main クラスと同じソースコードが利用できます。version.properties は忘れずソースコード管理システムから無視するファイルとして設定しておきましょう。

LWJGFont ver 1.1 開発時には、ここまでやるなら新しく Maven プラグインを作ったほうが綺麗なんじゃないかという思いも多分にしてありました。できる限り少ないコンポーネントでシンプルなシステムを作るべきというのは私としても目指す所ではあります。ただ、リリース間際だったこともあり、別プロジェクトを作って Maven Central Repository に登録するとなるとまた 1 週間程度かかりそうなので、java コマンドを利用する方法を採用しました。

この記事で触れたのは、LWJGFont を開発する中で私の中にあった選択肢ですが、もし他にもよい方法をご存知の方は、ぜひぜひ教えていただけると嬉しいです。


  LWJGFont 開発  コメント (0) 2014/03/04 07:45:53


公開範囲:
プロフィール HN: ももかん
ゲーム作ったり雑談書いたり・・・していた時期が私にもありました。
カレンダー
<<2024, 11>>
272829303112
3456789
10111213141516
17181920212223
24252627282930