XML読み書き、Java Object との相互変換に何を使う?で問われてだいたいは、JAXB を挙げる。
他の選択肢は?で XStream を挙げてくれる人は少ない。
5年前に、XStream を知ってその頃は、あまり活発でなかった。しばらく触れる機会がなかったが、
2022年1月には、バージョン 1.4.19 になっていたんですね。
<dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.4.19</version> </dependency>
XStream の チュートリアル
https://www.tutorialspoint.com/xstream/index.htm
Java Object → XML
対象Object lombok を使う。
package org.example.entity; import java.util.List; import com.thoughtworks.xstream.annotations.XStreamAlias; import lombok.Data; /** * Person */ @XStreamAlias("person") @Data public class Person { private String firstname; private String lastname; private PhoneNumber phone; private PhoneNumber fax; private List<Integer> checkYear; private String body; }
package org.example.entity; import lombok.Data; /** * PhoneNumber */ @Data public class PhoneNumber{ private int code; private String number; }
XStream インスタンスの準備
@XStreamAlias を使う場合は、XStream#processAnnotations で対象クラスを指定
@XStreamAlias を使わない場合は、各クラス、タグに対して
XStream#alias(タグ名, クラス) を実行しなければならない
null に対して空タグでシリアライズしたいのでカスタムの Converter を用意して0
設定する。
XStream null value を出力するケース、再び書き直す。 - Oboe吹きプログラマの黙示録 参照
XStream xstream = new XStream(); // null の場合のカスタム Converter CustomEmptyConverter emptyConverter = new CustomEmptyConverter(xstream.getMapper(), e->{ if (e instanceof Person) { // Person クラス上だから。。 return "body"; } return null; }); xstream.registerConverter(emptyConverter, XStream.PRIORITY_VERY_LOW); xstream.processAnnotations(Person.class); xstream.alias("year", int.class);
CustomEmptyConverter のコード
import java.util.function.Function; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.reflection.ReflectionConverter; import com.thoughtworks.xstream.converters.reflection.SunUnsafeReflectionProvider; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.mapper.Mapper; /** * CustomEmptyConverter */ public class CustomEmptyConverter extends ReflectionConverter{ private Function<Object, String> emptywriter; /** * コンストラクタ. */ public CustomEmptyConverter(Mapper mapper, Function<Object, String> emptywriter){ super(mapper, new SunUnsafeReflectionProvider()); this.emptywriter = emptywriter; } @Override protected void doMarshal(final Object object, final HierarchicalStreamWriter writer, final MarshallingContext context){ super.doMarshal(object, writer, context); String tagname = emptywriter.apply(object); if (tagname != null){ writer.startNode(tagname); writer.setValue(""); writer.endNode(); } } }
シリアライズ実行
String xml = xstream.toXML(person);
結果の例
<person> <firstname>太郎</firstname> <lastname>山田</lastname> <phone> <code>110</code> <number>000-0000-1111</number> </phone> <fax> <code>120</code> <number>03-1111-1112</number> </fax> <checkYear> <year>2017</year> <year>2021</year> <year>2022</year> </checkYear> <body></body> </person>
タグ名に対する Java Object のクラス名を解決する必要があり、
XStream#allowTypesByWildcard
で、パッケージ名+".**" を指定しなければならない。
String[] なので、複数を指定できる。
XStream xstream = new XStream(); xstream.processAnnotations(Person.class); xstream.allowTypesByWildcard(new String[]{ "org.example.entity.**" });
シリアライズ時同様に、processAnnotationsを忘れずに。
デシリアライズ実行
try(InputStream inst = getSourceInputStream(this.getClass(), "test.xml")){ Person tp = (Person)xstream.fromXML(inst); // TODO }catch(IOException e){ e.printStackTrace(); }catch(URISyntaxException e){ e.printStackTrace(); }
指定する Class と同じ場所のファイルの InputStream を取得するメソッド
public InputStream getSourceInputStream(Class<?> cls, String fileName) throws IOException, URISyntaxException { return new FileInputStream(new File(Thread.currentThread() .getContextClassLoader() .getResource(cls.getPackageName().replaceAll("\\.", "/") + "/" + fileName) .toURI() )); }