Java/JAXB

Last-modified: 2015-06-14 (日) 17:04:08

基本

次のサイトで使い方の基本はひととおりわかる

JAXB使い方メモ

考え方

(たぶん)

XML Schema     <--->   Java クラス
   ↓                      ↓
   ↓                      ↓
 XML文書       <--->   オブジェクトツリー

XML Schema は、

  • elementの内容(属性含む)を型として定義
    • 型は名前付きの場合も、名前なしの場合もある
  • elementの出現順や名前を定義し、elementに型を割り当て
    • 名前付きの型は、複数のelementに割り当てることができる。

elementの名前は型と独立であるに注意。

このため、XMLにおいてelementの名前が直接的にその内容(型)を規定している、elementは型の名前である、という発想で考えている場合には、JAXBをどう使うのか(Java側をどう記述したらよいのか)わからなくなってくる。

JAXBは、

  • XML Schemaの型をJavaクラスにマッピング
  • elementや属性を、Javaのプロパティ(またはインスタンス変数)にマッピング
  • element - 型 の対応関係は、プロパティ-そのクラス の対応関係にマッピング

例えば、以下のクラス定義を考えると、

 public class Feed {
    @XmlElement
    private String title;

    @XmlElement
    private Person author;

    @XmlElement
    private Person editor;
 }

対応するXMLは、

 <????>
   <title>テキスト</title>
   <author>
     Personクラスで定義される内容
   </author>
   <editor>
     Personクラスで定義される内容
   </editor>
 </????>
  • Feedクラスにより、内容の構成は定義される。Feedに対応するelementの名前は決まらない。elementは、Feedを使う側によって決まる。
  • 同様に、author elementとeditor elementの内容はPersonクラスによって定義されるが、elementの名前は、Personを利用している側、つまりFeedクラス側によって(この場合はインスタンス変数名authro, editorによって)決まる。

@XmlRootElement

@XmlRootElementは、クラスで定義した(XMLの)型にelementの名前を対応付ける。

 @XmlRootElement
 public class Feed {
   ...
 }

この例では、Feed型にfeedというelementが対応付けられる。@XmlRootElementにname= を指定すれば、クラス名とは別の名前のelementを対応付けられる。

この情報は、型からelement名を求めなければならない状況で利用されるようだ。たとえば、FeedクラスをXMLに変換するときは、@XmlRootElementがなければ、XMLのルートelementの名前が決まらない。

フィールドとプロパティのどちらをelementやattributeにマッピングするか

@XmlElementや@XmlAttributeはフィールド(インスタンス変数)とプロパティのどちらにつけてもよい。
プロパティの場合はgetterにつける(setterでもよいのか?)

デフォルトのルールの選択

これらのアノテーションをつけなくても、デフォルトのルールを適用して、自動でマッピングすることもできる。

デフォルトのルールは@XmlAccessTypeで、クラスごと、または、パッケージごとに選択する。

パッケージに対して指定する場合は、パッケージのディレクトリにpackage-info.javaを作成し、その中で指定する。

@XmlAccessorType(XmlAccessType.NONE) // Beanのプロパティは、明示的に指定された場合のみXMLにマッピングする
package my.sample;

JAXBContext.newInstance(Class...)

この引数にどのクラスを渡せばよいのか。

MOXyのマニュアルから理解したところでは

  • XMLの型と対応付けられるすべての型をJAXBに認識させる必要がある
  • newInstanceにクラスを渡すと、次のものはJAXBがたどって自動的に認識する
    • そのスーパークラス
    • そのクラスから参照しているクラス

このため、基本的にはXMLのルートelementに対応するクラスを渡せばよさそう。

複数のクラスを渡す必要があるのは、たとえば、

  • ルートelementが何種類かある場合
  • ルートから再帰的に参照されているクラスの、子孫クラスもJAXBに認識させる必要がある場合。たとえば、次のクラスが該当する。
    • @XmlElementRefを使ってクラスを切り替える場合の、切り替え先のクラス。
    • MOXy独自の機能で @XmlDiscriminatorNode / @XmlDiscriminatorValue を使ってクラスを切り替える場合の、切り替え先のクラス。

サブクラスを切り替えて使いたい場合

elementの名前で切り替える

@XmlElementsを使う

@XmlElementsの中で@XmlElementを使って、element名とJavaクラスの対応を明示的に指定する。

public class Feed {
    @XmlElements(
        value={
            @XmlElement(name="tw", type=Twitter.class),
            @XmlElement(name="fb", type=Facebook.class)
        }
    )
    private Sns sns;
}
public class Twitter extends Sns {
    @XmlElement
    private String username;
}
public class Facebook extends Sns {
    @XmlElement
    private String realname;
}

この定義により、以下のどちらのXMLも解析できる。Feed.snsには、elementがtwかfbかで代入されるクラスが異なる。

<feed>
    <tw>
        <username>@twittername</username>
    </tw>
</feed>
<feed>
    <fb>
        <realname>foo bar</realname>
    </fb>
</feed>

@XmlElementRefを使う

@XmlElementsの場合は、element名 - 型 の対応関係を、その型を参照する側で定義した。@XmlElementRefの場合は、参照される側で定義する。

その点を除けば、@XmlElementsと同じ (@XmlElementsとの機能的な差異はあるのだろうか??)

public class Feed {
    @XmlElementRef
    private Contact contact;
}
@XmlRootElement
public class Phone extends Contact {
    @XmlElement
    private String number;
}
@XmlRootElement
public class Mail extends Contact {
    @XmlElement
    private String address;
}

Phone, Mailクラスに@XmlRootElementをつけることで、element名を対応付けている。@XmlRootElementにname=を指定すれば、クラス名(phone, mail)とは別のelement名を対応付けることができる(はず)。

この方法の場合は、JAXBContext.newInstanceを実行する際、引数に Mail.class, Phone.classも渡す必要がある。

attributeの値で切り替える

以下が詳しい。

JAXBニッチ技特集 XMLを属性に基づいて特定のサブクラスに非整列化(unmarshal)する

標準のJAXB 2の範囲では実現できず、EclipseLink MOXy の独自機能を利用する。

例として、mediaというelementのtype attributeの値によって、クラスを切り替えたいとする。

XMLの例

<feed>
    <media type="book">
        <publisher>foo出版</publisher>
    </media>
</feed>

Java側の記述

public class Feed {
    @XmlElement
    private Media media;
}
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorNode;

@XmlDiscriminatorNode("@type")
class Media {

}
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;

@XmlDiscriminatorValue("book")
public class Book extends Media {
    @XmlElement
    private String publisher;
}
@XmlDiscriminatorValue("web")
public class Web extends Media {
    @XmlElement
    private String url;
}
  • @XmlDiscriminatorNode で、どの値を参照するか、を指定する。XPathで指定する。@typeは、XPathによる表記で、typeというattributeを指す。
  • @XmlDiscriminatorValue で、値を指定する。参照した値がこの値に一致した場合に、このクラスが利用される。

先の例のXMLを解析すると、Feedクラスのmediaには、Bookのオブジェクトが代入される。

なお、JAXBContext.newInstanceの実行時は、引数に Book.classとWeb.classも渡す必要がある。

名前空間も利用する場合

@XmlDiscriminatorNode で指定するXPathに、名前空間付きのアトリビュートを指定したい場合は、
名前空間のprefixを@XmlSchemaであらかじめ定義しておき、XPath中ではそのprefixを使って名前空間を指定する。

例えば、XML側が <media foo:type="book"> となっており、my.sample.Mediaクラスに @XmlDiscriminatorNode("@foo:type") をつけたとする。また、fooの名前空間は、"http://foo.bar/baz" とする。この場合、my.sampleパッケージのpackage-info.javaは、

@XmlSchema(
        xmlns = {
            @XmlNs(prefix = "foo", namespaceURI = "http://foo.bar/baz")
        }
)
package my.sample;

名前空間

以下のサイトを参照。

JAXB使い方メモ

XmlAdapter

JAXBでXMLとJavaを直接マッピングできない場合は、マッピング可能なJavaを経由してマッピングさせる。

XML <--> JAXBでマッピング可能なJavaクラス <-- XmlAdapter --> 希望のJavaクラス

このため、希望のJavaクラスのほかに、「JAXBでマッピング可能なJavaクラス」も作成が必要となる場合がある。「JAXBでマッピング可能なJavaクラス」がStringなどの場合は、作成は不要。

以下は、XML側の "on" , "off" という文字列を、Java側のbooleanとマッピングさせる例である。ここではunmarshalの処理だけ用意している。

public class OnOffAdapter extends XmlAdapter<String, Boolean>{
    @Override
    public Boolean unmarshal(String v) throws Exception {
        return "on".equalsIgnoreCase(v); // "on"以外はすべて"off"とみなす。
    }

    @Override
    public String marshal(Boolean v) throws Exception {
        throw new UnsupportedOperationException("Not supported yet.");
    }
}
public class Feed {
    @XmlElement
    @XmlJavaTypeAdapter(OnOffAdapter.class)
    private boolean available;
}

JAXBは element "available" の型として、String(に対応するXMLの)型を割り当てて解析を行うと思われる。JAXBは変数availableに割り当てられたXmlAdapterのValueTypeを取得し (XmlAdapter<String, Boolean>のStringがValueType) それを element "available"の型として扱うだろう。

XML側が"on"/"off"のような単純な文字列ではなく、子要素を持つような複合型の場合は、この例のStringの部分がFooBeanのようなBeanになるだろう。その場合、FooBeanは、JAXBの範疇でXMLとマッピング可能な型でなければならない。FooBeanから、希望する型(この例でのboolean)への変換方法は、XmlAdapterによって自由に定義できる。

繰り返し

XML側は典型的には2パターンありそうだ。

<feed>
  <entry>...</entry>
  <entry>...</entry>
  <entry>...</entry>
</feed>
<feed>
  <list>
    <entry>...</entry>
    <entry>...</entry>
    <entry>...</entry>
  </list>
</feed>

上記のどちらの場合も、Java側は以下の構造にマッピングできる。

class Feed {
    private List<Entry> entries;
}

前者のXMLの場合、Java側で使うアノテーションは次のようになる。

class Feed {
    @XmlElement(name = "entry")
    private List<Entry> entries;
}

element名と変数名が異なるのでname=を指定したが、同じなら省略可能。

後者のXMLの場合、

class Feed {
    @XmlElementWrapper(name = "list")
    @XmlElement(name = "entry")
    private List<Entry> entries;
}

その他

自分の子孫のelementの内容やattributeを自分のプロパティ(や変数)にマッピング

MOXyの独自機能で@XmlPathを利用する。

    @XmlPath("nameKana/@disclose") // element "nameKana"の "disclose" attribute
    private String nameKanaDisclosed;

プロパティや変数として定義していないその他のelementやattributeを拾う

@XmlAnyElementや@XmlAnyAttribute を使うらしい。

コンテントで、テキストと要素が混在するような要素を扱う

XML Schema では、complexTypeにmixed="true"を指定することで表現される。

JAXBでは、@XmlMixedを使うらしい。Java側はListとなり、Listの要素は、テキストはString、要素はJAXBElementまたはElementとなる。

@XmlMixed

6.2.7.8 Annotations for Mixed Content: XmlElementRef, XmlMixed

simple typeの要素であっても、要素を1つのクラスに対応付けて、要素のコンテントをプロパティにセットしたい

@XmlValueを使う。
要素のクラスを用意し、フィールドを1つだけ持たせ、そこに@XmlValueを付ける。

@XmlRootElement(name = "p")
public class Parameter {
    @XmlValue
    String text;
}

同じXML Schemaに対し、処理目的別に、異なるJavaクラスを用意する

これ自体は可能で、unmarshalするときに使うJavaクラスの使い分けをすることができる。
使い分けは、JAXBContext.newInstanceに指定するクラスを切り替えることで実現できる。

以下のケースはうまくいかない。

  • ある処理用(JAXBでの解析)にクラスAを作成。これは、XMLの型aに対応するものとする。
  • 別の処理用(JAXBでの解析)にクラスAを継承したBを作成。これも、XMLの型aに対応する。

JAXBContext.newInstance(B.class)をにより、Bを利用しようとすると、Bの親クラスであるAもJAXBに認識されるが、AもBも同じXML型に対応するのでエラーとなる。

また、このエラーが回避できたとしても(クラスAまたはBに@XmlType(name="...")を指定して、XML側での型名がAとBで異なるようにすれば回避できる)、次の課題が生じる。
Aの変数 X foo; により、element "foo"の型がXであると認識される。Xは「ある処理」用のクラスであり、「別の処理」のときに、Xとは異なるクラスを割り当てたくても、そうすることはできない。クラスBにY foo;を定義し、Y fooに@XmlElementなどをつけて親クラスの定義を上書きしようとしても、無視された。(MOXyで確認)

enum

enumをマッピングする

EclipseLink MOXy 利用時の留意点

MOXyを有効にするには、jaxb.propertiesというファイルを作り、以下の内容とする。

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory

jaxb.propertiesは、クラスsome.package.AでMOXy固有機能を使っているなら、some.packageのディレクトリに置く。

maven利用時は置く場所に注意。src/main/java/<パッケージ名>ではなく、src/main/resources/<パッケージ> に置く。たとえば、src/main/resources/some/package に置く。

MOXyが利用されているか確認するには、
JAXBContext.newInstance(Class...)実行の結果取得されたJAXBContextの実装クラスが org.eclipse.persistence.jaxb.JAXBContext であることを確認する。

利用されていない場合には、JAXBが、some.package.Aを処理対象として認識していない可能性がある(このページの上のほうに記載のとおり)。newInstanceの引数にAを加えるのがより確実。

JAXB.unmarshalでは、うまく認識されない場合があった。想像でしかないが、JAXBContext.newInstanceでルートelementのクラス以外も指定しなければならない状況では、JAXB.unmarshalは動作しないのではないか。