Serializable について

# はじめに

JAVAの基本的なお話ですが、
昨今のJAVAではシリアライズを意識しているレイヤについては開発者は殆ど手を触れないので、
知らない人も多いと思ったので取り上げました。

JAVAを触って2ヶ月くらいの時の話です。

```java
public abstract class ActionForm implements Serializable {
```

なんてコードがあった。
_「？？？ `Serializable` って、何だろ？」_

ちょっと調べてみた。中身はこんなの。

```java:java.io.Serializable
package java.io;
public interface Serializable {
}
```

私の最初の感想は _「何これ？」_ でした。
中身の無いInterfaceに意味ってあるの？

今回の話は `Serializable` の使用例がメインとなります。
知らない人多いと思ってますが、
_「こんなの常識だよ！皆知っているよ！」_
ってな感じで認識違ってたらゴメンナサイ
 ('・ω・)

# んでSerializableって何？
解らないワードはとりあえずググります。
Javaの仕様見なくても良いのだから良い時代ですね。
んで、 `Serializable` をググると納得。

```txt:wikipeia
シリアライズ可能なクラスの実装[編集]
Java言語では直列化したいクラスに Serializable または Externalizable インタフェースを実装することで、
そのクラスのオブジェクトは直列化できるクラスを作ることができる。
シリアライズ - Wikipedia
https://ja.wikipedia.org/wiki/シリアライズ
```

これを読んでも仕組みはピンと来なかったけど、
何が出来るのかとりあえず解った。
んで、遊んでみると非常に面白かった。

# Serializableの使い方

SerializableはC言語を触った事がある人なら理解しやすいと思いますが、
_「バイト配列に変換できますよ」_ って *マーカー* です。

C言語では構造体をそのままファイルシステムに保存したことがあるかと思います。
要素がメモリ上に直列で並んでいることが明確だったので、
こちらは直感的にやっている人は多かったと思います。
(ポインタ+バイト長して次の要素へアクセスとか。。。)
JAVAでは明示的にSerializableのマーカが指定されたクラスのみが、
バイト配列に変換可能になります。
つまり、インスタンスをファイルに保存し、
復元することが可能となります。

## 一度ファイルに書き込み、再度読み込むサンプル
一度ファイルに書き込んだインスタンスを復元して返すだけの処理

```java
	public static Object fileOutputStream(String fileName, Object obj) throws Exception {
		File file = new File(fileName);
		ObjectOutputStream os = null;
		try {
			os = new ObjectOutputStream(new FileOutputStream(file));
			// ここでインスタンスをファイルに書込
			os.writeObject(obj);
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			if (os != null) {
				try {
					os.close();
				} catch (IOException e1) {
					e1.printStackTrace();
				}
			}
		}

		ObjectInputStream is = null;
		try {
			is = new ObjectInputStream(new FileInputStream(file));
			// 上記コードで書き込んだインスタンスを復元する
			return is.readObject();
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException e1) {
					e1.printStackTrace();
				}
			}
		}
	}
```

# Serializable使用上の注意点

`Serializable` 自体に変換する機能とか無いので、
あくまでバイト配列に出来ますよって宣言だけで、
変換できない場合は `java.io.NotSerializableException` 例外が発生します。
`transient` の修飾子を変換不可の要素に付与すれば対象外となり、
例外は発生しません。

# こんなことも出来るSerializable

ファイルにインスタンスを保存・復元するについてですが、
クラス情報とフィールド値を保持しています。
クラスとは処理と値情報の集まりであるため、
復元したインスタンスは当然処理の実行が可能となります。
つまり、処理をクラスを連携する事により、
非同期処理や負荷分散といった事を同一インスタンスにて、
開発者は意識せず実装する事ができます。

バイト配列による保存や連携が可能であればなんでも使えます。
ファイル・メッセージサービス・DBなんでもござれ。

イメージとしては以下のような感じ。
インスタンス毎連携して他プロセスでそのまま処理する。
![非同期処理.png](https://qiita-image-store.s3.amazonaws.com/0/101761/53af0dc5-0e58-70c1-6ddc-833a6533bf5d.png)


# 実際のサンプル

以下３つのプロジェクトを用意
処理クラス：実際に実行する処理を記載するパッケージ
同期/非同期をディスパッチするクラス：引数によりモード(即時実行/別プロセス実行)を指定する
非同期処理クラス：別プロセスとして連携されたインスタンスを実行する。

## 処理クラス
初期化とパラメータ検証とメイン処理を持つクラスを作成
時間がかかりそうな素因数分解の処理。
当然Serializableは付ける。

```java
package com.hoge.execute.impl;

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

import com.hoge.execute.IExecuter;

public class SimpleExecuter implements IExecuter, Serializable {

	/**
	 *
	 */
	private static final long serialVersionUID = 1L;

	private String[] params;
	private long longVal;

	@Override
	public void init(String... params) {
		System.out.println("初期化処理");
		for (String param : params) {
			System.out.println("param:" +  param);
		}
		this.params = params;
	}

	@Override
	public void validate() {
		System.out.println("パラメータの妥当性検証");
		if (params.length != 3) {
			throw new RuntimeException("パラメータ数不正：Mode Class Value");
		}
		if (params[2].matches("^[0-9]+$")) {
			longVal = Long.parseLong(params[2]);
		} else {
			throw new RuntimeException("パラメータ型不正：第３引数は数値型です。");
		}
	}

	@Override
	public void execute() {
		System.out.println("処理実行");
		List<Long> result = calc(longVal);
		StringBuilder sb = new StringBuilder(longVal + " = ");
		for (Long val : result) {
			sb.append(val).append(" * ");
		}
		System.out.println(sb.delete(sb.length() - 3, sb.length()));
	}

	/**
	 * 引数で与えられた値を素因数分解する
	 * @param val 値
	 * @return 結果のLIST
	 */
	private List<Long> calc(Long val) {
		List<Long> result = new ArrayList<Long>();
		Long num = 2L;
		while (val != 1)
		if (val % num == 0) {
			result.add(num);
			val = val / num;
			num = 2L;
		} else {
			num++;
		}
		return result;
	}
}
```

## 同期/非同期をディスパッチするクラス
init(初期化)とvalidate(パラメータチェック)を実行後、
args[0]により

+ メイン処理即時実行
+ インスタンスのファイル保存
+ インスタンスのDB保存

何れかのモードで起動。

```java
package com.hoge;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;

import com.hoge.consts.Constant;
import com.hoge.db.DBConnection;
import com.hoge.execute.IExecuter;

public class Main {

	public static void main(String[] args) {
		if (args == null || args.length < 2) {
			System.out.println("実行モードと実行クラス名は必須です。");
			System.exit(0);
		}
		if (!Constant.MODE.contains(args[0])) {
			System.out.println("実行モードが不正です。");
			System.exit(0);
		}
		IExecuter exec = null;
		try {
			exec = (IExecuter) Class.forName(Constant.PACKAGE_NAME + args[1]).newInstance();
		} catch (Exception e) {
			System.out.println("実行クラスが存在しません。");
			System.exit(0);
		}
		// インスタンスの初期化を行う
		exec.init(args);
		// パラメータの妥当性検証を行う
		exec.validate();
		if (Constant.MODE.get(Constant.MODE_SYNC).equals(args[0])) {
			// 即時実行の場合executeを実行する。
			exec.execute();
		} else if (Constant.MODE.get(Constant.MODE_FILE_ASYNC).equals(args[0])) {
			// ファイルを用いた非同期処理の場合、インスタンスをファイルに書き込む
			fileOutputStream(args[1] + "_" + (new Date()).getTime(), exec);
		} else if (Constant.MODE.get(Constant.MODE_DB_ASYNC).equals(args[0])) {
			// DBを用いた非同期処理の場合、インスタンスをDBに書き込む
			insertObject(exec);
		} else {
			// NP
			throw new IllegalArgumentException();
		}
	}

	/**
	 * ファイルにインスタンスを書き込む。
	 * @param fileName ファイル名
	 * @param exec 実行インスタンス
	 */
	private static void fileOutputStream(String fileName, IExecuter exec) {
		File tmpFile = new File(fileName);
		File file = new File(Constant.FILE_ROOT + fileName);
		ObjectOutputStream os = null;
		try {
			os = new ObjectOutputStream(new FileOutputStream(tmpFile));
			os.writeObject(exec);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}	finally {
			if (os != null) {
				try {
					os.close();
				} catch (IOException e1) {
					e1.printStackTrace();
				}
			}
		}
		tmpFile.renameTo(file);
	}

	/**
	 * DBにインスタンスを書き込む
	 * @param exec 実行インスタンス
	 */
	private static void insertObject(IExecuter exec) {
		try {
			Connection con = DBConnection.getInstance().getConnection();
			PreparedStatement ps = con.prepareStatement("INSERT INTO exec (TIME, INSTANCE) values (?, ?)");
			ps.setDate(1, new java.sql.Date(new Date().getTime()));
			ps.setBinaryStream(2, new ByteArrayInputStream(getByteObject(exec)));
			ps.executeUpdate();
			ps.close();
			con.commit();
		} catch(Exception e) {
			e.printStackTrace();
			try {
				DBConnection.getInstance().getConnection().rollback();
			} catch (SQLException e1) {
				e1.printStackTrace();
			}
			throw new RuntimeException(e);
		} finally {
			try {
				DBConnection.getInstance().getConnection().close();
			} catch (SQLException e) {
				throw new RuntimeException(e);
			}
		}
	}

	/**
	 * インスタンスをバイト配列に変換
	 * @param obj 変換対象インスタンス
	 * @return
	 */
	public static byte[] getByteObject(Object obj) {
		byte retObject[] = null;
		try {
			ByteArrayOutputStream byteos = new ByteArrayOutputStream();
			ObjectOutputStream objos = new ObjectOutputStream(byteos);
			objos.writeObject(obj);
			objos.close();
			byteos.close();
			retObject = byteos.toByteArray();
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
		return retObject;
	}
}
```

## 非同期処理クラス
### ファイルからインスタンスを復元し、executeを実行する。

ファイルシステムを無限ループで監視して、
処理対象のファイルが見つかったら処理するサーバっぽいの。

```java
	public static void main(String[] args) {
		long bfTime = (new Date()).getTime() + 10000;
		while (true) {
			if (bfTime < (new Date()).getTime()) {
				System.out.println("待機中...");
				bfTime = (new Date()).getTime() + 10000;
			}
			File file = getFirstFile(new File(Constant.FILE_ROOT));
			if (file != null) {
				IExecuter exec = fileInputStream(file);
				exec.execute();
				file.delete();
			}
		}
	}

	private static IExecuter fileInputStream(File file) {
		ObjectInputStream is = null;
		try {
			is = new ObjectInputStream(new FileInputStream(file));
			return (IExecuter) is.readObject();
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException e1) {
					e1.printStackTrace();
				}
			}
		}
	}

	private static File getFirstFile(File dir) {

		File[] fileList = dir.listFiles();
		if (fileList.length == 0) {
			return null;
		}
		Arrays.sort(fileList, new Comparator<File>() {
			public int compare(File src, File target) {
				   int diff = src.getName().compareTo(target.getName());
				   return diff;
			}
		});
		if (fileList[0].canRead()) {
			return fileList[0];
		}
		return null;
	}
```

### DBからインスタンスを復元し、executeを実行する。
テーブルを無限ループで監視して、
処理対象のレコードが見つかったら処理するサーバっぽいの。

```java
	public static void main(String[] args) {
		long bfTime = (new Date()).getTime() + 10000;
		while (true) {
			if (bfTime < (new Date()).getTime()) {
				System.out.println("待機中...");
				bfTime = (new Date()).getTime() + 10000;
			}
			try {
				IExecuter exec = selectExecuter();
				if (exec != null) {
					exec.execute();
				}
				DBConnection.getInstance().getConnection().commit();
			} catch (Exception e) {
				try {
					DBConnection.getInstance().getConnection().rollback();
				} catch (SQLException e1) {
					e1.printStackTrace();
				}
				throw new RuntimeException(e);
			}
		}
	}

	private static IExecuter selectExecuter() {
		IExecuter exec = null;
		java.sql.Date Time = null;
		try {
			Connection con = DBConnection.getInstance().getConnection();
			PreparedStatement ps = con.prepareStatement("SELECT TIME, INSTANCE FROM EXEC WHERE ROWNUM = 1 ORDER BY TIME");
			ps.execute();
			ResultSet rs = ps.getResultSet();
			if (rs.next()) {
				Time = rs.getDate(1);
				Blob blob = rs.getBlob(2);
				ObjectInputStream is  = new ObjectInputStream(blob.getBinaryStream());
				exec = (IExecuter) is.readObject();
				is.close();
			}
			rs.close();
			ps.close();
			ps = con.prepareStatement("DELETE FROM EXEC WHERE TIME = ?");
			ps.setDate(1, Time);
			ps.executeUpdate();
			ps.close();
		} catch(Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
		return exec;
	}
```


# 最後に
EJBとかRMIとか....とか別サーバにて処理を実行する仕組みや製品は色々あるのですが、
中でどんな事をやっているのかを知っておくと嵌った時とか解析しやすくなるかなと思います。

最初の例で記載したStrutsのFormはシリアライズ可能なんで、
StrutsフレームワークのWEBアプリでもフィールド情報にFormを保持して、
時間がかかる処理を終了を待たずにレスポンスを返すとかできたりします。
具体的な用途としては、

- 他システムへの照会処理
- 大量データのDB更新処理

とかで使っていました。

WEBサーバと実行サーバのAPを個別実装する必要がなくなるので、
開発の習熟としては楽になるかなと思います。

一部の開発者がシリアライズ不可のフィールドとか作ったりして、
嵌ってましたが、、、、

あと、メッセージサービスを使った連携の場合は、
パーシステントメッセージにしないとエラー時に解析できなくなったりします。
ご注意を。。。。