XMLシリアライズ・デシリアライズライブラリ"Simple"

職場で"XML設定ファイルの仕様+実物を作って欲しい、もちろん急ぎで。"とか言われたものの、スキーマ定義書くのも面倒だしテキストエディタXML書くのも冗長でめんどくさい。JavaのオブジェクトをそのままXMLに落し込みたいなあ・・・と思って色々とライブラリを探してみた。
設定ファイルと聞いてまず最初に思いつくのがJakarta Commons Digesterだが・・・・XMLファイルから内容を読み込むためのルールをまたXMLで定義するとか、面倒過ぎる。それにcommons系のライブラリは依存jar地獄に陥りがちだし。

で、見つけてきたのが"Simple"。とにかくシンプルで楽に使えることを目指しているらしいので、ちょっと使ってみた。

jarファイルについて

必要なライブラリは下記の3つのみ。ダウンロードページから入手できる。

もちろんmavenでもデフォルトのセントラルリポジトリから入手可能。pom.xmlに下記のdependencyを追加すればOK。

<dependency>
    <groupId>org.simpleframework</groupId>
    <artifactId>simple-xml</artifactId>
    <version>2.4</version>
</dependency>


simpleのライブラリに付属してくるstaxのライブラリについては、ダウンロードページの説明によると

For performance it is recommended that the StAX dependencies are used with Java 5, without these DOM is used by default.

とあり、特にパフォーマンスにこだわらなければ無くても構わないようだ。
mavenプロジェクトでこれらのライブラリを除外したい場合には、pom.xmlのdependency設定を下記のようにすればよい。

<dependency>
  <groupId>org.simpleframework</groupId>
  <artifactId>simple-xml</artifactId>
  <version>2.4</version>
  <exclusions>
    <exclusion>
      <groupId>stax</groupId>
      <artifactId>stax-api</artifactId>
    </exclusion>
    <exclusion>
      <groupId>stax</groupId>
      <artifactId>stax</artifactId>
    </exclusion>
  </exclusions>
</dependency>

シリアライズ対象クラスを作る

まず最初に、xmlシリアライズするためのJavaクラスを作っておく。オリジナルの設定クラスは晒せないので、取り敢えずの例として会社を表す"Company"クラスと、そこに所属する従業員を表す"Employee"クラスを作成する。

Company.java

import java.util.ArrayList;
import java.util.List;

import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.ElementList;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.Root;
import org.simpleframework.xml.Version;

@Root
public class Company {
  
  /**XMLファイルのバージョンを示すプロパティ*/
  @Version(revision=1.0)
  private double version;
  
  /**社名*/
  @Attribute(name="name")
  private String name;

  /**住所*/
  @Element
  private String address;
  
  /**アノテーションがない場合、XMLシリアライズ化の対象外*/
  private String secret = null;
  
  /**従業員リスト*/
  @ElementList
  private List<Employee> employees = new ArrayList<Employee>();

  public Company(){}
  public Company(String name, String address, String secret, List<Employee> employees){
    this.name = name;
    this.address = address;
    this.secret = secret;
    this.employees = employees;
  }
  
  //以下、getter, setter, toString()オブジェクトは省略
}

クラスプロパティについているアノテーションは以下の意味を持つ
@Root: XMLルート要素
@Element: XML子要素
@ElementList: 列挙型の子要素
@Attribute: XML属性
引数nameで、XML要素・属性名を指定することが可能。指定しない場合にはクラス名・クラスプロパティ名となる。
アノテーションされたプロパティの値がnullの場合には、XMLシリアライズは失敗する。setter側でnullチェックを行ったほうが良い。

アノテーションされていないプロパティはシリアライズされない。

次に、従業員リスト要素にネストされるEmployeeクラスを作成。
Employee.java

import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.Root;

@Root
public class Employee {
  
  @Element
  private String name;
  
  @Attribute
  private String id;
  
  @Element
  private String section;
  
  public Employee(){}
  public Employee(String name, String id, String section){
    this.name = name;
    this.id = id;
    this.section = section;
  }
  //以下、getter, setter, toString()オブジェクトは省略
}

次に、シリアライズ・デシリアライズを行うためのクラスを作成する。
XmlManager.java

import java.io.File;

import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;

import Company;

public class XmlManager {
  //Simpleの提供するserialize,deserialize用クラス
  private Serializer serializer = new Persister();
  //読み込み・書き出し用ファイル
  private File file; 
  
  /**
   * @param file 読み込み・書き出し用ファイル
   * */
  public XmlManager(File file){
    if(file == null){
      throw new IllegalArgumentException("File is null.");
    }
    this.file = file;
  }
  
  /**
   * CompanyオブジェクトをXMLに変換します。
   * */
  public void serialize(Company company) throws Exception{
    serializer.write(company, file);
  }

  /**
   * XMLファイルからCompanyオブジェクトを読む込みます。
   * */
  public Company deserialize() throws Exception{
    return serializer.read(Company.class, file, true);
  }
}

テストデータの作成とシリアライズ実行

SimpleExample.java

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import Company;
import Employee;

public class SimpleExample {
  
  public static void main(String[] args){
    
    //データ作成
    Employee emp1 = new Employee("国税太郎", "id:001", "税務課");
    Employee emp2 = new Employee("国勢太郎", "id:002", "調査課");
    Employee emp3 = new Employee("出羽守", "id:003", "海外事業部");
    List<Employee> empList = new ArrayList<Employee>();
    empList.add(emp1);
    empList.add(emp2);
    empList.add(emp3);
    //エスケープ文字を含むデータ
    Company company = new Company("<クトゥルフ商事>", "\"マサチューセッツアーカム\"", 
        "皆でイァ!イァ!と称え合うだけの楽しい職場です。", empList);

    //XML読み出し・書き込みオブジェクトの作成
    XmlManager manager = new XmlManager(new File("example.xml"));
    
    try {
      manager.serialize(company);
    } catch (Exception e) {
      e.printStackTrace();
      System.exit(1);
    }
  }
}

上記の例で作成されるxmlファイルの内容は、以下のとおり。

<company name="&lt;クトゥルフ商事&gt;">
   <address>&quot;マサチューセッツアーカム&quot;</address>
   <employees class="java.util.ArrayList">
      <employee id="id:001">
         <name>国税太郎</name>
         <section>税務課</section>
      </employee>
      <employee id="id:002">
         <name>国勢太郎</name>
         <section>調査課</section>
      </employee>
      <employee id="id:003">
         <name>出羽守</name>
         <section>海外事業部</section>
      </employee>
   </employees>
</company>

おお!アノテーションしたとおりにXMLに出力される。ちゃんとエスケープもされてるし、UTF-8で出力されてるから全角文字を含んでも大丈夫だし良かったよかった・・・・
と言いたいところだが、xml宣言されてないよ。orz

XML宣言を付ける

Simpleのチュートリアル(凄く充実してるので、目を通しておいたほうが良い)を探してみたが、特にXML宣言については触れられていなかった。で、Javadocを探してみると・・・FormatクラスXML宣言を指定できそう。早速試してみる。

XmlManager.javaでSerializerを作成する処理にFormatの指定を入れてみた。

public class XmlManager {
  private static final String XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; 
  private Format format = new Format(XML_DECLARATION);
  private Serializer serializer = new Persister(format);

で、出力されるXMLにちゃんとXML宣言が入っていることを確認した。

<?xml version="1.0" encoding="UTF-8"?>
<company name="&lt;クトゥルフ商事&gt;">
   <address>&quot;マサチューセッツアーカム&quot;</address>
   <employees class="java.util.ArrayList">
      <employee id="id:001">
         <name>国税太郎</name>
         <section>税務課</section>
      </employee>
      <employee id="id:002">
         <name>国勢太郎</name>
         <section>調査課</section>
      </employee>
      <employee id="id:003">
         <name>出羽守</name>
         <section>海外事業部</section>
      </employee>
   </employees>
</company>

同じXMLファイルから内容を読み込んで、正しくオブジェクトの値が指定されていることも確認した。
文字コードについては、Persisterクラスcharsetを指定できるメソッドが用意されている。
ただ、Serializerインターフェイスにはそのメソッドは無いので、注意。

まとめ

xmlスキーマ定義をアノテーションから生成してくれればベストだけど、そこまでは望まない。
追記:正直、どのみちスキーマ定義を書くなら最初からjaxb使っておけばよかったのか?Serializer#validateメソッドでバリデーションチェックは出来るわけだし、アノテーション+実装クラスが定義だ!と言い張ってもいい気がしてきた。
しかし・・・アノテーションが実質的なスキーマ定義に成ってるわけだが、やはり実装クラス内に定義を書き込むというのは違和感がある。

参考資料

内容が充実しており、目を通しておいたほうがいいというか必読。

Simpleの使い方についての説明としては、DeveloperWorksの記事だけでもなんとかなる。今回のエントリではカバーできなかった「テンプレート・フィルター」機能についても述べられている。

XML宣言やエンコーディングについては、Javadocを調べる前にぐぐってここを見とけば解決してた。

以下追記

class属性を消したい

上のXMLをよくみると、リスト型要素に必ずclass属性が付き、Javaクラス名が指定されている。

class="java.util.ArrayList"

実はこの属性を消した状態でXMLファイルを読み込んでもちゃんとオブジェクトにマッピングしてくれるし、設定ファイルとして使うには邪魔なので消してみる。
(ただし、実装クラスを指定する必要がある場合もあるので、そういう場合にはむやみに消してしまわないこと!)

まずは下記のように、org.simpleframework.xml.strategy.Visitorインターフェイスを実装するクラスを作成し、writeメソッド内にXMLファイルへの書き出し時に行う処理を書いておく。


RemoveClassAttributeVisitor.java

import org.simpleframework.xml.strategy.Type;
import org.simpleframework.xml.strategy.Visitor;
import org.simpleframework.xml.stream.InputNode;
import org.simpleframework.xml.stream.NodeMap;
import org.simpleframework.xml.stream.OutputNode;

public class RemoveClassAttributeVisitor implements Visitor {

   public void write(Type type, NodeMap<OutputNode> node) throws Exception {
      OutputNode element = node.getNode();
      NodeMap<OutputNode> attrs = element.getAttributes();
      //属性名が"class"の属性を削除
      attrs.remove("class");
   }

  public void read(Type type, NodeMap<InputNode> node) throws Exception {
    // 読み込み用メソッドなので無視
  }
} 

次に、XmlManagerクラス内でPersisterインスタンスを作成するときに、PersisterのコンストラクタでStragegyを指定する。StrategyのインスタンスはVisitorStrategyを使用し、コンストラクタに上で作ったVisitor実装クラスを指定する。

import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
import org.simpleframework.xml.strategy.Strategy;
import org.simpleframework.xml.strategy.VisitorStrategy;
import org.simpleframework.xml.stream.Format;

//中略

  private static final String XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; 
  private Format format = new Format(XML_DECLARATION);
  private Strategy strategy = new VisitorStrategy(new RemoveClassAttributeVisitor());
  private Serializer serializer = new Persister(strategy,format);

こうして作成したSerializerを使ってxmlファイルに書きこむと、下のようにclass属性が書きだされることは無くなる。

<?xml version="1.0" encoding="UTF-8"?>
<company name="&lt;クトゥルフ商事&gt;">
   <address>&quot;マサチューセッツアーカム&quot;</address>
   <employees>
      <employee id="id:001">
         <name>国税太郎</name>
         <section>税務課</section>
      </employee>
      <employee id="id:002">
         <name>国勢太郎</name>
         <section>調査課</section>
      </employee>
      <employee id="id:003">
         <name>出羽守</name>
         <section>海外事業部</section>
      </employee>
   </employees>
</company>

このようにVisitor実装クラスを使用することで、属性の削除の他にもXMLシリアライズするときにコメントを付けることが出来たりと色々便利。