Serializableの意味わかりますか?
Java でWebアプリケーションを作ると、以下のような JavaBeans クラスを作ることがあると思います。
import java.io.Serializable;
public class JavaBeans implements Serializable {
private static final long serialVersionUID = 1L;
private String aaa = null;
public String getAaa() {
return aaa;
}
public void setAaa(String aaa) {
this.aaa = aaa;
}
}
このimplements Serializable
とserialVersionUID = 1L;
の意味わかりますか?
「わかりません!」っていう人向けに書いた記事です。「わかるよ!」って人はこの記事を読む必要ないです。
まずは概念から
Javaのインスタンスをバイト配列として出力することをSerialize(シリアライズ)と言い、その逆をDeserializeと言います。下の図がわかりやすいのですが、ファイルやメモリ、データベースなどにインスタンスを保存することをSerializeと言っています。
図はこの記事から抜粋
なのでimplements Serializable
と宣言するということは、「このインスタンスはdiskなどに保存することができます!」って宣言していることになります。
ちょっと個人的に作ったWebアプリケーションがあるので、それで実験してみます。
public class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
// ・・・
}
上記TestServlet
はHttpServlet
を extends しており、HttpServlet
はGenericServlet
を extends、GenericServlet
はSerializable
を implements しているので、TestServlet
はSerializable
です。
このWebアプリケーションのサーバ(Tomcat)を起動します。デプロイ先のlocalhostフォルダ直下にはまだ何もありません。
この後、TestServlet
が動くようにブラウザを開くか何かして、サーバーを停止します。すると、デプロイ先のlocalhostフォルダ直下にSESSIONS.ser
という謎のファイルが出来上がります。
再度サーバーを起動します。すると、SESSIONS.ser
は消えます。
SESSIONS.ser
はTomcatがセッション情報をSerializeしたものです。これのおかげで、サーバが再起動されてもセッション情報は失われず、再起動後にアクセスすることで再起動前の情報を復元(Deserialize)することができます。逆に言えば、この仕様のために、セッションに格納するオブジェクトはSerializableをimplementsしている必要があります。
一見便利そうなんですけど、これ、再起動前後でプログラムが変更された場合にどうなるかわかりますか?
実際にSerialize/Deserializeさせてみる
下図のような構成のアプリケーションを作り、実際にSerialize/Deserializeさせてみます。
まずSerializeする方です。任意のフォルダにSampleBean.java
と、それをSerializeするWriteObject.java
を作ります。WriteObject.javaはSampleBeanインスタンスを生成し、2つのプロパティに100を設定してそれをSerializeしています。WriteObjectを実行することでsample.ser
を作ります。sample.serはSampleBeanインスタンスをSerializeしたものです。
import java.io.Serializable;
public class SampleBean implements Serializable {
private static final long serialVersionUID = 1L;
private double propertyD;
private int propertyE;
public double getPropertyD() {
return propertyD;
}
public void setPropertyD(double propertyD) {
this.propertyD = propertyD;
}
public int getPropertyE() {
return propertyE;
}
public void setPropertyE(int propertyE) {
this.propertyE = propertyE;
}
}
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class WriteObject {
public static void main (String args[]) {
SampleBean sample = new SampleBean();
sample.setPropertyD(100);
sample.setPropertyE(100);
try {
FileOutputStream fout = new FileOutputStream("sample.ser");
ObjectOutputStream oos = new ObjectOutputStream(fout);
oos.writeObject(sample);
oos.close();
System.out.println("Done");
} catch(Exception ex) {
ex.printStackTrace();
}
}
}
次にDeserializeする方です。
上記で生成したsample.ser
を読み込むクラス(JspTest.java)を作ります。
JspTest.javaで使用しているSampleBeanクラスは、先ほどのものと中身は同じものを別のフォルダにコピーして使用しています。
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/JspTest")
public class JspTest extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
SampleBean sample = null;
List<SampleBean> list = new ArrayList<>();
try {
FileInputStream fin = new FileInputStream("c:\\sample.ser");
ObjectInputStream ois = new ObjectInputStream(fin);
sample = (SampleBean) ois.readObject();
ois.close();
list.add(sample);
} catch(Exception ex) {
ex.printStackTrace();
}
request.setAttribute("samplelist", list);
String path = "/WEB-INF/jsp/jsptest.jsp";
RequestDispatcher disp = request.getRequestDispatcher(path);
disp.forward(request, response);
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
それを表示するjsp
<%@ page language="java" contentType="text/html;charset=Windows-31J"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ page isELIgnored="false" %>
<html><body>
<table border="1">
<thead>
<tr>
<th>プロパティD</th>
<th>プロパティE</th>
</tr>
</thead>
<tbody>
<c:forEach items="${samplelist}" var="item">
<tr>
<td><c:out value="${item.propertyD}" /></td>
<td><c:out value="${item.propertyE}" /></td>
</tr>
</c:forEach>
<tbody>
</table>
</body></html>
SerializeされたインスタンスをDeserializeしてそれをブラウザに表示することができました。
これがSerialize/Deserializeです。
serialVersionUIDとは?
次はserialVersionUID
です。クラスに implements Serializable と書き、serialVersionUID を宣言しなかった場合、The serializable class XXXX does not declare a static final serialVersionUID field of type long
というワーニングが出てきます。
serialVersionUID
とは、インスタンスのバージョンです。と言われてもあまりピンとこないと思うので、もっと具体的に理解するために、少し実験をしてみます。
先ほどSerializeした時に使用したSampleBean.javaと、それをDeserializeした時に使用したSampleBean.javaのserialVersionUIDを不一致にさせてみます。
JspTest.javaが使用するSampleBean.java(Deserializeする方)をserialVersionUID = 2L;
に変更します。SerializeされたSampleBean.javaはserialVersionUID = 1L;
のままです。
変更した後、ブラウザをF5で更新すると以下のようなエラーが出力されます。
java.io.InvalidClassException: SampleBean; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:715)
at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1997)
at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1866)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2159)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1685)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:499)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:457)
at JspTest.doGet(JspTest.java:27)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:690)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.base/java.lang.Thread.run(Thread.java:832)
InvalidClassException
というエラーが発生し、その原因はSampleBeanのserialVersionUID が不一致だということがわかります。
逆にserialVersionUID は同じ値だけど中身が違う場合はどうなるでしょうか?
Serializeする方のSampleBeanは変更せず、Deserializeする方だけ以下のように、getterの中で×100してみます(getPropertyE()の中)。
import java.io.Serializable;
public class SampleBean implements Serializable {
private static final long serialVersionUID = 1L;
private double propertyD;
private int propertyE;
public double getPropertyD() {
return propertyD;
}
public void setPropertyD(double propertyD) {
this.propertyD = propertyD;
}
public int getPropertyE() {
return propertyE * 100; // getterの中で×100してみる
}
public void setPropertyE(int propertyE) {
this.propertyE = propertyE;
}
}
エラーは発生せず、×100した値が表示されていることがわかります。正常終了はしたものの、この挙動は本当にあなたが想定した挙動ですか?Serializeした時とDeserializeした時でプログラムの中身が変更されていた場合の挙動を考慮したことがありますか?
Deserializeで復元しているのは、インスタンス変数の値です。Serializeした時に、propertyDにはdouble型の100、propertyEにはint型の100を設定しているので、Deserializeではその値を復元しています。setter/getterの中で何をしているかなんて気にしていません。
なので、例えばsetterの中に、引数の値が10以上の場合はエラーとするようなチェックを入れたとしても、それを無視した値が復元されます。
import java.io.Serializable;
public class SampleBean implements Serializable {
private static final long serialVersionUID = 1L;
private double propertyD;
private int propertyE;
public double getPropertyD() {
return propertyD;
}
public void setPropertyD(double propertyD) {
// 引数の値が10以上の場合はエラー
if (propertyD > 10) {
throw new IllegalArgumentException();
}
this.propertyD = propertyD;
}
public int getPropertyE() {
return propertyE;
}
public void setPropertyE(int propertyE) {
this.propertyE = propertyE;
}
}
setterで10以上ならエラーというチェックを入れたにも関わらず、特にエラーもなく表示できてしまいます。
この記事の最初の方で、セッション情報がSerialize/Deserializeされているという話をしました。Webサーバがセッション情報をSerializeするのは以下のような場合です。
- サーバを停止した時
- セッションを格納する領域(メモリ)が満杯になった時
Deserializeするのはこの逆のタイミングです。SerializeされてからDeserializeされるまでの間にプログラムが変更されていたとしても、その変更内容を無視してDeserializeされてしまいます。それが困るようであればserialVersionUIDを変更する必要があります。
Serialize/DeserializeのJavaの仕様は以下にまとめられています。
5.6 直列化に影響する型変更
特に意識してほしいのは「5.6.2 互換性のある変更」の方で、Javaの仕様として互換性がある(=Deserialize可能)だとしても、実際に使用しているそのシステムにおいて互換性があるかどうかはまた別の話です。Javaの仕様として互換性がある場合に、それは本当にDeserializeさせて大丈夫なのか?改めて画面を操作させないと変なことが起こるようなのであれば、serialVersionUIDを変更してエラーを発生させた方が安全な場合もあると思います。
serialVersionUIDの変更方法
自分の手で変更することも可能ですが、IDEに生成させることも可能です。Eclipseであれば以下のような手順になります。
(IntelliJ IDEAを使用している方はこちら)
クラスからprivate static final long serialVersionUID = 1L;
をいったん消します。するとクラスでワーニングが出るのでそこにカーソルを合わせる(下図)。
private static final long serialVersionUID = 2779728212248637579L;
というserialVersionUIDが生成されました。この値はクラスの構造が変わると生成される値も変更されます。クラスの構造とは、インスタンス変数の追加や変数名の変更、メソッドの追加、・・・などです。
transientでSerialize/Deserializeの対象外とする
インスタンス変数にtransient修飾子を付与することで、Serialize/Deserializeの対象外とすることができます。試しに以下のようにpropertyEにtransient修飾子を付与してみます。
import java.io.Serializable;
public class SampleBean implements Serializable {
private static final long serialVersionUID = 4798282072280430232L;
private double propertyD;
private transient int propertyE; // transientを付与
public double getPropertyD() {
return propertyD;
}
public void setPropertyD(double propertyD) {
this.propertyD = propertyD;
}
public int getPropertyE() {
return propertyE;
}
public void setPropertyE(int propertyE) {
this.propertyE = propertyE;
}
}
Serializeする際には100を設定します。
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class WriteObject {
public static void main (String args[]) {
SampleBean sample = new SampleBean();
sample.setPropertyD(100);
sample.setPropertyE(100);
try {
FileOutputStream fout = new FileOutputStream("sample.ser");
ObjectOutputStream oos = new ObjectOutputStream(fout);
oos.writeObject(sample);
oos.close();
System.out.println("Done");
} catch(Exception ex) {
ex.printStackTrace();
}
}
}
ここで生成したsample.serを読み込ませてブラウザ表示した結果が以下です。
propertyEにtransientを付けたことで、Serializeの際に設定した100という値がDeserializeした時には設定されていないことがわかります。0が表示されているのはintのデフォルト値が0だからです。
ちなみにtransientを付けるとIDEが生成するserialVersionUID の値も変わります。
おわりに
ざっとこんな感じです。私もよくわかっていない部分がありますが、私の周囲でこのあたりのことをちゃんと理解した上でimplements Serializable
とかserialVersionUID = 1L
を書いている人があまりいないような気がしたので書いてみました。この記事を読んで少しでも多くの方が、Serializeの意味をちゃんと理解した上でimplements Serializable
とかserialVersionUID = 1L
と書いてくれようになってくれたらいいなと思います。
以上。
参考
公式
Serialize:シリアライズ(直列化)とDeserialize:デシリアライズ
- 直列化(serialize:シリアライズ)
- Serialization and Deserialization in Java
- Serializable について
- Javaのシリアライズ(直列化)メモ
- 難解なSerializableという仕様について俺が知っていること、というか俺の理解
- Javaとシリアライズと互換性
Serializeとセッション
- 3.1.14 セッションリカバリ機能を利用する場合のWebアプリケーション作成方法
- セッションに保存されるオブジェクトがシリアライズ可能でない
- sessionに登録するObjectはSerializableしなければならない
- [JAVA] フェイルオーバー時や再起動時に NullPointerException や Cannot get property ‘xxxxxx’ on null object
serialVersionUID
Tomcatのセッション管理