プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

JAXBをEclipseLink MOXyに移行する

 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ってどういうことなんだろ?)

f:id:nodamushi:20180401171240p:plain:w320

XML Schema をJavaクラスに変換する


 試しに、XML Schemaファイルperson.xsdを作成し、Javaクラスに変換してみます。構造は以下の図の様になっています。

f:id:nodamushi:20180401173218p:plain

<?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クラスを以下の様に実装。
 長々と書いていますが、やってることは単純。

  1. XMLを読み込んで、xjcとjaxb-compilerで生成したクラスにバインド
  2. test.xjc.Personとtest.eclipselink.Personでは、同じクラス名で使いにくいので、App内で定義したPersonクラスに変換
  3. System.out.printlnで出力
  4. エラーがあった場合は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-apiEclipseLink MOXyをdependencyに追加する


 というわけで、あまり苦労することなく、EclipseLink MOXyに移行出来そうです。

 が、依存含めて外部jarファイルが5Mbもあるのはいかがなもんでしょうかね………。