JavaでXMLを処理しようと思えば、やっぱJAXBですよね。便利ですもんね。
ですが、このJAXBがJava9で非推奨になり、早くもJava11で完全削除されることになりました。早くねぇ?1年しかないんですけどぉ?
けっこう呑気していたnodamushiも一瞬巨大に見える程のOracle圧力にはビビった!!
というわけで、太った 重い腰をいい加減あげて、脱JREのJAXBをしてみました。
EclipseLink MOXy
脱JREのJAXBと言いましたが、何も脱JAXBをする訳ではありません。外部実装に移行します。
その選択肢の一つがEclipseLink MOXyです。(選択肢の一つと言いましたが、他の選択肢があるのか知りません。)
MOXyはXMLだけじゃなくて、JSONのバインディングにも対応しているそうです。
また、XSD→Javaに変換するコンパイラもちゃんとあります。(jaxb-compiler)
今の所、xjcはJDKに付属していますが、JDK11で無くなると思うので、EclipseLink Installer Zipをダウンロードして、適当な場所に展開し、binディレクトリにパスを通してください。
(展開するだけな気がするんだけど、Installerってどういうことなんだろ?)
XML Schema をJavaクラスに変換する
試しに、XML Schemaファイルperson.xsdを作成し、Javaクラスに変換してみます。構造は以下の図の様になっています。
<?xml version="1.0"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <xsd:element name="persons"> <xsd:complexType> <xsd:sequence> <xsd:element ref="person" minOccurs="0" maxOccurs="unbounded"/> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="person"> <xsd:complexType> <xsd:sequence> <xsd:element name="name" type="xsd:string" minOccurs="1" maxOccurs="1"/> <xsd:element name="age" type="xsd:unsignedInt" minOccurs="0" maxOccurs="1"/> <xsd:element name="sex" type="sexType" minOccurs="0" maxOccurs="1"/> </xsd:sequence> <xsd:attribute name="id" type="xsd:string" use="optional"/> </xsd:complexType> </xsd:element> <xsd:simpleType name="sexType"> <xsd:restriction base="xsd:string"> <xsd:enumeration value="female"/> <xsd:enumeration value="male"/> </xsd:restriction> </xsd:simpleType> </xsd:schema>
上記のperson.xsdをJavaに変換します。
xjcとの差を確かめる為に両方で変換してみました。(person.xsdはsrc/main/xsdディレクトリに配置しています)
xjc -no-header -encoding utf8 -d src/main/java -p test.xjc src/main/xsd/person.xsd jaxb-compiler -no-header -encoding utf8 -d src/main/java -p test.eclipselink src/main/xsd/person.xsd
生成されたファイルのdiffをとり、違いを見てみました。
xjc | EclipseLink | diff |
---|---|---|
test/xjc/ObjectFactory.java | test/eclipselink/ObjectFactory.java | packageの名前が違うのみ |
test/xjc/Persons.java | test/eclipselink/Persons.java | packageの行のみ違う |
test/xjc/Person.java | test/eclipselink/Person.java | packageの行のみ違う |
test/xjc/SexType.java | test/eclipselink/SexType.java | packageの行のみ違う |
test/eclipselink/jaxb.properties | xjcでは生成されない |
上記の結果を見る限り、生成結果は全く同じなようです。安心してそのまま移行出来ますね。
なお、EclipseLinkのほうでは生成されたjaxb.propertiesファイル(中身は以下の一行)は、XMLを読み込み、各クラスのデータにバインドする処理を行うクラスをどこから取得するのかという設定ファイルになります。
javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory
JDK11からはJAXBが同梱されない為、この設定ファイルは必須になります。
MOXyをプログラムで使ってみる
上記で生成したpersonクラスを実際にXMLを読み出して、バインドしてみます。
環境は以下です。
- JDK:Java10
- ビルドツール:Gradle
まずは、build.gradleのdependenciesに以下の二行を追加します。versionは適宜変更してください。
compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0' compile group: 'org.eclipse.persistence', name: 'org.eclipse.persistence.moxy', version: '2.7.1'
gradle initで生成されたAppクラスを以下の様に実装。
長々と書いていますが、やってることは単純。
- XMLを読み込んで、xjcとjaxb-compilerで生成したクラスにバインド
- test.xjc.Personとtest.eclipselink.Personでは、同じクラス名で使いにくいので、App内で定義したPersonクラスに変換
- System.out.printlnで出力
- エラーがあった場合はFailとだけ表示
import java.io.File; import java.lang.reflect.Method; import java.util.List; import java.util.stream.Collectors; import javax.xml.bind.JAXB; public class App { public static void main(String[] args) { File xml = new File("./src/test/resources/test.xml"); try { System.out.println("-------XJC------------------"); loadAndPrint(xml,test.xjc.Persons.class); }catch(Exception e) { System.out.println("Fail"); } try { System.out.println("--------EclipseLink---------"); loadAndPrint(xml,test.eclipselink.Persons.class); }catch(Exception e) { System.out.println("Fail"); } } public static void loadAndPrint(File xml,Class<? extends Object> clz)throws Exception{ Object persons = JAXB.unmarshal(xml, clz); List<Person> person = toPersonList(persons); person.forEach(System.out::println); } public static class Person { public String name,sex,id; public Long age; public Person(String name, Long age, Object sex, String id){ this.name = name; this.age = age;this.id = id; this.sex = sex==null?null:sex.toString(); } @Override public String toString(){ return String.format("person:[id=%s],name=%s,age=%d,sex=%s", id,name,age,sex); } } @SuppressWarnings("unchecked") private static List<Person> toPersonList(Object persons) { return ((List<Object>)get(persons,"person",List.class)).stream() .map(p->new Person( get(p,"name",String.class), get(p,"age",Long.class), get(p,"sex",Object.class), get(p,"id",String.class))) .collect(Collectors.toList()); } private static String firstUpperCase(String str) { if(str.length()==1)return str.toUpperCase(); return new StringBuilder(str.length()) .append(Character.toUpperCase(str.charAt(0))) .append(str,1,str.length()) .toString(); } private static <T> T get(Object element,String childName,Class<T> clz){ try { Class<? extends Object> c = element.getClass(); String mName = "get"+firstUpperCase(childName); Method m = c.getMethod(mName); m.setAccessible(true); Object o = m.invoke(element); return clz.cast(o); }catch(Exception e) { throw new RuntimeException(e); } } }
10行目で直接指定しているテスト用のXMLファイルは以下の様な内容です。
<persons> <person id="1"> <name>nodamushi</name> <age>19</age> <sex>male</sex> </person> <person> <name>Aoki Kei</name> <age>30</age> </person> <person> <name>Koike Yuka</name> <sex>femail</sex><!-- typo--> </person> </persons>
で、ビルドして実行すると以下の様になりました。
-------XJC------------------ Fail --------EclipseLink--------- person:[id=1],name=nodamushi,age=19,sex=MALE person:[id=null],name=Aoki Kei,age=30,sex=null person:[id=null],name=Koike Yuka,age=null,sex=null
XJC側は失敗していますが、EclipseLink MOXyの方は正常に読み込めていますね。
(EclipseLinkだけ読み込めたのはtest.eclipselink.jaxb.propertiesがあるからです。typo処理の違いではありません。なお、Java8で実行すると、両方ちゃんと読み込めます。)
まとめ
- EclipseLink Installerをダウンロードし、binディレクトリにパスを通す。(jaxb-compilerが使える様になる)
- GradleやMavenの設定ファイルにjaxb-apiとEclipseLink MOXyをdependencyに追加する
というわけで、あまり苦労することなく、EclipseLink MOXyに移行出来そうです。
が、依存含めて外部jarファイルが5Mbもあるのはいかがなもんでしょうかね………。