2018/08/17

Java 11 + JAXBのちょっとしたピットフォール

このエントリーをはてなブックマークに追加

前回はJigsawでSerciveLoaderクラスを使用した時の挙動について紹介しました。

今回はその続きのようなもの。

Java SE 11からJAXBが外されてしまうのは、このブログでも何度も書いてます。

なので、JAXBを使うのであれば、GitHubのJAXBのページからダウンロードするか、MavenのCentral Repositoryを使うことになります。

JAXBは5つのJARファイルから構成されています。

APIはjaxb-api.jar、ランタイムがjaxb-impl.jarとjaxb-core.jarです。他の2つはXMLスキーマとJavaのクラスの変換を行うツールxjcとjxcのJARファイルです。

xjcなどを使わないのであれば、jaxb-api.jar、jaxb-impl.jar、jaxb-core.jarの3つだけを使用します。

また、JAXBはJavaBeans Activation Framework (JAF)も使用するので、こちらもダウンロードしておきます。ただ、GitHubのJAFのプロジェクトではJARファイルの配布はしていないので、Maven Central RepositryにあるJAFを利用します。

 

ここでは、簡単なサンプルとして、次に示すJAXBDemoクラスを作りました。

package net.javainthebox;

import java.io.File;
import javax.xml.bind.JAXB;
import javax.xml.bind.JAXBException;

import net.javainthebox.xml.Name;

public class JAXBDemo {
    public static void main(String... args) throws JAXBException {
        File file = new File("name.xml");
        Name sakuraba = JAXB.unmarshal(file, Name.class);

        System.out.println(sakuraba.getFirst() + " " + sakuraba.getLast());
    }
}

name.xmlファイルを読み込んでNameオブジェクトに変換するプログラムです。

Nameクラスはfirstとlastという2つの文字列のフィールドを持っているクラスです。

module-info.javaを次に示します。

module net.javainthebox.xml {
    requires java.xml.bind;
    opens net.javainthebox.xml;
}

requires文で指定しているjava.xml.bindモジュールがjaxb-api.jarファイルです。

opensでnet.javainthebox.xmlパッケージを指定しているのは、JAXBがリフレクションでNameクラスにアクセスするためです。

次にjava.xml.bindモジュールの依存性を調べてみましょう。これにはjarコマンドの--describe-module (もしくは省略形の-d) オプションで調べることができます。

C:\jaxb\mod>jar -d -f jaxb-api-2.3.0.jar
java.xml.bind jar:file:///C:/jaxb/mod/jaxb-api-2.3.0.jar/!module-info.class
exports javax.xml.bind
exports javax.xml.bind.annotation
exports javax.xml.bind.annotation.adapters
exports javax.xml.bind.attachment
exports javax.xml.bind.helpers
exports javax.xml.bind.util
requires java.activation transitive
requires java.base mandated
requires java.desktop
requires java.logging
requires java.xml transitive
uses javax.xml.bind.JAXBContextFactory

一番上の行がモジュール名とモジュラーJARファイルの場所を示しています。

はじめのrequires文で指定しているjava.activationがJAFのモジュールです。

ただし、JAFのjavax.activation-api-1.2.0.jarファイルはAUTOMATIC-MODULE-NAMEは記載されているものの、モジュールにはなっていません。このため、自動モジュールとして扱います。

他のrequires文はJava SEの標準なので、特に何もしなくても大丈夫です。

最後のuses文が前回も登場したServiceLoaderクラスを使用したSPIでロードするインタフェースです。そして、この実装クラスがあるのが、jaxb-impl.jarファイルです。(本当に使用するインタフェースと実装クラスは違うようなのですが、ここでは深入りしません)

jaxb-impl.jarファイルはモジュラーJARではないので、前回説明したようにクラスパスでもモジュールパスで指定してもどちらでも大丈夫です。

まずクラスパスで指定して実行してみましょう。

ここではmodディレクトリにnet.javainthebox.xmlモジュールのjaxbdemo.jarファイル、java.xml.bindモジュールのjaxb-api.jarファイル、JAFのjavax.activation-api-1.2.0.jarファイルを配置してあります。

クラスパスで指定するjaxb-impl.jarファイルとjaxb-core.jarファイルはlibファイルに置きます。

C:\jaxb>java -p mod -cp lib\jaxb-impl.jar;lib\jaxb-core.jar -m net.javainthebox.xml/net.javainthebox.JAXBDemo
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.sun.xml.bind.v2.runtime.reflect.opt.Injector (file:/C:/home/yuichi/Web/java/diary/material/201808/20180817jaxb/lib/jaxb-impl.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int)
WARNING: Please consider reporting this to the maintainers of com.sun.xml.bind.v2.runtime.reflect.opt.Injector
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
Yuichi Sakuraba

C:\jaxb>

実行することができました。

警告が出ているのはJAXBが内部でsun.misc.Unsafeクラスを使用しているためです。

次にモジュールパスで指定して、実行してみます。

jaxb-impl.jarファイルはmodディレクトリに移動してあります。

C:\jaxb>java -p mod -cp lib\jaxb-core.jar -m net.javainthebox.xml/net.javainthebox.JAXBDemo
Exception in thread "main" java.lang.NoClassDefFoundError: com/sun/xml/bind/v2/model/annotation/AnnotationReader
        at java.base/java.lang.ClassLoader.defineClass1(Native Method)
        at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1016)
        at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1095)
        at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:206)
        at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:760)
        at java.base/jdk.internal.loader.BuiltinClassLoader.findClassInModuleOrNull(BuiltinClassLoader.java:681)
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:606)
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
        at java.base/java.lang.Class.getDeclaredMethods0(Native Method)
        at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3167)
        at java.base/java.lang.Class.getMethodsRecursive(Class.java:3308)
        at java.base/java.lang.Class.getMethod0(Class.java:3294)
        at java.base/java.lang.Class.getMethod(Class.java:2107)
        at java.xml.bind/javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:295)
        at java.xml.bind/javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:286)
        at java.xml.bind/javax.xml.bind.ContextFinder.find(ContextFinder.java:409)
        at java.xml.bind/javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:721)
        at java.xml.bind/javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:662)
        at java.xml.bind/javax.xml.bind.JAXB$Cache.<init>(JAXB.java:127)
        at java.xml.bind/javax.xml.bind.JAXB.getContext(JAXB.java:154)
        at java.xml.bind/javax.xml.bind.JAXB.unmarshal(JAXB.java:168)
        at net.javainthebox.xml/net.javainthebox.JAXBDemo.main(JAXBDemo.java:12)
Caused by: java.lang.ClassNotFoundException: com.sun.xml.bind.v2.model.annotation.AnnotationReader
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
        ... 24 more

C:\jaxb>

AnnotationReaderクラスがないという例外が発生してしまいました!

なぜ、クラスパスだと正常に実行できて、モジュールパスだと実行できないのでしょう?

 

答えはjaxb-impl.jarファイルをモジュールとして扱うか、普通のJARファイル(無名モジュール)として扱うかというところにあります。

ここでロードできないcom.sun.xml.bind.v2.model.annotation.AnnotationReaderクラスはjaxb-core.jarファイルに含まれています。

一方、jaxb-impl.jarファイルにもcom.sun.xml.bind.v2.model.annotationパッケージが含まれているのです。

Project Jigsawでは同じパッケージを複数のモジュールで定義することはできません。1つのパッケージは1つのモジュールで定義します。

jaxb-impl.jarファイルをモジュールパスに配置してしまうと自動モジュールとして扱うので、このパッケージに関する制限に引っかかってしまうのです。このため、jaxb-core.jarファイルのクラスはロードされなかったのです。

しかし、クラスパスで指定すればモジュールではないので、パッケージが複数のJARファイルに分かれていても問題ないわけです。

JAXBがjaxb-impl.jarファイルとjaxb-core.jarファイルに分かれていなければ、なんの問題もないのですが...

既存のライブラリでも同じように同じパッケージを複数のJARファイルに含んでいる場合があるかもしれません。クラスをロードできるはずなのに、ClassNotFoundException例外が発生するような場合は、パッケージが複数のJARに分かれている可能性が高いですね。

こういう問題は、ライブラリがちゃんとメンテされていれば、時間が解決してくれるとは思います。

それにしても、こういうことがあるので、ライブラリはともかく、アプリケーションであれば今すぐモジュール化する必要はないのではないでしょうか。

0 件のコメント: