[Java How To Programming] [Home on 246net] [Home on Alles net]
Powered by SmartDoc

JAXBによるXMLデータとプログラムの双方向結合

2010/12/15 GMT
TAKAHASHI,Toru
JAXBは、XMLデータとプログラム内のオブジェクトとの間を双方向に対応付けるAPIです。XMLデータを読み込みプログラム内にそのデータ内容を持つオブジェクトを作ることや、プログラム内で用意したオブジェクトをXMLデータに出力することができます。その際、文字列変換などの処理がまったく不要という便利な仕組みです。

目次

JAXBの使い方いろいろ

JAXBにはいろいろな使い方があると思います。JAXBの導入として、この章ではいろいろな使い方について軽くサンプルを作りながら見ていきます。

プログラムの設定ファイルとしての使い方

プログラムでは、実行時に外部から設定値を読み込んで、その値によって挙動を変えることがよくあります。Windows系のプログラムでは、よく「iniファイル」と言われるものが該当します。UNIX系のプログラムでは、ユーザーのホームディレクトリにドット('.')で始まるファイル名で設定を記述する(例: .vimrc)ものが該当します。Javaでは、java.util.Propertiesクラスで使用するプロパティ形式のテキストファイルがその仲間です。

また、企業内システムなどでは、設定内容をCSV形式で記述して読み込むことも見られます。これは、企業内のPCにはExcelが蔓延しているため、Excelで作成したい、というニーズから来ているようです。

こうしたテキスト形式のデータをプログラムで読み込む場合、プログラムでは、文字列を適切な型(単一の文字、文字列、整数、小数、列挙値、真偽値、など)に変換し、オブジェクトに形作っていく処理を書く必要があります。一見簡単なようですが、文字エンコード、改行コードの違い、シングルクォート/ダブルクォートの取り扱い、円記号('\')の扱い、大文字/小文字の違い、ファイルパスの場合のパス区切り子やドライブレターの扱い、などの煩雑な要素があります。

テキスト形式のデータは人が編集して作るため、そこには間違えが含まれていることを想定してプログラムを作ることが必要です。スペルミス、項目自体の指定忘れ、数値の桁間違い、文字エンコーディング/改行コードの間違い、コメントの書式間違い、などいろいろな間違いが想定されます。よく見かけるコーディングは、項目が読み出せない(存在しない)場合や読み出した値が所定の型に変換できない場合にデフォルト値(ハードコーディングした値)を使うというものですが、これは、運用の観点からみると設定したとおりにシステムが動作しないことになり、システムを稼動させて途中で間違いが検出された場合、あるいは間違いに気付かず動作を継続して運用ミスに発展してしまうと大問題です。

間違いを防ぐには、可能な限り早い段階でのエラー検出を行います。設定ファイルを作成時に検証すること、起動時に設定ファイルを検証すること、などが対策として考えられます。これを、各種プロパティ形式、CSV形式で行うにはかなりの作り込みが必要となります。

ここで、JAXBを使うと、設定ファイルをXMLのスキーマ定義に基づくXMLデータとして記述するので、作成時の検証、読み込み時の検証が作り込みなしに実現できます。スキーマ定義をどこまで厳密にするかによって検出できるエラーの範囲は変わりますが、設定ファイルをJAXBで実現することのメリットは大です。

JAXBによる設定ファイルの作成例

プログラムが起動したときに表示するGUIウィンドウの表示位置・大きさ・タイトル、そしてウィンドウの閉じるボタンを押したときの振舞いを、設定ファイルに基づき実現する例を作成します。

設定ファイルのXML記述イメージ

XML Schemaを定義するにあたって、設定ファイルのXML記述(XMLインスタンス文書)イメージを書きます。

設定ファイルのイメージ
<?xml version="1.0" encoding="UTF-8"?>
<configurations xmlns="http://java-conf.gr.jp/torutk/learn/jaxb/config">
  <window>
    <width>320</width>
    <height>200</height>
    <left>400</left>
    <top>300</top>
    <title>JAXB設定ファイル取り込み</title>
    <closeOperation>EXIT_ON_CLOSE</closeOperation>
  </window>
</configurations>

設定ファイルのXMLスキーマ定義

このXML Schemaを定義します。

設定ファイルのスキーマ定義
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://java-conf.gr.jp/torutk/learn/jaxb/config"
           xmlns:tns="http://java-conf.gr.jp/torutk/learn/jaxb/config">

<!-- ルート要素 configurations の定義 -->
<xs:element name="configurations">
 <xs:complexType>
  <xs:sequence>
   <xs:element ref="tns:window"/>
  </xs:sequence>
 </xs:complexType>
</xs:element>

<!-- 要素 window の定義 -->
<xs:element name="window">
 <xs:complexType>
  <xs:sequence>
   <xs:element ref="tns:width"/>
   <xs:element ref="tns:height"/>
   <xs:element ref="tns:left"/>
   <xs:element ref="tns:top"/>
   <xs:element ref="tns:title"/>
   <xs:element ref="tns:closeOperation"/>
  </xs:sequence>
 </xs:complexType>
</xs:element>

<!-- 要素 width, height, left, top の定義 -->
<xs:element name="width" type="xs:int"/>
<xs:element name="height" type="xs:int"/>
<xs:element name="left" type="xs:int"/>
<xs:element name="top" type="xs:int"/>

<!-- 要素 title の定義 -->
<xs:element name="title" type="xs:string"/>

<!-- 要素 closeOperation の定義 -->
<xs:element name="closeOperation" type="tns:closeOperationType"/>

<!-- 要素 closeOperation の型(列挙型)の定義 -->
<xs:simpleType name="closeOperationType">
 <xs:restriction base="xs:string">
  <xs:enumeration value="DISPOSE_ON_CLOSE"/>
  <xs:enumeration value="DO_NOTHING_ON_CLOSE"/>
  <xs:enumeration value="EXIT_ON_CLOSE"/>
  <xs:enumeration value="HIDE_ON_CLOSE"/>
 </xs:restriction>
</xs:simpleType>

</xs:schema>

XML Schemaは非常に複雑な仕様なので、これを隅々まで理解して使おうとしたら、大変なことになってしまいます。ここでは、JAXBで使うのに適した記述範囲だけ把握することを目的に、定義のノートを以下に記述します。

XML宣言文

XML Schema定義ファイルもXML文書なので、最初にXML宣言文が必要です。

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

encodingは、Javaのソースファイルを記述する文字エンコーディングに合わせておくとベターでしょう。日本語Windows OSの場合、システムの文字エンコーディングはシフトJISと言われますが、encoding="Shift_JIS"を指定すると厳密には異なるので、日本語Windows OSでシフトJISを使って記述するときは、encoding="Windows-31J"とします。

名前空間

XMLの名前空間はjavaのパッケージに変換されること、複数のスキーマ定義を1つのXMLファイルに記述できることから、必ず指定するようにします。

XMLスキーマの名前空間定義
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://java-conf.gr.jp/torutk/learn/jaxb/config"
           xmlns:tns="http://java-conf.gr.jp/torutk/learn/jaxb/config">

xmlns:xs属性は、この通りに記述します。XML Schema自身の名前空間識別子(http://www.w3.org/2001/XMLSchema)と、その名前空間接頭辞としてこのファイルではxsを使うことを宣言するものです。(古めの資料では、xsではなくxsdとしているものもありますが、命名の流儀の問題なのでどちらでもかまいません)。名前空間識別子は一意な文字列であれば形式は問いません。ここではURL形式の文字列を使用していますが、これはそのURLにスキーマが実在することを意味しているものではありません。単に一意な文字列を得ることを目的としてURLを使用したものです。

targetNamespace属性は、アプリケーション開発者が考える必要があります。このXML Schema定義ファイルによって定義される各要素・属性などが、ここで指定する名前空間識別子で識別される名前空間に属します。JAXBでは、ここで作成した識別子がJavaコードに生成される際のパッケージとなります。Javaのパッケージ名はインターネットドメイン名に基づき命名される規約があるので、インターネットドメイン名(URL)で指定するとよいでしょう。例えば、 http://java-conf.gr.jp/torutk/learn/jaxb/config は、インターネットドメイン名からJavaのパッケージ命名規約に従いパッケージ名 jp.gr.java_conf.torutk.learn.jaxb.config に対応付けられます。

xmlns:tns属性は、上のtargetNamespaceで指定した識別子と同じ識別子を記述します。このXML Schema定義で定義される項目を参照するときに使う接頭辞を導入するために記述しています。XML Schema定義ファイル自身の名前空間接頭辞は慣習的にtns(This NameSpace)が使われます。

ルート要素

ルート要素となる要素configurationsの定義を記述します。

ルート要素
<xs:element name="configurations">
 <xs:complexType>
  <xs:sequence>
   <xs:element ref="tns:window"/>
  </xs:sequence>
 <xs:complexType>
</xs:element>

xs:elementで要素名を指定します。ルート要素configurationsは子要素windowを持つので、xs:complexTypeで囲んで子要素を定義します。xs:complexTypeはxs:sequence、xs:choice、xs:all のいずれかを子に持ちますがよく使われるのがxs:sequenceです。子要素windowは、ここで(ローカルに)定義を記述せずに、別箇所で(グローバルに)定義しておき、それをref属性で参照するようにします。なお、同じ文書中であっても名前空間で定義される要素等を参照(ref)するときは、名前空間接頭辞を付ける必要があるので、ここではref="window"ではなく、ref="tns:window"と記述します。XML Schemaとしては、ローカル宣言でもグローバル宣言でもよいのですが、XML Schema定義のTipsとして名前空間を使え、要素のローカル宣言を使うな、を反映しています。

子要素windowの定義

子要素windowの定義を記述します。

子要素 window
<xs:element name="window">
 <xs:complexType>
  <xs:sequence>
   <xs:element ref="tns:width"/>
   <xs:element ref="tns:height"/>
   <xs:element ref="tns:left"/>
   <xs:element ref="tns:top"/>
   <xs:element ref="tns:title"/>
   <xs:element ref="tns:closeOperation"/>
  </xs:sequence>
 </xs:complexType>
</xs:element>

同じくxs:elementで要素名を指定します。子要素 window は、さらに子要素である width、height、left、top、title、closeOperation を持つので、xs:complexTypeで定義します。xs:complexTypeの子は今回もxs:sequenceを使用します。windowの子要素は先と同様ローカル宣言せず、グローバル宣言したものをrefで参照しています。

子要素width、height、left、top、titleの定義

windowの子要素となるwidth、height、left、top、titleの定義を記述します。これらは、さらに子要素を持たないので、typeで型を指定します。

子要素 width、height、left、top、title
<xs:element name="width" type="xs:int"/>
<xs:element name="height" type="xs:int"/>
<xs:element name="left" type="xs:int"/>
<xs:element name="top" type="xs:int"/>
<xs:element name="title" type="xs:string"/>

xs:intは、Javaのint型にマッピングされ、xs:stringはJavaのString型にマッピングされるので、JAXBでは一番よく使う型となります。

子要素closeOperationの定義

windowの子要素となるcloseOperationは、あらかじめ決まった複数の文字列のうちの1つを選択して記述するので、列挙型(Javaのenum)となるよう定義します。

子要素 closeOperation は列挙型で定義
<xs:element name="closeOperation" type="tns:closeOperationType"/>

<xs:simpleType name="closeOperationType">
 <xs:restriction base="xs:string">
  <xs:enumeration value="DISPOSE_ON_CLOSE"/>
  <xs:enumeration value="DO_NOTHING_ON_CLOSE"/>
  <xs:enumeration value="EXIT_ON_CLOSE"/>
  <xs:enumeration value="HIDE_ON_CLOSE"/>
 </xs:restriction>
</xs:simpleType>

xs:elementの定義では直後で定義する列挙型closeOperationTypeを型参照する記述をしています。JAXBで、Javaのenum型として生成するためには、ローカル宣言の型ではなくグローバル宣言の型を定義します。 列挙型の定義は、xs:stringをベースに取り得る文字列をxs:enumerationで複数定義していきます。

JAXBによるJavaコード生成

先に作成したXML Schema定義から、JAXBによりJavaソースコードを生成します。XML SchemaファイルからJavaソースコードを生成するには、JDK 6に含まれるxjcツールを使用します。

xjcツールでXML SchemaからJAXB Javaソースコードの生成
schema$ xjc windowSettings.xsd
parsing a schema...
compiling a schema...
jp\gr\java_conf\torutk\learn\jaxb\config\CloseOperationType.java
jp\gr\java_conf\torutk\learn\jaxb\config\Configurations.java
jp\gr\java_conf\torutk\learn\jaxb\config\ObjectFactory.java
jp\gr\java_conf\torutk\learn\jaxb\config\Window.java
jp\gr\java_conf\torutk\learn\jaxb\config\package-info.java
schema$  

自動生成されたJavaソースコードについて、コメント等を省略し、主要部分の抜粋を載せます。

ルート要素configurationsのJavaソースコード

Configurations.java
package jp.gr.java_conf.torutk.learn.jaxb.config;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {"window"})
@XmlRootElement(name = "configurations")
public class Configurations {
    @XmlElement(required = true)
    protected Window window;
    public Window getWindow() {
        return window;
    }
    public void setWindow(Window value) {
        this.window = value;
    }
}

いろいろアノテーションが付いていますが、それを除くとルート要素configurationsの名前のクラスで、子要素windowの名前のクラスをフィールドに持ち、そのアクセッサメソッドが定義されているだけのクラスです。

子要素windowのJavaソースコード

Window.java
package jp.gr.java_conf.torutk.learn.jaxb.config;

public class Window {
    protected int width;
    protected int height;
    protected int top;
    protected int left;
    protected String title;
    protected CloseOperationType closeOperation;

    public int getWidth() {
        return width;
    }
    public void setWidth(int value) {
        this.width = value;
    }
    :
}

アノテーションと、width以外のフィールドのアクセッサメソッドは省略しています。window要素に対応するクラスも、子要素のアクセッサメソッドで構成されています。

window要素の子要素で列挙型のcloseOperationのJavaソースコード

CloseOperationType.java
package jp.gr.java_conf.torutk.learn.jaxb.config;

public enum CloseOperationType {
    DISPOSE_ON_CLOSE,
    DO_NOTHING_ON_CLOSE,
    EXIT_ON_CLOSE,
    HIDE_ON_CLOSE;

    public String value() {
        return name();
    }

    public static CloseOperationType fromValue(String v) {
        return valueOf(v);
    }
}

列挙型がJavaのenum型にマッピングされています。

設定ファイル(XML)からデータの読み込み

JAXBによるJavaソースコード生成ができたら、次はいよいよJavaプログラム内から、外部のXMLファイルを読み込む処理になります。

XMLファイルの読み込みにあたっては、XMLスキーマによる妥当性検証をする/しないの選択があります。

読み込み時に使用するAPI

XMLデータを読み込むときに使用するAPIを使用する簡単な説明です。

JAXBでXMLデータを読み込むAPIの使用例(その1)
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

    :
try {
    JAXBContext context = JAXBContext.newInstance(Configurations.class);
    Unmarshaller marshaller = context.createUnmarshaller();
} Catch (JAXBException e) {
    // ここで捕捉する例外は、JAXBで生成したクラスに問題がある場合。
    // エラー回復は困難なので、プログラムの停止もしくは読み込み処理をスキップする
}

XMLデータを読み込むのに必要なのがUnmarshallerインスタンスです。これはJAXBContextインスタンスのcreateUnmarshallerメソッドで取得します。JAXBContextインスタンスは、同クラスのファクトリメソッドnewInstanceで取得します。newInstanceメソッドには複数のオーバーロードがありますが、ここでは引数に、JAXBでXMLスキーマのルート要素に対応する(この文書ではxjcによってXMLスキーマから生成されたConfigurationsクラスの)クラス・インスタンスを指定します。

この2つのAPIは、チェック例外JAXBExceptionをスローする可能性があります。チェック例外なので、try-catchで捕捉するか、throws節に記述し上位に伝搬する必要があります。ここで例外が発生するのは、JAXB対応クラスの記述に誤りがあるときなので、実行時に回復することは困難なエラーとなります。

JAXBでXMLデータを読み込むAPIの使用例(その2)
File file = new File("path/to/data.xml");
try {
    Configurations conf = (Configurations)unmarshaller.unmarshal(file);
} catch (JAXBException e) {
    // アンマーシャル時の予期されないエラーなのだが、ここではfileで指定したファイルが
    // 読み込み不可のときもこの例外がスローされる
}

(その1)で取得したUnmarshallerインスタンスのunmarshalメソッドを呼び出すと、ルート要素に対応するクラス(この例ではConfigurations)のインスタンスが取得できます。なお、unmarshalメソッドの戻り値はObject型なので、キャストしています。unmarshalメソッドはいくつものオーバーロードが用意され、この例でのFileインスタンスだけでなく、InputStream、URL、StringBufferをもとに生成したStreamSource、DOMのDocument、SAXSource、StAXのXMLStreamReaderやXMLEventReaderなど多彩な入力源から取り込むことができます。

なお、この方法では、指定されたXMLインスタンス文書はXMLスキーマ定義にもとづく妥当性検証を行わないので、整形式(XMLの文法に適合している)であればスキーマ定義で指定した型と矛盾する記述があってもエラーを通知することなく読み込んでしまいます。例えば、画面の幅(width)はスキーマではxs:intを型に指定していますが、32a0のように数値以外の文字を記述してもエラーではなく、値が0となるだけです。

そこで、スキーマを指定して妥当性検証を行うAPIの使用例を次に示します。

特定のパスにあるスキーマで妥当性検証

特定のパスにあるスキーマで妥当性検証
    JAXBContext context = JAXBContext.newInstance(Configurations.class);
    Unmarshaller unmarshaller = context.createUnmarshaller();
    SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    Schema schema = factory.newSchema(new File("path/to/windowSetting.xsd"));
    unmarshaller.setSchema(schema);

    File file = new File("path/to/setting.xml");
    Configurations configurations = (Configurations)unmarshaller.unmarshal(file);
    Window windowSetting = configurations.getWindow();

スキーマ定義ファイルのパスを指定してスキーマを生成し、アンマーシャラにセットします。妥当性検証によって、XMLデータファイル読み込み時にエラーが検出できます。ただし、スキーマ定義ファイルのパスをどう持つかが課題です。クラスパス上に置くというのも一手です。クラスパスの起点から、schemas/windowSetting.xsdとなる場所に置くようにします。このときは、schemaインスタンスを取得するコードは以下になります。

クラスパス上にあるスキーマからschemaインスタンスを生成する
Schema schema = factory.newSchema(Configurations.class.getResource("/schemas/windowSetting.xsd"));    

XMLインスタンス中で指定したスキーマで妥当性検証

XMLインスタンス中で指定したスキーマで妥当性検証
    JAXBContext context = JAXBContext.newInstance(Configurations.class);
    Unmarshaller unmarshaller = context.createUnmarshaller();
    SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    Schema schema = factory.newSchema();
    unmarshaller.setSchema(schema);

    File file = new File("path/to/setting.xml");
    Configurations configurations = (Configurations)unmarshaller.unmarshal(file);
    Window windowSetting = configurations.getWindow();

XMLインスタンス中にスキーマ定義ファイルをschemaLocationで指定するこの方法では、スキーマの場所をURLで記述し、そのURLがネットワーク上の一元管理可能な場所であり、かつ、そのURLへアクセス可能なマシン上で実行する場合は問題はないですが、そうでない場合はやっかいです。スキーマ定義ファイルを絶対パスや相対パスで指定することになり、スキーマ定義ファイルがあちこちに散在することになりかねず、スキーマ定義ファイルの変更管理が破綻します。

スキーマ定義の指定は何がよいか

構成管理(変更管理)として一番問題が少ないと思われるのが、スキーマ定義ファイルをプログラムと一緒に配布する1番目の方法です。これならば、スキーマファイルはプログラムの構成管理の一部として一緒に行えるので、スキーマの変更→スキーマからJavaソースファイル生成→ビルド→配布と一元管理できます。他の方法では、少なからずスキーマファイルの変更とソースコード変更、プログラムの配布が連動しない可能性があります。