Javaで仕様変更耐性を保ちつつMessagePackとJSONの相互変換を行う

# MessagePackとJSON

* 圧縮効率の高さ: MessagePack>JSON
* 可読性: JSON>MessagePack
* (表現の複雑度: XML>JSON≧MessagePack)　※表現の複雑度≒実装の難易度

であるため、(人間が読む可能性のある)ファイルの書き出しはJSON、アプリケーション同士の通信はMessagePackなどで行われるのが最近の流行(な気がする)。イメージとしては以下のような感じです。

ユーザ ⇐(JSON)⇒ API ⇐(MessagePack)⇒ DB

また、RDBなどで検索対象に入らない**階層的なデータ**をMessagePackで格納すると圧縮効率、処理効率が高くとても良いです。

# JavaのMessagePackの実装の問題点とその対応

MessagePackは2015年8月現在、0.6と0.7の実装がありますが、0.7は絶賛開発中で、templateパッケージなどJavaのObjectとの連携を自力で実装する必要があるので、今回は0.6を前提に書きます。
JavaのMessagePackでJavaのObjectをシリアライズした場合、フィールド名はシリアライズされません。
フィールド名をシリアライズしないということはObjectの定義が予め用意できれば圧縮効率が高く効率的ですが、運用中にあるフィールドが足された場合は必ずObjectの定義をアプリケーション間で同時に変更しなければなりません。そうでなければ追加されたフィールドの値をJavaでは取得することができません。
ちなみに、perlなど動的型付け言語ではObject(instance)をMessagePackでシリアライズした時にはフィールド名を含むMap形式でシリアライズされます。つまり、JavaのMessagePackのライブラリが効率を重視した実装であるということです。
そこで、今回は不要なフィールド名はシリアライズせず、仕様変更によりフィールドが変更されやすい項目はMap形式でシリアライズできるように実装してみました。また、JSON形式でも読み書きできることを確認しています。

# 考えられるユースケース

1. アプリケーション間は常に同時に更新可能で効率を重視する場合
    * 通常通り＠Messageの利用か、速度を求めるならTemplateを書く
2. DBの項目が追加されるが、その追加された項目に対しAPIは処理をせずにそのまま返したい場合
    * 今回の記事の内容に該当。処理が必要のない項目をMap<String,Object>で定義する
3. DBの項目が追加されるが、APIは常に固定の項目のみを返したい場合(APIの更新タイミングで項目を追加する)
    * フィールド名のシリアライズに対応する

# ユースケース2の実装

DBの項目が追加されるが、その追加された項目に対しAPIは処理をせずにそのまま返したい場合

仕様変更が想定される項目はMap<String, Object>で格納することを想定します。
そこで、そのObjectをMessagePackに対応するためにObjectTemplateを追加しています。
Objectで定義したため、使用する時にキャストが前提になってしまうデメリットがありますが、JSONにも相互変換できるようにするためにこのようにしています。（この問題をうまくクリアできるならMessagePackのValueの様なオブジェクトを定義したい)

```java:MessagePackTest.class
import static org.msgpack.template.Templates.*;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.msgpack.MessagePack;
import org.msgpack.template.Template;
import org.msgpack.type.Value;

import com.google.gson.Gson;

/**
 * MessagePack0.6とJSONの相互変換のテスト
 */
public class MessagePackTest {

	/**
	 * テスト対象のPOJO
	 */
	public static class TestObj {
		public String str;
		public int i;
		public Child child;
		public Map<String, Object> children; //ObjectはJSONに入る型のみ, 設計が変わる可能性のあるフィールドの格納方式

		public String toString() {
			return "{str:"+str+",i:"+i+",child:"+child+",children:"+children.toString()+"}";
		}
	}

	/**
	 * テスト対象のPOJOの子クラス
	 */
	public static class Child {
		public String child;

		public String toString() {
			return "{"+child+"}";
		}
	}

	/**
	 * main program
	 * @param args 必要なし
	 * @throws Exception 恐らく発生しないexception
	 */
	public static void main(String[] args) throws Exception {

		//テスト対象のオブジェクト生成
		Child child = new Child();
		child.child = "hoge";

		TestObj obj = new TestObj();
		obj.str = "test";
		obj.i = 2;
		obj.child = child;
		obj.children = new HashMap<String, Object>();
		obj.children.put("foo", "bar");
		obj.children.put("piyo", ObjectTemplate.mapObject(child)); //Map<String,Object>には任意のオブジェクトは入れれないのでMapに変換
		obj.children.put("puyo", new String[]{"aaa"});

		//MessagePackの初期化、registerを使うので@Messageのアノテーションの使用はなし
		MessagePack msgpack = new MessagePack();
		msgpack.register(Object.class, ObjectTemplate.getInstance());
		msgpack.register(Child.class);
		msgpack.register(TestObj.class);

		//------------------------------------------------------
		//MessagePackのserialize
		byte[] bytes = msgpack.write(obj);

		//フィールド名がserializeされないことを確認
		System.out.println(bytes.length); //49
		System.out.println(Arrays.toString(bytes)); //[-108, -92, 116, 101, 115, 116, 2, -111, -92, 104, 111, 103, 101, -125, -93, 102, 111, 111, -93, 98, 97, 114, -92, 112, 105, 121, 111, -127, -91, 99, 104, 105, 108, 100, -92, 104, 111, 103, 101, -92, 112, 117, 121, 111, -111, -93, 97, 97, 97]

		Value value = msgpack.read(bytes);
		System.out.println(value); //["test",2,["hoge"],{"foo":"bar","piyo":{"child":"hoge"},"puyo":["aaa"]}]

		//MessagePackのdesirialzie
		TestObj to = msgpack.read(bytes, TestObj.class);
		System.out.println(to); //{str:test,i:2,child:{hoge},children:{foo=bar, piyo={child=hoge}, puyo=[aaa]}}

		//-----------------------------------------------------
		//Map変換後のフィールド名付きのserializeできることを確認
		bytes = msgpack.write(ObjectTemplate.mapObject(obj));

		//フィールド名が付くため圧縮効率は落ちる
		System.out.println(bytes.length); //76
		System.out.println(Arrays.toString(bytes)); //[-124, -93, 115, 116, 114, -92, 116, 101, 115, 116, -88, 99, 104, 105, 108, 100, 114, 101, 110, -125, -93, 102, 111, 111, -93, 98, 97, 114, -92, 112, 105, 121, 111, -127, -91, 99, 104, 105, 108, 100, -92, 104, 111, 103, 101, -92, 112, 117, 121, 111, -111, -93, 97, 97, 97, -95, 105, 2, -91, 99, 104, 105, 108, 100, -127, -91, 99, 104, 105, 108, 100, -92, 104, 111, 103, 101]

		value = msgpack.read(bytes);
		System.out.println(value); //{"str":"test","children":{"foo":"bar","piyo":{"child":"hoge"},"puyo":["aaa"]},"i":2,"child":{"child":"hoge"}}

		//MessagePackのdesirialzie, static importを使うことに注意
		Template<Map<String, Object>> mapTmpl = tMap(TString, ObjectTemplate.getInstance());
		Map<String, Object> mo = msgpack.read(bytes, mapTmpl);

		System.out.println(mo); //{str=test, i=2, children={foo=bar, piyo={child=hoge}, puyo=[aaa]}, child={child=hoge}}

		//-----------------------------------------------------
		//このオブジェクトがJSONでもserialize/desirializeできることを確認
		Gson gson = new Gson();

		//Map版も順番は違うが同じオブジェクトになることを確認
		String json = gson.toJson(to);
		String json2 = gson.toJson(mo);
		System.out.println(json); //{"str":"test","i":2,"child":{"child":"hoge"},"children":{"foo":"bar","piyo":{"child":"hoge"},"puyo":["aaa"]}}
		System.out.println(json2); //{"str":"test","i":2,"children":{"foo":"bar","piyo":{"child":"hoge"},"puyo":["aaa"]},"child":{"child":"hoge"}}

		TestObj to2 = gson.fromJson(json, TestObj.class);
		@SuppressWarnings("unchecked")
		Map<String, Object> mo2 = (Map<String, Object>)gson.fromJson(json2, Map.class);

		System.out.println(to2); //{str:test,i:2,child:{hoge},children:{foo=bar, piyo={child=hoge}, puyo=[aaa]}}
		System.out.println(mo2); //{str=test, i=2.0, children={foo=bar, piyo={child=hoge}, puyo=[aaa]}, child={child=hoge}}
	}

}

```

```java:ObjectTemplate.class
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.msgpack.MessageTypeException;
import org.msgpack.packer.Packer;
import org.msgpack.template.AbstractTemplate;
import org.msgpack.unpacker.Unpacker;


/**
 * POJO系のObjectのTemplate
 *
 */
public class ObjectTemplate extends AbstractTemplate<Object> {

	/** Singletonのinstance */
	private static final ObjectTemplate INSTANCE = new ObjectTemplate();

	/** private constructor */
	private ObjectTemplate() {
	}

	/** get singleton instance */
	public static ObjectTemplate getInstance() {
		return INSTANCE;
	}

	/**
	 * pojoをmap形式のオブジェクトに変換する
	 * @param obj pojo object
	 * @return map object
	 */
	@SuppressWarnings("unchecked")
	public static Map<String, Object> mapObject(Object obj) {
		Map<String, Object> map;
		if(obj instanceof Map) {
			map = (Map<String, Object>)obj;
		} else {
			map = new HashMap<String, Object>();
			for (Field field : obj.getClass().getFields()) {
				int mod = field.getModifiers();
				if (Modifier.isPublic(mod) && !Modifier.isStatic(mod)
						&& !Modifier.isTransient(mod)) {
					try {
						Object val = field.get(obj);
						if (!(val instanceof Number || val instanceof Boolean || val instanceof String)) {
							val = mapObject(val);
						}
						map.put(field.getName(), val);
					} catch (IllegalArgumentException | IllegalAccessException e) {
						// 発生しないはず
						throw new RuntimeException(e);
					}
				}
			}
		}
		return map;
	}

	@Override
	public void write(Packer paramPacker, Object paramT, boolean paramBoolean)
			throws IOException {
		if(paramT==null) {
			if(paramBoolean) {
				throw new MessageTypeException("Attempted to write null");
			}
			paramPacker.writeNil();
			return;
		} else {
			if(paramT instanceof Number ||
					paramT instanceof Boolean ||
					paramT instanceof String) {
				paramPacker.write(paramT);
			} else {
				paramPacker.write(paramT);
			}
		}

	}

	@Override
	public Object read(Unpacker paramUnpacker, Object paramT,
			boolean paramBoolean) throws IOException {
		if (!paramBoolean && paramUnpacker.trySkipNil()) {
			return null;
		}
		Object obj = readObject(paramUnpacker);
		return obj;
	}

	/**
	 * 型をチェックしながらMessagePackのunpackを行う
	 * @param paramUnpacker unpacker
	 * @return object
	 * @throws IOException exception
	 */
	private Object readObject(Unpacker paramUnpacker) throws IOException {
		Object obj = null;
		int size;
		switch(paramUnpacker.getNextType()) {
		case NIL:
			paramUnpacker.readNil();
			obj = null;
			break;
		case ARRAY :
			//他のPOJOもここに入るので何かしらの対策が必要(なんのオブジェクトかはデータからは判断できない)
			size = paramUnpacker.readArrayBegin();
			ArrayList<Object> list = new ArrayList<>(size);
			for(int i=0;i<size;i++) {
				Object val = readObject(paramUnpacker);
				list.add(val);
			}
			paramUnpacker.readArrayEnd();
			obj = list;
			break;
		case BOOLEAN :
			obj = paramUnpacker.readBoolean();
			break;
		case FLOAT :
			obj = paramUnpacker.readFloat();
			break;
		case INTEGER :
			obj = paramUnpacker.readInt();
			break;
		case MAP :
			size = paramUnpacker.readMapBegin();
			HashMap<String,Object> map = new HashMap<>();
			for(int i=0;i<size;i++) {
				String key = paramUnpacker.readString();
				Object val = readObject(paramUnpacker);
				map.put(key, val);
			}
			paramUnpacker.readMapEnd();
			obj = map;
			break;
		case RAW :
			obj = paramUnpacker.readString();
			break;
		default:
			break;
		}
		return obj;
	}

}

```

```xml:pom.xml
	<dependencies>
		<dependency>
			<groupId>org.msgpack</groupId>
			<artifactId>msgpack</artifactId>
			<version>0.6.12</version>
		</dependency>
		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
			<version>2.3.1</version>
		</dependency>
	</dependencies>
```

# ユースケース3の実装

DBの項目が追加されるが、APIは常に固定の項目のみを返したい場合(APIの更新タイミングで項目を追加する)

フィールド名を含めシリアライズするようにします。
また、ただフィールド名をシリアライズすると圧縮率が低下するため、明示的にアノテーションを与えることで数値でフィールド名を圧縮するオプションを実装してみました。
今回、WildcardType(List<? extnds Number>など)の実装などは私の理解度が低かったので考慮していません。また、リフレクションのオーバーヘッドなどもうまく考慮できていないのであくまでも暫定的な実装例となります。

```java:MessagePackTest2
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.msgpack.MessagePack;
import org.msgpack.type.Value;

import com.google.gson.Gson;


/**
 * MessagePackをフィールド名有りでシリアライズするテスト
 *
 */
public class MessagePackTest2 {

	/**
	 * テスト対象のPOJO
	 */
	public static class TestObj {
		@CompressField(1)
		public String str;

		@CompressField(2)
		public int i;

		//@CompressField(3) //一部をわざと圧縮しないで置く
		public String[] strary;

		@CompressField(4)
		public List<String> strlist;

		@CompressField(5)
		public Map<String,Boolean> boolmap;

		@CompressField(6)
		public Child child;
	}

	/**
	 * TestObjの仕様変更版
	 *
	 */
	public static class AdvTestObj {
		public String str;

		public int i;

		public boolean additionalField;
	}

	/**
	 * テスト対象のPOJOの子クラス
	 */
	public static class Child {

		@CompressField(1)
		public String child;
	}

	/**
	 * main program
	 * @param args 必要なし
	 * @throws Exception 恐らく発生しないexception
	 */
	public static void main(String[] args) throws Exception {
		TestObj obj = new TestObj();
		obj.str = "test";
		obj.i = 2;
		obj.strary = new String[]{"aa"};
		obj.strlist = new ArrayList<String>();
		obj.strlist.add("bb");
		obj.boolmap = new HashMap<String,Boolean>();
		obj.boolmap.put("hoge", false);

		Child child = new Child();
		child.child = "hoge";

		obj.child = child;

		// Map形式でシリアライズする
		MessagePack msgpack = new MessagePack();
		msgpack.register(Child.class, new MapObjectTemplate<Child>(Child.class));
		msgpack.register(TestObj.class, new MapObjectTemplate<TestObj>(TestObj.class));

		byte[] bytes = msgpack.write(obj);

		System.out.println(bytes.length); // 69
		System.out.println(Arrays.toString(bytes)); // [-122, -93, 115, 116, 114, -92, 116, 101, 115, 116, -95, 105, 2, -90, 115, 116, 114, 97, 114, 121, -111, -94, 97, 97, -89, 115, 116, 114, 108, 105, 115, 116, -111, -94, 98, 98, -89, 98, 111, 111, 108, 109, 97, 112, -127, -92, 104, 111, 103, 101, -62, -91, 99, 104, 105, 108, 100, -127, -91, 99, 104, 105, 108, 100, -92, 104, 111, 103, 101]

		Value value = msgpack.read(bytes);
		System.out.println(value); // {"str":"test","i":2,"strary":["aa"],"strlist":["bb"],"boolmap":{"hoge":false},"child":{"child":"hoge"}}

		TestObj to = msgpack.read(bytes, TestObj.class);
		System.out.println(new Gson().toJson(to)); // {"str":"test","i":2,"strary":["aa"],"strlist":["bb"],"boolmap":{"hoge":false},"child":{"child":"hoge"}}


		// 通常との圧縮率の比較
		msgpack = new MessagePack();
		msgpack.register(Child.class);
		msgpack.register(TestObj.class);

		bytes = msgpack.write(obj);

		System.out.println(bytes.length); // 28
		System.out.println(Arrays.toString(bytes)); // [-106, -92, 116, 101, 115, 116, 2, -111, -94, 97, 97, -111, -94, 98, 98, -127, -92, 104, 111, 103, 101, -62, -111, -92, 104, 111, 103, 101]

		value = msgpack.read(bytes);
		System.out.println(value); // ["test",2,["aa"],["bb"],{"hoge":false},["hoge"]]


		// フィールド名をアノテーションで圧縮する場合
		msgpack = new MessagePack();

		msgpack.register(Child.class, new MapObjectTemplate<Child>(Child.class, true));
		msgpack.register(TestObj.class, new MapObjectTemplate<TestObj>(TestObj.class, true));

		bytes = msgpack.write(obj);

		System.out.println(bytes.length); // 41
		System.out.println(Arrays.toString(bytes)); // [-122, 1, -92, 116, 101, 115, 116, 2, 2, -90, 115, 116, 114, 97, 114, 121, -111, -94, 97, 97, 4, -111, -94, 98, 98, 5, -127, -92, 104, 111, 103, 101, -62, 6, -127, 1, -92, 104, 111, 103, 101]

		value = msgpack.read(bytes);
		System.out.println(value); // {1:"test",2:2,"strary":["aa"],4:["bb"],5:{"hoge":false},6:{1:"hoge"}}


		// 仕様変更があるオブジェクトが来た場合、不要なフィールドは無視する
		AdvTestObj obj2 = new AdvTestObj();
		obj2.str = "test";
		obj2.i = 2;
		obj2.additionalField = true;

		msgpack = new MessagePack();

		msgpack.register(Child.class, new MapObjectTemplate<Child>(Child.class, true));
		msgpack.register(AdvTestObj.class, new MapObjectTemplate<AdvTestObj>(AdvTestObj.class));
		msgpack.register(TestObj.class, new MapObjectTemplate<TestObj>(TestObj.class));

		bytes = msgpack.write(obj2);

		System.out.println(bytes.length);
		System.out.println(Arrays.toString(bytes));

		value = msgpack.read(bytes);
		System.out.println(value);

		TestObj obj3 = msgpack.read(bytes, TestObj.class);
		System.out.println(new Gson().toJson(obj3));
	}
}
```

```java:MapObjectTemplate.java
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.msgpack.MessageTypeException;
import org.msgpack.packer.Packer;
import org.msgpack.template.AbstractTemplate;
import org.msgpack.type.ValueType;
import org.msgpack.unpacker.Unpacker;


/**
 * 通常のリスト形式でなくMap形式でシリアル・デシリアライズする
 *
 * @param <T> 対象のクラス
 */
public class MapObjectTemplate<T> extends AbstractTemplate<T> {

	private Class<T> clazz;

	private boolean isCompress;

	private Map<Integer,Field> compressIndex;

	/** private constructor */
	public MapObjectTemplate(Class<T> clazz) {
		this(clazz, false);
	}

	/** private constructor */
	public MapObjectTemplate(Class<T> clazz, boolean isCompress) {
		this.clazz = clazz;
		this.isCompress = isCompress;

		if (isCompress) {
			compressIndex = new TreeMap<Integer,Field>();
			for(Field field:clazz.getFields()) {
				int mod = field.getModifiers();
				if(!Modifier.isPublic(mod) || Modifier.isStatic(mod) || Modifier.isTransient(mod)) {
					continue;
				}
				CompressField cfield = field.getAnnotation(CompressField.class);
				if (cfield==null) {
					continue;
				}
				compressIndex.put(cfield.value(), field);
			}
		}
	}

	@Override
	public void write(Packer paramPacker, T paramT, boolean paramBoolean)
			throws IOException {
		if(paramT==null) {
			if(paramBoolean) {
				throw new MessageTypeException("Attempted to write null");
			}
			paramPacker.writeNil();
			return;
		} else {
			Field[] fields = paramT.getClass().getFields();
			paramPacker.writeMapBegin(fields.length);
			for(Field field:fields) {
				int mod = field.getModifiers();
				if(Modifier.isPublic(mod) && !Modifier.isStatic(mod) && !Modifier.isTransient(mod)) {
					try {
						CompressField cfield;
						if (isCompress && (cfield = field.getAnnotation(CompressField.class)) != null) {
							// 圧縮オプションがある場合、数値でフィールド名を表現する
							paramPacker.write(cfield.value());
						} else {
							paramPacker.write(field.getName());
						}
						paramPacker.write(field.get(paramT));
					} catch (IllegalArgumentException | IllegalAccessException e) {
						throw new MessageTypeException("Illegal Exception");
					}
				}
			}
			paramPacker.writeMapEnd(paramBoolean);
		}
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	@Override
	public T read(Unpacker paramUnpacker, T paramT,
			boolean paramBoolean) throws IOException {
		T obj = null;

		try {
			obj = clazz.newInstance();
			int size = paramUnpacker.readMapBegin();
			for(int i=0;i<size;i++) {
				try {
					Field field;
					if (isCompress && paramUnpacker.getNextType()==ValueType.INTEGER) {
						field = compressIndex.get(paramUnpacker.readInt());
						if (field == null) {
							// skip value (フィールド名が存在しない場合)
							paramUnpacker.skip();
							continue;
						}
					} else {
						field = clazz.getField(paramUnpacker.readString());
					}

					Object val = null;

					if (paramUnpacker.getNextType() == ValueType.NIL) {
						paramUnpacker.readNil();
						continue;
					}

					Type type = field.getGenericType();

					if(type instanceof Class) {
						val = paramUnpacker.read(field.getType());
					} else if(type instanceof ParameterizedType) {
						// ListやMapの場合
						ParameterizedType pt = (ParameterizedType) type;
						Type[] types = pt.getActualTypeArguments();
						if(paramUnpacker.getNextType()==ValueType.MAP) {
							int len = paramUnpacker.readMapBegin();
							Map map = new LinkedHashMap();
							for(int j=0;j<len;j++) {
								Object key = paramUnpacker.read((Class)types[0]);
								Object value = paramUnpacker.read((Class)types[1]);
								map.put(key, value);
							}
							paramUnpacker.readMapEnd();
							val = map;
						} else if(paramUnpacker.getNextType()==ValueType.ARRAY){
							int len = paramUnpacker.readArrayBegin();
							List list = new ArrayList<>(len);
							for(int j=0;j<len;j++) {
								list.add(paramUnpacker.read((Class)types[0]));
							}
							paramUnpacker.readArrayEnd();
							val = list;
						}
					}
					field.set(obj, val);
				} catch (NoSuchFieldException | SecurityException e) {
					// skip value (フィールド名が存在しない場合)
					paramUnpacker.skip();
				}
			}
		} catch (InstantiationException | IllegalAccessException e) {
			throw new MessageTypeException("Instantiation, Illegal Exception");
		}

		return obj;
	}

}
```

```java:CompressField.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * フィールド名を数値で圧縮する
 * 番号はそのオブジェクトで一意である必要がある
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface CompressField {
	int value();
}
```

# コード公開

Apache License 2.0でgithubにコードをあげています。

https://github.com/ksgwr/MessagePackTemplate

# 編集履歴

* 2015/08/15 初稿
* 2015/08/23 文章校正、ユースケースの追加
* 2015/08/30 ユースケース3の実装を追加
* 2015/09/21 ライセンスを付けてコード公開