CLOVER🍀

That was when it all began.

JAXBをXML Schemaなしで使ってみる

最近、仕事でXMLとかJSONをデータの入出力形式とする開発をすることになったのですが、ここでどのライブラリとか使おうかなぁといろいろ思考中。

JSONはJSONICでいこうと思っているのですが(Seasar2系使ってるし…)、XMLはどうしようかなと…。XStreamという手もあるのですが、XStreamには名前空間のサポートが無い(今回、これはマズいのです…)ので除外。

そこで、JAXBにちょっと注目しています。

世間の情報を見ていると、XML SchemaをコンパイルしてJavaコードを生成〜みたいな内容が多いですが、XML Schemaはなくても使えるので、標準で使える簡単なオブジェクト-XMLマッピングライブラリとして覚えておくのも有りだなぁと思うのです。特に、XML Schemaでの検証なんていいから、とりあえず簡単に使いたいなんて時には。

では、ちょっと使ってみましょう。

サンプルとして、こういうXMLをターゲットにすることを考えてみます。

<?xml version="1.0" encoding="UTF-8"?>
<data>
  <title>名簿</title>
  <persons>
    <person id="1">
      <firstName>Taro</firstName>
      <lastName>Tanaka</lastName>
      <age>17</age>
    </person>
    <person id="2">
      <firstName>Hanako</firstName>
      <lastName>Suzuki</lastName>
      <age>15</age>
    </person>
  </persons>
</data>

これを、Javaのクラス的には以下のようにマッピングしてみましょう。

Data(1) - Person(n)

Dataクラス1インスタンスにつき、N個のPersonクラスのインスタンスが保持されるって感じで。

これを行うために、Data.javaとPerson.javaを用意します。
まずはData.java

package jaxb.entity;

import java.util.List;

import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "data")
public class Data {
    @XmlElement(name = "title") // 省略可
    private String title;

    @XmlElementWrapper(name = "persons")
    @XmlElement(name = "person")
    private List<Person> persons;

    public String getTitle() {
        return title;
    }

    public List<Person> getPersions() {
        return persons;
    }

    @Override
    public String toString() {
        return String.format("title = [%s], persons = %s", title, persons);
    }
}

@XmlAccessorTypeでフィールドをバインド対象に、@XmlRootElementアノテーションでdataタグをこのクラスとのマッピングを行うように指示します。

@XmlAccessorTypeアノテーションでは他にもバインドの方法が指定できて、以下のようになっています。

XmlAccessType 意味
NONE フィールドやプロパティはXMLにバインドされない
FIELD @XmlTransientアノテーションが付かない限り、staticでないフィールドすべてがXMLにバインドされる
PROPERTY @XmlTransientアノテーションが付かない限り、すべてのgetter/setterのペアがXMLにバインドされる
PUBLIC_MEMBER @XmlTransientアノテーションが付かない限り、すべてのgetter/setterのペアとpublicフィールドがXMLにバインドされる

また、@XmlElementではフィールドをXMLのタグ(要素)にバインドするように指示しています。なお、この例のようにタグ名とフィールド名が同じ場合は、省略しても問題ありません。

@XmlElementWrapperアノテーションは、ラッパー要素となり、繰り返しなどの親タグを指定します。ここでは、personsタグが親となり、その中での繰り返しとしてpersonタグを使用するので、そのようにアノテーションを付与しています。

続いて、Person.java

package jaxb.entity;

import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
public class Person {
    @XmlAttribute(name = "id")
    private int id;

    @XmlElement(name = "firstName") // 省略可
    private String firstName;
    @XmlElement(name = "lastName") // 省略可
    private String lastName;
    @XmlElement(name = "age") // 省略可
    private int age;

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return String.format("!!id = [%d], firstName = [%s], lastName = [%s], age = [%d]!!", id, firstName, lastName, age);
    }
}

@XmlAttributeは、属性とバインドするためのアノテーションです。こちらは、@XmlElementと異なり省略できません。まあ、属性ってことはフィールド定義だけからはわかりませんからね…。

では、上記のクラスとXMLのマーシャル/アンマーシャルを行うクラスを書いてみます。
JaxbSupport.java

package jaxb;

import java.io.StringReader;
import java.io.StringWriter;

import javax.xml.bind.DataBindingException;
import javax.xml.bind.JAXB;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class JaxbSupport {
    public static <T> T unmarshalSimply(String inputString, Class<T> requiredType) throws DataBindingException {
        return JAXB.unmarshal(new StringReader(inputString), requiredType);
    }

    public static <T> T unmarshal(String inputString, Class<T> requiredType) throws JAXBException {
        JAXBContext context = JAXBContext.newInstance(requiredType);
        Unmarshaller unmarshaller = context.createUnmarshaller();
        return requiredType.cast(unmarshaller.unmarshal(new StringReader(inputString)));
    }

    public static String marshalSimply(Object target) throws DataBindingException {
        StringWriter writer = new StringWriter();
        JAXB.marshal(target, writer);
        return writer.toString();
    }

    public static String marshal(Object target) throws JAXBException {
        StringWriter writer = new StringWriter();
        JAXBContext context = JAXBContext.newInstance(target.getClass());
        Marshaller marshaller = context.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(target, writer);
        return writer.toString();
    }
}

ちなみに、XML→Javaオブジェクトの方向がアンマーシャルで、Javaオブジェクト→XMLの方向がマーシャルです。

簡単に使うだけなら、JAXBクラスを使用すると事足りたりします。

アンマーシャルは

JAXB.unmarshal(new StringReader(inputString), requiredType);

で、JAXB#unmarshalの第1引数に入力するXMLを、第2引数に結果型を指定します。第1引数はFileやXMLのパスなどいろいろ指定できるので、詳細はAPIドキュメントをご覧ください。

マーシャルの場合は

        JAXB.marshal(target, writer);

この1行に集約されます。第1引数がXML変換対象のJavaオブジェクトで、第2引数にXMLの出力先を指定します。ここではWriterを渡していますが、unmarshal同様、他にもいくつか指定できるので、こちらも詳細はAPIドキュメントをご覧ください。

なお、これらのメソッドは処理の失敗時にはjavax.xml.bind.DataBindingException(RuntimeExceptionのサブクラスです)がスローされるので適宜捕捉してください。

では、使ってみましょう。XMLを用意するのが面倒だったので、エントリポイントはScalaで書いて生文字リテラルを借りました…。

JaxbStartup.scala 
package jaxb

import javax.xml.bind.{DataBindingException, JAXBException}

import jaxb.entity.Data

object JaxbStartup {
  def main(args: Array[String]): Unit = {
    val inputXml = """<?xml version="1.0" encoding="UTF-8"?>
                      |<data>
                      |  <title>名簿</title>
                      |  <persons>
                      |    <person id="1">
                      |      <firstName>Taro</firstName>
                      |      <lastName>Tanaka</lastName>
                      |      <age>17</age>
                      |    </person>
                      |    <person id="2">
                      |      <firstName>Hanako</firstName>
                      |      <lastName>Suzuki</lastName>
                      |      <age>15</age>
                      |    </person>
                      |  </persons>
                      |</data>""".stripMargin

    try {
      val data = JaxbSupport.unmarshalSimply(inputXml, classOf[Data])
      printf("Result = [%s]%n", data)

      printf("Marshal XML => %n%s%n", JaxbSupport.marshalSimply(data))
    } catch {
      case e: DataBindingException =>
        // パースエラーなどの場合
        printf("Fail Reason => [%s]%n", e.getMessage)
    }
  }
}

実行結果は、こんな感じ。
XML→Java

Result = [title = [名簿], persons = [!!id = [1], firstName = [Taro], lastName = [Tanaka], age = [17]!!, !!id = [2], firstName = [Hanako], lastName = [Suzuki], age = [15]!!]]

Java→XML

Marshal XML => 
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<data>
    <title>名簿</title>
    <persons>
        <person id="1">
            <firstName>Taro</firstName>
            <lastName>Tanaka</lastName>
            <age>17</age>
        </person>
        <person id="2">
            <firstName>Hanako</firstName>
            <lastName>Suzuki</lastName>
            <age>15</age>
        </person>
    </persons>
</data>

XML変換結果の

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

のstandalone="yes"が邪魔なのですが、これはちょっと外れない感じですね…。XML宣言ごとなら外せるのですが…。

なお、このサンプルでは使いませんでしたが、マーシャル/アンマーシャルの処理をカスタマイズしたい場合は、MarshallerとUnmarshallerを使います。
上記コードだと、こちらがアンマーシャルで

    public static <T> T unmarshal(String inputString, Class<T> requiredType) throws JAXBException {
        JAXBContext context = JAXBContext.newInstance(requiredType);
        Unmarshaller unmarshaller = context.createUnmarshaller();
        return requiredType.cast(unmarshaller.unmarshal(new StringReader(inputString)));
    }

こちらがマーシャルですね。

    public static String marshal(Object target) throws JAXBException {
        StringWriter writer = new StringWriter();
        JAXBContext context = JAXBContext.newInstance(target.getClass());
        Marshaller marshaller = context.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(target, writer);
        return writer.toString();
    }

それぞれ、一度JAXBContextを作成してからUnmarshaller/Marshallerインターフェースのインスタンスを作成します。それぞれの処理は、ここで作成したUnmarshaller/Marshallerを使用することになりますが、その際にsetPropertyメソッドなどでインスタンスの設定を変更することが可能です。

なお、これらの処理中にスローされる例外は、javax.xml.bind.JAXBExceptionとなっており、こちらはチェック例外ですね。

アノテーションの配置の都合上、XMLの表現形式がJAXBで解釈できるものに縛られる可能性がありますが、標準で使える分には割と便利な気がします。