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

## Ratpack入門シリーズ

1. [Ratpack入門 (1) - Ratpackとは](https://qiita.com/minebreaker/items/d7eeca815872a97069f5)
2. [Ratpack入門 (2) - アーキテクチャー](https://qiita.com/minebreaker/items/a9b256fd8a1513104669)
3. [Ratpack入門 (3) - hello world 詳解](https://qiita.com/minebreaker/items/b26d7d0677f91f17a7b5)
4. [Ratpack入門 (4) - ルーティング & 静的コンテンツ](https://qiita.com/minebreaker/items/8f26d8350f936ad669c2)
5. [Ratpack入門 (5) - Json & Registry](https://qiita.com/minebreaker/items/4353611cc541e3cb35fb)
6. [Ratpack入門 (6) - Promise](https://qiita.com/minebreaker/items/4689058d0d7b34d99c74)
7. [Ratpack入門 (7) - Guice & Spring](https://qiita.com/minebreaker/items/52b303bc18db5b33bd68)
8. [Ratpack入門 (8) - セッション](https://qiita.com/minebreaker/items/1ebaa492b257365e1e5e)
9. [Ratpack入門 (9) - Thymeleaf](https://qiita.com/minebreaker/items/50693a4d6614979f654c)


## セッション

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

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

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

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

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

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

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

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

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

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

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

```java
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モジュールが用意されています。

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

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

```java
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`から実装を切り替えられます。

![キャプチャ2.png](https://qiita-image-store.s3.amazonaws.com/0/231669/2f47892f-9295-5f8c-788b-44cc2f2412a6.png)

```
# 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](https://lettuce.io/)を使用しています(いつもキャベツと間違える)。