2016年4月20日水曜日

Jar の META-INF/MANIFEST.MF に書いた Class-Path が効かない件

案外ググったりしてもぴったりの記事には遭遇しなかったので、ブログに書いておきます。

まずやりたかったことですが、Javaで書いたコードをjarにまとめて、それをjava -jar (やダブルクリック等)で開けるようにしようと思ったのですが、その際別のライブラリがjarで提供されているので、そのjarもclasspathに追加した状態で起動したい、ということでした。これ自体は META-INF/MANIFEST.MF に書けばなんら問題なくできます。できるはずでした。

まずそのまとめるjarのほうですが、mavenを使って書いていたので、pom.xmlにこのように書けば main メソッドの指定は簡単にできます。
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <configuration>
        <archive>
          <manifest>
            <mainClass>jartest.Main</mainClass>
          </manifest>
        </archive>
      </configuration>
    </plugin>
  </plugins>
</build>
これでビルドして実行すると
$ java -jar jartest-0.0.1.jar 
Hello!
当然ですが、問題ありません。

次に外部 jar の呼び出しですが、その前にこちらの jar の main メソッドをこのように書いておきます。
public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println("Loaded:" + Class.forName("jartest2.EndPoint").getName());
    }
}
呼び出される側は jartest2.EndPoint というクラスを持っています。この時点で
$ java -cp jartest-0.0.1.jar:jartest2-0.0.1.jar jartest.Main
Loaded:jartest2.EndPoint
となります。これを jartest2 のほうを classpath に指定しなくても java -jar jartest-0.0.1.jar で起動できるようにしたいというのが今回の目的なわけです。
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <configuration>
        <archive>
          <manifest>
            <mainClass>jartest.Main</mainClass>
          </manifest>
          <manifestEntries>
            <Class-Path>jartest2-0.0.1.jar</Class-Path>
          </manifestEntries>
        </archive>
      </configuration>
    </plugin>
  </plugins>
</build>
このように pom.xml を変更すればいけるはずです。実際はこちらの jar は他にも依存してるライブラリがあって、ただそちらは maven で取得できるライブラリなので、dependencies に記述し、よくある方法にて maven-assembly-plugin のほうに上のような記述をします。さていざ実行。
$ java -jar jartest-0.0.1-jar-with-dependencies.jar 
Exception in thread "main" java.lang.ClassNotFoundException: jartest2.EndPoint
 at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
 at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
 at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
 at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
 at java.lang.Class.forName0(Native Method)
 at java.lang.Class.forName(Class.java:264)
 at jartest.Main.main(Main.java:5)
!?
とりあえず jartest.Main.main() までは来ているので MANIFEST.MF が全くダメなわけではないということはわかります。中身を見てみると、
$ tail -3 META-INF/MANIFEST.MF 
Main-Class: jartest.Main
Class-Path: jartest2-0.0.1.jar
と、正しく指定されているように見えます。

正直相当はまりました。しょうもないtypoしてるのかと思ってコピペでクラス名を書いてみたり、ファイル名もコピペしてみたり。はたまた開発環境に使っているMac特有の問題なのじゃないかと思ってWindows機でテストしてみたり・・・。

そしていろいろやった後も解決せず、jar を展開してぼーっと眺めていたら
$ ls META-INF/
INDEX.LIST    LICENSE.txt    MANIFEST.MF    maven/        services/
ん?MANIFEST.MF以外にもいろいろあるな?まさかな、と思いつつ MANIFEST.MF だけにしてみたところ・・・
$ java -jar jartest-0.0.1-jar-with-dependencies.jar 
Loaded:jartest2.EndPoint
おおお!動きましたね!

結論から言うとこの INDEX.LIST が悪さをしていたようです。そもそもこれは何かというのを知らなかったですが、Jarファイルの仕様にしっかり書かれています。でも今までこんなファイルを作るなんて指定はどこにもしてなかったけど・・・?原因はこれでした。
    <dependency>
      <groupId>io.undertow</groupId>
      <artifactId>undertow-core</artifactId>
      <version>1.0.16.Final</version>
    </dependency>
私が作っていた java application は embedded web server を実装するのに undertow を使っていました。そしてそれを dependency にして jar-with-dependency で1つの jar にしていたわけですが、もともと undertow-core 用に書かれた INDEX.LIST が jar-with-dependency によって1つの jar に収められたことにより生じた問題だったようです。いやはやなんとも難しい・・・。

というわけで解決方法としては INDEX.LIST ファイルを jar に含めない、またはちゃんと INDEX.LIST ファイルを生成する、のどちらかだと思うのですが、undertow が使っている INDEX.LIST の内容は特になくても問題ない(ように思える)ので、含めない方法をとることにしました。maven-assembly-plugin の jar-with-dependency の指定のところを外部の assembly.xml に記述することとし、そこに以下のような記述を行います。
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
  <id>jar-with-dependencies</id>
  <formats>
    <format>jar</format>
  </formats>
  <includeBaseDirectory>false</includeBaseDirectory>
  <dependencySets>
    <dependencySet>
      <outputDirectory>/</outputDirectory>
      <useProjectArtifact>true</useProjectArtifact>
      <unpack>true</unpack>
      <scope>runtime</scope>
      <unpackOptions>
        <excludes>
          <exclude>META-INF/INDEX.LIST</exclude>
        </excludes>
      </unpackOptions>
    </dependencySet>
  </dependencySets>
</assembly>
これで無事 INDEX.LIST を含まずに dependency を含んだ jar が生成されました。めでたしめでたし。