Spring BootのRedisキャッシュで、List.ofを使えるようにする方法

f:id:excite-takayuki-miura:20220418112217p:plain

こんにちは。 エキサイト株式会社の三浦です。

Spring Boot には様々な機能が用意されており、その一つに Redis へのキャッシュ機能があります。 非常に便利なのですが、少なくとも Spring Bootver.2.6.6 では、 List.of (正確には、 List.of などによって返却されるイミュータブルコレクション)等の一部の型でキャッシュが使えないという問題があります。

今回は、 List.of を使えるようにする方法について説明していきます。

Spring BootとRedisキャッシュ

Spring Boot では、spring-data-redisというライブラリを使って、任意のデータを Redis にキャッシュすることができます。 便利なのですが、少なくとも Spring Boot ver.2.6.6 の段階では、 List.of をキャッシュすると以下のようなエラーが出てしまいます。

Could not read JSON: Unexpected token (END_ARRAY), expected VALUE_STRING: need JSON String that contains type id (for subtype of java.lang.Object)\n at [Source: (byte[])\"[]\"; line: 1, column: 2]; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Unexpected token (END_ARRAY), expected VALUE_STRING: need JSON String that contains type id (for subtype of java.lang.Object)\n at [Source: (byte[])\"[]\"; line: 1, column: 2]

これは、 Redis へのキャッシュ方法が原因となっています。

Redis キャッシュの流れとして、

  • キャッシュを作成する際は、Java用のデータをJSON化して Redis に保存
    • この際、Java側の型情報も一緒に保存
  • キャッシュを取得する際は、 Redis のデータをJSONからJava用のデータに変換して使用
    • この際、保存されている型情報も参照する

となっています。

List.of などの final なデータはJSON化はできるのですが、JSONからデータを戻す際に型情報の変換がうまく行かずエラーが起きてしまうようです。

List.ofをキャッシュする方法

これを解決するためには、JSONの変換周りをカスタマイズする必要があります。

Spring Boot では、 Redis キャッシュの値のシリアライザを開発者側で指定することができるのですが、それを以下のようにすれば、 List.of をキャッシュできるようになります。

    private GenericJackson2JsonRedisSerializer serializer() {
        // *1
        ObjectMapper objectMapper = new ObjectMapper()
                .activateDefaultTyping(
                        LaissezFaireSubTypeValidator.instance,
                        ObjectMapper.DefaultTyping.EVERYTHING,
                        JsonTypeInfo.As.PROPERTY
                );

        // *2
        // GenericJackson2JsonRedisSerializer.registerNullValueSerializer(objectMapper, null);

        return new GenericJackson2JsonRedisSerializer(objectMapper);
    }

少し解説していくと、

*1

基本的にはデフォルト設定と同じですが、第二引数である DefaultTyping.EVERYTHING が変わっています。 これを第二引数に入れることで、 final なものであっても型情報を保存するようになります。

*2

disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) の設定に必要ですが、 DefaultTyping.EVERYTHING で使用するには GenericJackson2JsonRedisSerializer 内の一部処理を変更する必要があります。 spring-data-redisver.2.7 以降であれば処理が変更されているようなので、アップデートすれば設定しても問題ないと思われます。

最後に

List.of は、非常に便利な機能です。 使えるのであればこれを使わない手は無いので、ぜひ今回の設定をしていただき、ガンガン List.of を使っていきましょう!

なお、spring-data-redisver.2.7 以降であればデフォルトでDefaultTyping.EVERYTHINGとなっているようなので、将来的にはこの設定は不要になるかもしれません。 現在のデフォルト設定も見てみると良いでしょう。