Wicket の
以下、java.lang.RuntimeException
が
このjava.lang.RuntimeException
が
この
[1]serialVersionUID を
これは
[2]ユーザビリティが
- 発生エラーの
抜粋 java.lang.RuntimeException: Could not deserialize object from byte[] at org.apache.wicket.serialize.java.JavaSerializer.deserialize(JavaSerializer.java:143) at xyz.monotalk.festivals4partypeople.web.wicket.protocol.http.Festivals4PartyPeopleWicketApplication$4.deserialize(Festivals4PartyPeopleWicketApplication.java:248) at org.apache.wicket.pageStore.AbstractPageStore.deserializePage(AbstractPageStore.java:152) at org.apache.wicket.pageStore.AbstractCachingPageStore.getPage(AbstractCachingPageStore.java:67) at org.apache.wicket.page.PageStoreManager$SessionEntry.getPage(PageStoreManager.java:211) at org.apache.wicket.page.PageStoreManager$PersistentRequestAdapter.getPage(PageStoreManager.java:367) at org.apache.wicket.page.AbstractPageManager.getPage(AbstractPageManager.java:82) at org.apache.wicket.page.PageManagerDecorator.getPage(PageManagerDecorator.java:50) at org.apache.wicket.page.PageAccessSynchronizer$2.getPage(PageAccessSynchronizer.java:246) at org.apache.wicket.DefaultMapperContext.getPageInstance(DefaultMapperContext.java:113) at org.apache.wicket.core.request.handler.PageProvider.getStoredPage(PageProvider.java:299) at org.apache.wicket.core.request.handler.PageProvider.isNewPageInstance(PageProvider.java:211) at org.apache.wicket.core.request.mapper.AbstractBookmarkableMapper.checkExpiration(AbstractBookmarkableMapper.java:335) at org.apache.wicket.core.request.mapper.AbstractBookmarkableMapper.processHybrid(AbstractBookmarkableMapper.java:258) at org.apache.wicket.core.request.mapper.AbstractBookmarkableMapper.mapRequest(AbstractBookmarkableMapper.java:364) at org.apache.wicket.request.mapper.CompoundRequestMapper.mapRequest(CompoundRequestMapper.java:147) at org.apache.wicket.request.cycle.RequestCycle.resolveRequestHandler(RequestCycle.java:189) at org.apache.wicket.request.cycle.RequestCycle.processRequest(RequestCycle.java:219) at org.apache.wicket.request.cycle.RequestCycle.processRequestAndDetach(RequestCycle.java:293) at org.apache.wicket.protocol.http.WicketFilter.processRequestCycle(WicketFilter.java:261) at org.apache.wicket.protocol.http.WicketFilter.processRequest(WicketFilter.java:203) at org.apache.wicket.protocol.http.WicketFilter.doFilter(WicketFilter.java:284) .......中略 Caused by: java.io.InvalidClassException: xyz.monotalk.festivals4partypeople.web.ui.pages.festival.search.FestivalsSearchPage; local class incompatible: stream classdesc serialVersionUID = 7766378564606176148, local class serialVersionUID = 7766378564606176111 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:621) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371) at org.apache.wicket.serialize.java.JavaSerializer.deserialize(JavaSerializer.java:126) ... 59 more
前提
- wicket の
version
7.7.0 です。
<!-- WICKET DEPENDENCIES --> <dependency> <groupId>org.apache.wicket</groupId> <artifactId>wicket-core</artifactId> <version>7.7.0</version> </dependency>
参考
subject:”Bes practice for deployment to avoid ClassNotFoundException”
java.io.InvalidClassException
に関する ものではないですが、 Class名変更した 際に、 ClassNotFoundException
が発生する 話 Wicket: Best practice for deployment to avoid ClassNotFoundException - Stack Overflow
こちらも上記と 同じです。 難解な
Serializableと いう 仕様に ついて 俺が 知っている こと、というか 俺の 理解 - 都元ダイスケ IT-PRESS
Serializable の動作に ついて Serializableと
serialVersionUID - 都元ダイスケ IT-PRESS
Serializable の動作に ついての 続き Javaと
シリアライズと 互換性 - CLOVER Java 6 以降での シリアライズに ついて
エラー発生の 原理
DiskDataStore
を
デプロイ前、
1.デプロイ前
- ユーザが
ページに アクセス。 ページ情報 が Disk に 書き出される
2.デプロイ
- アプリケーションが
停止. 書き出されていた ページ情報 は Disk に 残る
3.デプロイ後
アプリケーションが
起動. ユーザーが
アプリケーションに アクセス。 スティッキーセッション であれば、 jessionid に 基づいて 同一サーバに アクセス。 Wicket が
Disk から ページ情報の 復旧を 試みるが、 クラスの 互換性が ない(serialVersionUIDが 変わっている ため)、
java.lang.RuntimeException
が発生する。
StackOverFlow の 記事を 読んだ 限りの 理解
方針レベルでの 案
回避方法 と
ユースケースに
jsessionid を
解放して、 古い Version の クラスに アクセスできないようにする。 セッション内に
新しい ページインスタンスを 作成する (ブラウザの URLに ある?0、 ?1231等の versionパラメータを 削除する ) サーバ 上の
すべての セッションデータを 消去する
具体的な 回避実装
具体的な
BookmarkablePageLink を
使用して ページ間を 移動する。 3 DelegatingJavaSerializer を
作成し、 InvalidClassException 発生時は、 Nullを 返して、 Classを 再生成する。
[3] BookmarkablePageLink を
DelegatingJavaSerializer を 作成する
このDelegatingJavaSerializer
を
InvalidClassException の
1. DelegatingJavaSerializer では、
InvalidClassException
の場合は、 InvalidClassRuntimeException
をスロー。 2. DefaultExceptionMapper の
拡張クラスを 作成して、 InvalidClassRuntimeException
の場合は、 専用エラーページへ 遷移。 4 - 3. 専用エラーページでは、
jsessionId
の削除と、 ページ状態が おかしいので、 この ページに きた 旨の 通知する。
[4]追記:RequestCycleListener#onException()
で
RequestCycleListener#onException()
で
作成した
DelegatingJavaSerializer.java
package xyz.monotalk.festivals4partypeople.web.wicket.serialize.java; import lombok.extern.log4j.Log4j2; import org.apache.wicket.serialize.java.JavaSerializer; import java.io.InvalidClassException; /** * Created by kem */ @Log4j2 public class DelegatingJavaSerializer extends JavaSerializer { /** * Construct.. * * @param applicationKey */ public DelegatingJavaSerializer(String applicationKey) { super(applicationKey); } /** * deserialize * * @param data * @return */ public Object deserialize(byte[] data) { Object object; try { object = super.deserialize(data); } catch (RuntimeException e) { Throwable t = e.getCause(); if (t instanceof InvalidClassException) { log.warn("Throw InvalidClassRuntimeException as InvalidClassException occurred. ", e); throw new InvalidClassRuntimeException(e); } throw e; } return object; } /** * InvalidClassRuntimeException */ public static class InvalidClassRuntimeException extends RuntimeException { public InvalidClassRuntimeException(Throwable t) { super(t); } } }
- 補足
RuntimeException 発生時に原因例外を 取得し、 原因例外が InvalidClassException
の場合は、 自前の InvalidClassRuntimeException
を返すようにしました。
ClassNotFoundException
も出力される 可能性も あるのですが、 ClassNotFoundException
は、クラス名を 変更した 際などに 送出される 例外です。
あまり発生確率は 高くない 気が したので、 InvalidClassException
のみ捕捉するようにしました。
ApplicationExceptionMapper.java
DefaultExceptionMapper の
/* * Copyright 2016 Kem. */ package xyz.monotalk.festivals4partypeople.web.wicket.protocol.http; import lombok.extern.log4j.Log4j2; import org.apache.wicket.Application; import org.apache.wicket.DefaultExceptionMapper; import org.apache.wicket.core.request.handler.PageProvider; import org.apache.wicket.request.IRequestHandler; import xyz.monotalk.festivals4partypeople.web.ui.pages.error.session.session.SessionErrorPage; import xyz.monotalk.festivals4partypeople.web.wicket.serialize.java.DelegatingJavaSerializer; /** * ApplicationExceptionMapper * * @author Kem */ @Log4j2 public class ApplicationExceptionMapper extends DefaultExceptionMapper { @Override public IRequestHandler map(Exception e) { return super.map(e); } @Override protected IRequestHandler mapExpectedExceptions(Exception e, Application application) { if (e instanceof DelegatingJavaSerializer.InvalidClassRuntimeException) { // Page page = super.extractCurrentPage(); // System.out.println("------------------------------------"); // System.out.println(page); // System.out.println("------------------------------------"); // Url url = ((WebRequest) RequestCycle.get().getRequest()).getUrl(); // String fullUrl = RequestCycle.get().getUrlRenderer().renderFullUrl(url); // System.out.println("------------------------------------"); // System.out.println(fullUrl); // System.out.println("------------------------------------"); // MapperUtils.getPageComponentInfo(url); return super.createPageRequestHandler(new PageProvider(SessionErrorPage.class)); // return new RenderPageRequestHandler(new PageProvider(SessionErrorPage.class)); } return super.mapExpectedExceptions(e, application); } }
補足1
super.extractCurrentPage();
でアクセスしている ページオブジェクトが 取得できるので、 ページオブジェクト取得して、 特定ページの 場合は、 その ページインスタンスを 改めて 生成して 返すことができるかと 思いました。
しかし、InvalidClassException
が発生する ケースで、 アクセスしている ページオブジェクトは Null
になっていました。 補足2
コメントしている以下のの 実装で、 アクセスしている URLが 取得できます。 このUrl url = ((WebRequest) RequestCycle.get().getRequest()).getUrl(); String fullUrl = RequestCycle.get().getUrlRenderer().renderFullUrl(url);
URLを 元に アクセスした ページを 特定して、 復旧可能な ページであれば 復旧処理を 行うのも 方針と しては 有りかもしれません。 補足3
super.createPageRequestHandler
と、new RenderPageRequestHandler()
でPageRequestHandlerを 作成した 際の、 挙動の 違いに ついてです。 メソッドでreturn super.createPageRequestHandler(new PageProvider(SessionErrorPage.class));
ページを 返すと、 アクセスしている URLは 変わらずに、 Renderingする ページだけが 変わります。 メソッドでreturn new RenderPageRequestHandler(new PageProvider(SessionErrorPage.class));
ページを 返すと、 アクセスしている URLも 変わり、 Renderingする ページも 変わります。
RequestCycleListener#onException() で ハンドリングする
AbstractRequestCycleListenerの
このRequestCycle
が
// Add RequestCycleListener getRequestCycleListeners().add(new AbstractRequestCycleListener() { @Override public IRequestHandler onException(RequestCycle cycle, Exception ex) { // Log log.info("============================================================"); log.info("Raise Exception", ex); log.info("============================================================"); if (ex instanceof DelegatingJavaSerializer.InvalidClassRuntimeException) { return new RenderPageRequestHandler(new PageProvider(SessionErrorPage.class)); } return super.onException(cycle, ex); } });
挙動に
Wicket 例外ハンドリング(Exception handling)に
SessoinErrorPage.java
package xyz.monotalk.festivals4partypeople.web.ui.pages.error.session.session; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.markup.html.link.ExternalLink; import org.apache.wicket.request.mapper.parameter.PageParameters; import xyz.monotalk.festivals4partypeople.web.ui.pages.base.BasePage; /** * SessionErrorPage */ public class SessionErrorPage extends BasePage { @Override protected void initPage(PageParameters parameters) { // Add ErrorMessage add(new Label("errorMessage", "Invalid Session")); // Add Top page Link BookmarkablePageLink link = new BookmarkablePageLink<>("topLink", getApplication().getHomePage()); link.add(new Label("topLinkText", "Top Page").setRenderBodyOnly(true)); add(link); // Add Previous page Link ExternalLink previousLink = new ExternalLink("previousLink", "../", "Previous Page"); add(previousLink); // ReplaceSession getSession().replaceSession(); } }
補足1
getSession().replaceSession();
で、セッションの 破棄、 再生成を 行なってます。 5
[5]実装みた限りそのような 動作と なります。 実際そうなるか 検証が できてませんが。。 補足2
ログインした認証情報が、 jsessionid に 紐づいていると、 強制ログアウトしている ことになるので、 その ケースだと 上手く いかないです。
jseesionid ではない値に 紐付けして、 認証情報管理していれば、 上記の 実装でうまく いくと 思います。
まとめ
シリアライズエラーがjava.lang.RuntimeException
が
現状でまだ
URLに
jsessionid付与されている 場合は、 除去する。
ブラウザのcookieが offの 場合、 jsessionidは、 URLに 付与されます。
session のreplace 時に その あたりの 考慮を してくれていれば、 現状実装でも 問題が ないと 思います。 元の
画面に 戻れるようにする。
これが実現できると、 より ユーザービリティは いいかと 思いますが、 refferを 参照する 必要が あったり、 若干危ないかもしれません。
以上です。
コメント