Ratpack入門 (8) - セッション


Ratpack入門シリーズ

  1. Ratpack入門 (1) - Ratpackとは
  2. Ratpack入門 (2) - アーキテクチャー
  3. Ratpack入門 (3) - hello world 詳解
  4. Ratpack入門 (4) - ルーティング & 静的コンテンツ
  5. Ratpack入門 (5) - Json & Registry
  6. Ratpack入門 (6) - Promise
  7. Ratpack入門 (7) - Guice & Spring
  8. Ratpack入門 (8) - セッション
  9. Ratpack入門 (9) - Thymeleaf

セッション

Ratpackはデフォルトではセッションを提供しません。いわゆるStickyセッションを使うには、ratpack-sessionモジュールが必要です。Redisによるセッションサポートを有効にするには、さらにratpack-session-redisモジュールが必要です。

build.gradle
dependencies {
    compile "io.ratpack:ratpack-session:1.5.1"
}

モジュールを登録する必要があります。前述のようにRatpackは、拡張モジュールをGuiceのモジュールとして提供しています。

Function<Registry, Registry> registry = ratpack.guice.Guice.registry( bindings -> {
    bindings.module( SessionModule.class );
} );

セッションを管理するSessionクラスをRegistryから取得して、セッションのデータを操作します。ハンドラーのContextRegistryを継承しているので、Context.get()メソッドを呼び出して、セッションを取得できます。

Session session = context.get( Session.class );

セッションのキーには、Stringやクラス自体を使用することができます。しかしStringのキーではデータの取得にキャストが必要になり、クラスを使用する場合は同じクラスを登録できなくなります。一番いい方法は、SessionKey<T>クラスを使用することです。

public static final SessionKey<String> KEY = SessionKey.of( "KEY_NAME", String.class );

キーの名前と型を指定することで、安全にデータのやり取りを行うことができます。

データの取得及び設定には、それぞれget(SessionKey)およびset(SessionKey<T>, T)を使用します。これらの操作はブロッキング操作として扱われます。従って、これらのメソッドは直接値を返すのではなく、PromiseおよびOperationを返します。これはセッションの情報がどう保存されるかは実装依存であり、サーバー内部ではなく外部のKVSなどに保存されることも想定しているためです。

以下、ユーザーがサイトを訪れた回数と時間を保存する例です。

Session session = ctx.get( Session.class );

session.get( countKey ).flatRight( v -> session.get( lastVisit ) ).then( pair -> {

    int count = pair.left.orElse( 0 );

    String response = count == 0
                      ? "This is the first visit."
                      : "You have visited " + count + " times.\n" +
                        "Last visit: " + pair.right.get().format( DateTimeFormatter.ISO_LOCAL_DATE_TIME );

    session.set( countKey, count + 1 )
           .next( session.set( lastVisit, LocalDateTime.now() ) )
           .then( () -> {
               ctx.render( response );
           } );
} );

get()が返す型はPromise<Optional<T>>です。Optionalにラップしてくれるので、Servletのようにnullチェックする必要がありません。
見てのとおり、複数の値を取得しようとするとPromiseが重なってコードが汚くなります。getData()メソッドで、まとめて値を取得できます。
また、保存されたデータはJavaのシリアライズ機能によって直列化されます。そのため、Serializableを実装している必要があります。シリアライズはSessionSerializerによって行われます。Registryに独自の実装を提供することで、シリアライズの手法を変更できます。

ClientSideSessionModule

ratpack-sessionのデフォルト実装はGuavaのキャッシュを利用したインメモリーキャッシュを使います。一方、ClientSideSessionModuleを使用することで、クッキーに暗号化してデータを保存する、クライアントサイドセッションを使うモジュールも用意されています。

デフォルトでは情報は暗号化されません。必ずキーを指定する必要があります。

ratpack-session-redis

セッション情報をRedisに保存するために、ratpack-session-redisモジュールが用意されています。

build.gradle
dependencies {
    compile "io.ratpack:ratpack-session-redis:1.5.1"
}

モジュールを設定します。

RedisSessionModule redisModule = new RedisSessionModule();
redisModule.configure( config -> {
    config.setHost( "localhost" );
    config.setPort( 6379 );
    config.setPassword( null );
} );

Function<Registry, Registry> registry = ratpack.guice.Guice.registry( bindings -> {
    bindings.module( SessionModule.class );
    bindings.module( redisModule );
} );

RedisSessionModuleは、必ずSessionModuleに登録する必要があります。Ratpackは後から登録されたクラスを優先します。順番を逆にしてしまうと、Sessionの実装がRedisではなく、Stickyセッションのものになってしまいます。
ここではホストやポートをプログラムから設定していますが、ConfigSourceから設定することができます。

使い方自体は通常のratpack-sessionと同様です。設定してしまえば、バックエンドがRedisになっていることを意識することはありません。

Redisサーバーには、セッションIDをそのままキーとして、データが保存されます。セッションIDはデフォルトではUUIDが使われます。IDの生成はSessionIdGenerator経由で行われ、Registryから実装を切り替えられます。

# redis-cli
127.0.0.1:6379> keys *
1) "036f9723-3df8-e018-9abe-2db2bca5f6f6"
127.0.0.1:6379> get 036f9723-3df8-e018-9abe-2db2bca5f6f6
"\xac\xed\x00\x05sr\x006ratpack.session.internal.DefaultSession$SerializedForm\x00\x00\x00\x00\x00\x00\x00\x02\x0c\x00\x00xpw\xd3\x00\x01\x00\x02\x01\x00\x05COUNT\x01\x00\x11java.lang.Integer\x00\x00\x00
Q\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x02\x01\x00\nLAST_V
ISIT\x01\x00\x17java.time.LocalDateTime\x00\x00\x003\xac\xed\x00\x05sr\x00\rjava.time.Ser\x95]\x84\xba\x1b\"H\xb2\x0c\x00\x00xpw\x0e\x05\x00\x00\a\xe2\x02\t\r8!\x1a\xe1\xb6\xc0xx"
127.0.0.1:6379>

ちなみに、Redisとの通信はLettuceを使用しています(いつもキャベツと間違える)。