Skip to main content
  1. 🤔 Blog/

How to get version defined in pom.xml

Getting ways of the version defined in pom.xml
#

Java 17が出たことだし,いい加減に Java のモジュールシステムを本格的に使いだそうとしている. 最近のJavaの自作ツールは一応モジュール対応にしたつもり(pochivhcなど).

で,ビルドツールは Maven を使うことが多いのだけど,pom.xml で設定したバージョン情報をアプリケーションからどんな情報で取得できるかを確認してみた.

次の4つの方法に分類できる.

  1. Constant: 自分でバージョンの文字列をString型リテラルとしてソースコードに書き込む.
  2. Property: src/main/resourcesにプロパティファイルとしてバージョン情報を置いておく.
  3. Package: MANIFEST.MFに書かれている Implementation-VersionSpecification-Versionのいずれかを利用する.
  4. Module: ModuleDescriptorversionメソッドから利用する.

それぞれの分類を独断と偏見で4段階で評価してみた(1が良くて,4が悪い).

ConstantPropertyPackageModule
わかりやすさ1234
取得のしやすさ1222
自動化3213

分かりやすさはバージョン番号取得ルーチンをみたときのわかりやすさ.Constantは単なる変数参照なので一番わかりやすいであろう.Propertyはプロパティファイルを探し,読み込み,エントリを取得する,という3段階が必要となる.

Packageはバージョン番号取得の処理自体はそれほど難しくはない.ただし,なぜそのように取得できるのか,どうやればそのように取得できるようにするのかに対して,jarファイル,MANIFESTファイルなどの知識が必要になるため Propertyよりも難しいと判断している.

同様にModuleもバージョン番号取得の処理自体はPackageと同様であるが,やはりモジュールシステムに対する理解が多少なりとも必要であるため,Packageよりも難しいと判断した.

Constant
#

public class VersionProvider {
  public static final String VERSION = "1.0.0";
  public String version() {
    return VERSION;
  }
}

みたいな感じ.設定は分かりやすいし,参照もしやすい.けれど自動化のためには別途スクリプトなどが必要となろう.Mavenのtemplating-maven-pluginを使う方法もあろう

Property
#

public class VersionProvider {
  public String version() {
    URL url = getClass().getResource("/resources/version.properties");
    try(InputStream in = url.openStream()) {
      Properties p = new Properties();
      p.load(in);
      return p.getProperty("someapp.version");
    } catch(IOException e) {
      throw new InternalError(e);
      // IOExceptionが発生するということはプログラムの何かがおかしいため,
      // アプリケーション自体を終了させる.
    }
  }
}

src/main/resources/resources/version.properties
#

someapp.version=${project.version}

こんな感じ.pom.xml に次のようなエントリを入れておくと上記の${project.version}を自動的にversionタグの内容に置き換えてくれる.

一度設定すると取得できなくなることはあまり考えられないが,万が一バージョン情報が取得できなくなった場合,速やかに対応しないといけないため,取得できなかった場合にInternalErrorで終了するようにしている.

pom.xml
#

...
  <build>
    ...
    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
      </resource>
    </resources>
    ...
  </build>
...

Package
#

public class VersionProvider {
  public String version() {
    return getClass().getPackage()
      .getImplementationVersion();
  }
}

これはjarファイル内のMETA-INF/MANIFEST.MFに書かれているエントリを読んで出力している.このエントリを追加するには,pom.xmlに次のようなエントリを入れておくと良い.

  ...
  <build>
    ...
    <plugins>
      ...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.1.2</version>
        <configuration>
          <archive>
            <manifest>
              <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
            </manifest>
          </archive>
        </configuration>
      </plugin>
      ...
    </plugins>
    ...
  </build>
...

Module
#

public class VersionProvider {
  public String version() {
    return getClass().getModule()
      .getDescriptor()
      .version().toString();
  }
}

これで取得できるはず.module-info.javaでバージョンを設定する箇所はないため,jarファイルでは設定できないと思われる.

jmodファイルを作成するjmodコマンドに--module-versionオプションがあるため,これを利用して指定するのであろう.

What the case in the use of native-image?
#

で,ここからが本番.GraalVM を使ってネイティブイメージを作成したときにバージョン情報を取得できなくなっているのはいただけない.

ということで,次のプログラムを用意した.プログラムの全貌は tamada/version_from_pomにあるので参照されたい.なお,どれくらい差が出るかはわからないが時間も計測してみた.

    public interface VersionProvider extends Supplier<Pair<Version, String>> {
        // 読み込んだバージョン番号とどこから読み込んだかを返す.
        Pair<Version, String> get();
    }
    public void perform() {
        Stream.of(new ConstantVersionProvider(), new PropertyVersionProvider(),
                        new PackageVersionProvider(), new ModuleVersionProvider())
                .forEach(provider -> printResult(provider));
    }
    private void printResult(VersionProvider provider) {
        var result = measure(() -> provider.get());
        Pair<Version, String> pair = result.right();
        String version = String.valueOf(pair.left());
        long time = result.left();
        System.out.printf("%5s,%10d nano sec,%s%n", version, time, pair.right());
    }
    private <R> Pair<Long, R> measure(Supplier<R> supplier) {
        long from = System.nanoTime();
        R result = supplier.get();
        return Pair.of(System.nanoTime() - from, result);
    }
    public static class ConstantVersionProvider implements VersionProvider {
      ... // 上記の Constant とほぼ同じ
    }
    public static class PropertyVersionProvider implements VersionProvider {
      ... // 上記の Property とほぼ同じ
    }
    public static class PackageVersionProvider implements VersionProvider {
      ... // 上記の Package とほぼ同じ
    }
    public static class ModuleVersionProvider implements VersionProvider {
      ... // 上記の Module とほぼ同じ
    }

このプログラムを次の5つの方法で実行した結果を示す.

  • Plain: java -cp target/classes jp.cafebabe.vfp.Main
    • jarにまとめないまま実行する方法.
  • Jar: java -jar target/mods/vfp-1.0.0.jar
    • Java 8以前の普通の実行方法,
  • Module: java --module-path target/mods --module jp.cafebabe.vfp/jp.cafebabe.vfp.Main
    • Modular jar として実行する.
  • Native-Jar: native-image -jar target/mods/vfp-1.0.0.jar vfp-jar
    • native-image でjarを渡して実行ファイルを作成して,作成された実行ファイルを実行する.
  • Native-Module: native-image --module-path target/mods --module jp.cafebabe.vfp/jp.cafebabe.vfp.Main vfp-module
    • native-imageにmodular jar を指定して実行ファイルを作成する.そして,作成された実行ファイルを実行する.

jmodコマンドで作成された jmod ファイルは実行できない.ただし,jmodコマンドではバージョンの指定が行える(--module-versionオプションで指定できる).では,jmodコマンドで作成された jmod ファイルの中から module-info.class を取り出し,元のmodule-info.class を置き換えてclasses2に置く.そして,classes2から新たにjarファイルを作成してみた.mods2/vfp2-1.0.0.jar とする.これでも実行してみる.

  • Plain2: java -cp classes2 jp.cafebabe.vfp.Main
  • Jar2: java -jar target/mods2/vfp2-1.0.0.jar
  • Native-Jar2: native-image -jar target/mods2/vfp2-1.0.0.jar vfp2-jar
  • Native-Module2: native-image --module-path target/mods2 --module jp.cafebabe.vfp/jp.cafebabe.vfp.Main vfp2-module

Result of Measurements
#

結果を以下に示す.列がバージョン番号の取得方法,行が実行方法を表している.実行環境は macOS Big Sur (11.6),チップ Apple M1, メモリ 16GB,graalvm64-17.0.1である.

ConstantPropertyPackageModule
Plain563,500 nsec7,336,375 nsecnullException
Jar3,145,250 nsec29,883,833 nsec25,667 nsecException
Module179,583 nsec9,867,208 nsecnullnull
Native-Jar35,791 nsecException1,442,458 nsecException
Native-Module113,875 nsecExceptionnullnull
Plain2396,000 nsec12,512,417 nsecnullException
Jar2556,750 nsec40,243,417 nsec57,875 nsecException
Module2145,708 nsec19,945,375 nsecnull38,522,208 nsec
Native-Jar241,583 nsecException1,448,750 nsecException
Native-Module294,125 nsecExceptionnull193,166 nsec

表からわかるように,一番簡単で高速なのはConstant であるが,自動化しておかなければ実際のバージョンと表示されるバージョンに差が出る可能性がある.

ネイティブコードでない場合は,Property が安定して取得できるものの,遅い.実行方法によって,PackageModuleで取得できるか否かが決められる.て言うかなんでネイティブコードにすると取得できなくなるんだよう.

なお,Moduleからバージョンを取得するためには,module-info.classにバージョン情報を加える必要がある.そのためには,jmodコマンドでバージョンを指定したjmodファイルを作成し,そこからmodule-info.classをコピーしてjarファイルなどを作成し直す必要がある(めんどくせ...).

加えて,native-imageの実行オプションで取得できる情報が異なるのは注意が必要であろう.

一番安定して取得できるのがConstantなのは意外でもなんでもなくその通りなのだが,あまり面白くない結果だな.

Module でバージョン番号を取得できるのは良いとして,準備が大変.もっと簡易にできる方法はないのかな???

ネイティブコードを作成するときのオプション(-jar-module)で変数の参照の実行時間に大きな差が出るのはなんでだろう.

Summary
#

結局のところ,バージョン番号は定数として扱うのが簡単で,どのような形態で実行されようが同様に取得できる.このことから定数として扱うのが一番良いように思う.ただし,定数として扱う場合,バージョン番号を更新したときに自動的に定数の値も更新されるような仕組みを導入しておかなければ定義と出力に差異が生じる.

次にPackageもしくはModuleで取得するのが良いが,native-image でビルドするときのオプションに注意しないといけない.注意しないといけないと書いている次点でダメだけど.

Propertyはネイティブイメージにする必要がない場合はOKだが,そうでない場合やめた方が良い.予想外のエラーで悩まされる可能性があるためである.

実行時間はいずれの方法でも誤差の範囲であろう.バージョン番号取得のみの時間で見るとそれぞれが大きな差のように思えるが,実際のアプリケーションの場合,バージョン番号取得以外にも様々な処理が行われる.そのような様々な処理の中から見るとバージョン番号取得の時間短縮に気を配るよりも他の処理の短縮に気を配る方が建設的であるためである.

References
#