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
エラー: 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.SaveXmlwork$
出来ました、と言いたいところですが、名前空間が消えてしまっています。うーむ。
これは今後の宿題として、次は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() ); } } }
参考
以下を参考にしました。