torutkのブログ

ソフトウェア・エンジニアのブログ

JAXBでJavaクラスからXML schema生成

Java SE 標準APIのJAXBを使うと、Javaインスタンスの内容をXML形式で出力したり、逆にXMLファイルからJavaインスタンスを生成することができます。

JAXBは最初、XML Schema定義からJavaクラスを生成し、そのクラスを利用して実行時にJavaインスタンスXML文書のマッピングをする機能が提供されました。その後のバージョンで、Javaクラス定義*1からJavaインスタンスXML文書のマッピングをする機能が提供されました。そのためもあってか、解説記事は大半がXML Schema定義からJavaクラスを生成するものとなっています。

今回、Javaクラス定義からXML文書とマッピングするために必要な流れを、一歩一歩試しながら理解したいと思います。

なお、Javaクラス定義からXML文書とマッピングさせる場合、XML Schemaを生成する必要はありませんが、生成するXML文書の定義を確認する意味で、いったんXML Schemaを生成して内容を見ていきます。

  • 実行環境は、Java SE 7

最初の一歩:素のクラス

まず、Javaクラスを定義します。

package jp.gr.java_conf.torutk.weather;

public class Weather {
}

これを次のディレクトリ構成に置きます。

work
 +- classes
 +- src
     +- jp
         +- gr
             +- java_conf
                 +- torutk
                     +- weather
                         +- Weather.java

XML Schemaを生成するときは、JDKに含まれるコマンドschemagenを実行します。

work$ schemagen -d classes src/jp/gr/java_conf/torutk/weather/Weather.java
work$

すると、classes/schema1.xsd が生成されます。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:complexType name="weather">
    <xs:sequence/>
  </xs:complexType>
</xs:schema>

XML Schema上には型定義(complexType型で名前がweather)が生成されています。型名は、クラス名を小文字にしたものがデフォルトです。*2

XML名前空間の指定

XML Schemaでは、極力名前空間を使うのがよいとされているので、生成されるSchemaに名前空間が含まれるように、JAXBのアノテーションを付与します。クラスに指定する方法もありますが、一般にはパッケージ名に対比させることが多いので、JavaのパッケージにJAXBのアノテーション@XmlSchemaを付けます。
パッケージにアノテーションを付けるために、package-info.javaを作成します。

@javax.xml.bind.annotation.XmlSchema(
    namespace="http://java-conf.gr.jp/torutk/weather",
    xmlns=@javax.xml.bind.annotation.XmlNs(
        prefix="tns",
        namespaceURI="http://java-conf.gr.jp/torutk/weather"
    )
)
package jp.gr.java_conf.torutk.weather;

名前空間を指定するため、namespace属性を指定するとともに、カメレオンスキーマはよくないとされているので、名前空間tnsを指定しています。
名前空間は、javaのパッケージ名(DNSの逆順)をURI式に表現したものを使用しています。

なお、javaのパッケージ名にはハイフン('-')が使えないため、規約に従いハイフンをアンダースコアに置き換えていますので、URI式に表現するときはもとのハイフンに戻しています。

このpackage-info.javaを含むディレクトリ構成は以下になります。

work
 +- classes
 +- src
     +- jp
         +- gr
             +- java_conf
                 +- torutk
                     +- weather
                         +- package-info.java
                         +- Weather.java

schemagenでXML Schemaを生成します。

work$ schemagen -d classes src/jp/gr/java_conf/torutk/weather/*.java
work$

生成されたスキーマファイルclasses/schema1.xsd は次のとおりです。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0"
    targetNamespace="http://java-conf.gr.jp/torutk/weather"
    xmlns:tns="http://java-conf.gr.jp/torutk/weather"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:complexType name="weather">
    <xs:sequence/>
  </xs:complexType>
</xs:schema>

XML Schemaには、型宣言は生成されていますが、要素宣言はまだ生成されていません。

XMLルート要素の指定

ルート要素になるクラスにJAXBのアノテーション@XmlRootElementを付けます。

package jp.gr.java_conf.torutk.weather;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Weather {
}

先ほどと同じ指定でschemagenコマンドを実行します。生成されたclasses/schema1.xsd は次のとおりです。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0"
    targetNamespace="http://java-conf.gr.jp/torutk/weather"
    xmlns:tns="http://java-conf.gr.jp/torutk/weather"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="weather" type="tns:weather"/>

  <xs:complexType name="weather">
    <xs:sequence/>
  </xs:complexType>
</xs:schema>

ルート要素 weather の指定が生成されました。要素名はクラス名を小文字にしたものとなっています。*3

デフォルトでは、型がグローバル定義となっています。*4

Weatherクラスのフィールド定義

Weatherクラスは今まで空っぽでしたが、フィールドを追加します。ここでは、Measureクラスのリストをフィールドに定義します。Weather(気象データ)がMeasure(計測値)を複数保持するというデータ構造です。

  • Weather.javaの修正
package jp.gr.java_conf.torutk.weather;

import javax.xml.bind.annotation.XmlRootElement;
import java.util.List;

@XmlRootElement
public class Weather {
    private List<Measure> measures;
}
  • Measure.javaの新規追加
package jp.gr.java_conf.torutk.weather;

public class Measure {
}

先ほど同様schemagenコマンドを実行します。しかし、生成されたschema1.xsdは先ほどと変わりません。

XMLに出力するフィールドは、JAXBアノテーションで指定する必要があります。アノテーションは、フィールドを、子要素とするか属性とするかで、@XmlElementあるいは@XmlAttributeを指定します。*5

フィールドにアノテーション@XmlElementを追加
package jp.gr.java_conf.torutk.weather;

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlElement;
import java.util.List;

@XmlRootElement
public class Weather {
    @XmlElement
    private List<Measure> measures;
}

これを先と同じくschemagenコマンドで実行すると、以下のclasses/schema1.xsdが生成されます。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0"
    targetNamespace="http://java-conf.gr.jp/torutk/weather"
    xmlns:tns="http://java-conf.gr.jp/torutk/weather"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="weather" type="tns:weather"/>

  <xs:complexType name="weather">
    <xs:sequence>
      <xs:element name="measures" type="tns:measure" minOccurs="0" maxOccurs="unbounded"/>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="measure">
    <xs:sequence/>
  </xs:complexType>
</xs:schema>

JavaのListが、XMLではmeasures要素の繰り返しに展開されています。java.util.ListはJAXBが標準で対応しています。ちなみに、java.util.Mapにしたところ、schemagenコマンドがエラーとなりました。

エラー: java.util.Map is an interface, and JAXB can't handle interfaces.
XML文書としてリストを束ねる要素があった方がいい?

上述のListの場合、XML文書のイメージは次のようになります。

<weather>
  <measures .../>
  <measures .../>
</weather>

リストの場合、XML的には次のようになっていた方が気持ちがいいです。

<weather>
  <measures>
    <measure .../>
    <measure .../>
  </measures>
</weather>

そのためには、Listのフィールドにアノテーション@XmlElementWrapperを追加します。

package jp.gr.java_conf.torutk.weather;

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import java.util.List;

@XmlRootElement
public class Weather {
    @XmlElementWrapper
    @XmlElement(name="measure")
    private List<Measure> measures;
}

schemagenコマンドで生成したclasses/schema1.xsdは次のようになります。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0"
    targetNamespace="http://java-conf.gr.jp/torutk/weather"
    xmlns:tns="http://java-conf.gr.jp/torutk/weather"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="weather" type="tns:weather"/>

  <xs:complexType name="weather">
    <xs:sequence>
      <xs:element name="measures" minOccurs="0">
        <xs:complexType>
          <xs:sequence>
            <xs:element name="measure" type="tns:measure" minOccurs="0" maxOccurs="unbounded"/>
          </xs:sequence>
        </xs:complexType>
      </xs:element>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="measure">
    <xs:sequence/>
  </xs:complexType>
</xs:schema>

Measure.javaのフィールド定義

先ほどまでは、空のクラスであったMeasureに、フィールドを追加します。まだ、JAXBアノテーションは付けないでおきます。

package jp.gr.java_conf.torutk.weather;

import java.util.Calendar;

public class Measure {
    private double temperature;
    private double humidity;
    private int windDirection;
    private int windSpeed;
    private Calendar dateTime;
}

schemagenコマンドを実行して生成されたclasses/schema1.xsdは、先ほどと変わりません。やはり、なんらかのJAXBアノテーションを付ける必要があります。

Measure.javaにJAXBアノテーションを追加する

ここで、各フィールドを属性として生成したいので、@XmlAttributeを指定します。

package jp.gr.java_conf.torutk.weather;

import java.util.Calendar;
import javax.xml.bind.annotation.XmlAttribute;

public class Measure {
    @XmlAttribute
    private double temperature;
    @XmlAttribute
    private double humidity;
    @XmlAttribute
    private int windDirection;
    @XmlAttribute
    private int windSpeed;
    @XmlAttribute
    private Calendar dateTime;
}

schemagenコマンドを実行すると、XML Schemaとしてclasses/schema1.xsdが生成されます。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0"
    targetNamespace="http://java-conf.gr.jp/torutk/weather"
    xmlns:tns="http://java-conf.gr.jp/torutk/weather"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="weather" type="tns:weather"/>

  <xs:complexType name="weather">
    <xs:sequence>
      <xs:element name="measures" minOccurs="0">
        <xs:complexType>
          <xs:sequence>
            <xs:element name="measure" type="tns:measure" minOccurs="0" maxOccurs="unbounded"/>
          </xs:sequence>
        </xs:complexType>
      </xs:element>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="measure">
    <xs:sequence/>
    <xs:attribute name="temperature" type="xs:double" use="required"/>
    <xs:attribute name="humidity" type="xs:double" use="required"/>
    <xs:attribute name="windDirection" type="xs:int" use="required"/>
    <xs:attribute name="windSpeed" type="xs:int" use="required"/>
    <xs:attribute name="dateTime" type="xs:dateTime"/>
  </xs:complexType>
</xs:schema>

JavaインスタンスからXML文書への出力

では、実際にJavaプログラムでWeatherインスタンスを生成し、その内容をXML文書に出力してみます。

Weather.javaとMeasure.javaは、フィールド定義しかないので、コンストラクタとアクセッサメソッドを追加します。

Weather.java の実装追加
package jp.gr.java_conf.torutk.weather;

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

@XmlRootElement
public class Weather {
    @XmlElementWrapper
    @XmlElement(name="measure")
    private List<Measure> measures;

    public Weather() {
        measures = new ArrayList<Measure>();
    }

    public void add(Measure measure) {
        measures.add(measure);
    }

    public List<Measure> getMeasures() {
        return Collections.unmodifiableList(measures);
    }
}
Measure.javaの実装追加
package jp.gr.java_conf.torutk.weather;

import java.util.Calendar;
import javax.xml.bind.annotation.XmlAttribute;

public class Measure {
    @XmlAttribute
    private double temperature;
    @XmlAttribute
    private double humidity;
    @XmlAttribute
    private int windDirection;
    @XmlAttribute
    private int windSpeed;
    @XmlAttribute
    private Calendar dateTime;

    Measure() {
    }

    public Measure(Calendar dateTime, double temperature, double humidity,
                   int windDirection, int windSpeed) {
        this.dateTime = (Calendar) dateTime.clone();
        this.temperature = temperature;
        this.humidity = humidity;
        this.windDirection = windDirection;
        this.windSpeed = windSpeed;
    }

    public double getTemperature() {
        return temperature;
    } 

    public double getHumidity() {
        return humidity;
    }

    public int getWindDirection() {
        return windDirection;
    }

    public int getWindSpeed() {
        return windSpeed;
    }

    public Calendar getDateTime() {
        return (Calendar) dateTime.clone();
    }
}

JAXB対象のクラスは、引数なしのデフォルトコンストラクタが必要です。publicでなくてもよいので、他のパッケージから使用させない場合にも対応できます。

XMLへの出力(SaveXml.java

標準出力にXML形式テキストを出力する簡単なサンプルです。

package jp.gr.java_conf.torutk.weather;

import java.util.Calendar;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

public class SaveXml {

    public static void main(String[] args) throws Exception {
        Calendar dateTime = Calendar.getInstance();
        dateTime.set(2011, 10 - 1, 1, 22, 0, 0);
        Measure m1 = new Measure(dateTime, 21.5, 55, 122, 3);
        dateTime.set(2011, 10 - 1, 1, 23, 0, 0);
        Measure m2 = new Measure(dateTime, 20.8, 58, 140, 2);
        Weather weather = new Weather();
        weather.add(m1);
        weather.add(m2);

        JAXBContext context = JAXBContext.newInstance("jp.gr.java_conf.torutk.weather");
        Marshaller marshaller = context.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(weather, System.out);
    }
}
コンパイルはできるが実行時にエラーが・・・

コンパイルします。

work$ javac -d classes -cp classes jp/gr/java_conf/torutk/weather/SaveXml.java
work$

実行します。

work$ java -cp classes jp.gr.java_conf.torutk.weather.SaveXml
Exception in thread "main" javax.xml.bind.JAXBException: 
Provider com.sun.xml.internal.bind.v2.ContextFactory could 
not be instantiated: javax.xml.bind.JAXBException:
 "jp.gr.java_conf.torutk.weather" doesnt contain ObjectFactory.class
 or jaxb.index
    :
work$ 

エラーメッセージによると、ObjectFactoryクラスかjaxb.indexをパッケージに含めないとだめとあります。

ObjectFactoryは、XML SchemaからJAXBのxjcコマンドを使ってJavaクラスを生成するときに作られるものですから、今回は、jaxb.indexを作成して対応します。

jaxb.indexファイルをパッケージに1つ作成する

jaxb.indexはテキストファイルで、JAXBでマーシャリング対象となるクラス名を列挙したものです。今回は2つのクラスWeatherとMeasureがマーシャリング対象なので、この2つを記述します。

Weather
Measure

クラス名は、パッケージ名を書かず、.classも付けません。
これを実行時のクラスパス上、対象パッケージの場所におきます。

work
 +- classes
 :   +- jp
         +- gr
             +- java_conf
                 +- torutk
                     +- weather
                         +- jaxb.index

もう一度プログラムを実行します。

work$ java -cp classes jp.gr.java_conf.torutk.weather.SaveXml


    
        
        
    

work$

出来ました、と言いたいところですが、名前空間が消えてしまっています。うーむ。
これは今後の宿題として、次はXML文書からの読み込みです。

XML文書からJavaインスタンスへの読み込み

XMLからの入力(LoadXml.java
package jp.gr.java_conf.torutk.weather;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import java.io.File;

public class LoadXml {

    public static void main(String[] args) throws Exception {
        JAXBContext context = JAXBContext.newInstance("jp.gr.java_conf.torutk.weather");
        Unmarshaller unmarshaller = context.createUnmarshaller();
        Weather weather = (Weather) unmarshaller.unmarshal(new File(args[0]));
        for (Measure measure : weather.getMeasures()) {
            System.out.printf(
                "%1$tFT%1$tT 気温:%2$3.1f 湿度:%3$3.1f 風向:%4$d 風速:%5$d%n",
                measure.getDateTime(), measure.getTemperature(),
                measure.getHumidity(), measure.getWindDirection(),
                measure.getWindSpeed()
            );
        }
    }
}

*1:JAXBのアノテーションを付与ししたもの

*2:クラスに@XmlType(name="Hoge")とアノテーションを付けると名前を変えることができます。また、@XmlType(name="")とすると、XML Schema上の型定義がグローバルではなくローカルとなります。

*3:@XmlRootElement(name="weatherinfo")とアノテーションで指定することもできます。

*4:型を要素のローカル定義とする場合、クラスのアノテーションに@XmlType(name="")とします。

*5:フィールドにアノテーションする以外にも、方法はありますが、ここでは割愛します。