RedisCacheConfigの設定

エキサイト株式会社 メディア事業部エンジニアの中尾です。

SQL Serverからデータを取得し、redisにデータをキャッシュさせようとしたら以下のエラーが出ました。

Could not write JSON: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module \"com.fasterxml.jackson.datatype:jackson-datatype-jsr310\" to enable handling

どうやらdatatype-jsr310が効いていないようです。

https://github.com/FasterXML/jackson-databind/issues/2983

issuesでもあったようですね。

objectMapperをカスタマイズする

redisとのやり取りの部分をobjectMapperの設定を追加してやります。

ポイントは、redisにキャッシュする際のデータの書き読み込みする際のobjectMapperの設定をGenericJackson2JsonRedisSerializerに設定しているところです。 SerializationFeature.WRITE_DATES_AS_TIMESTAMPSをすることでISO8601 形式にシリアライズします。

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * ElastiCacheではconfigコマンドの実行が禁止されているため、「ConfigureRedisAction」を「NO_OP」に設定しておく
     *
     * @return ConfigureRedisAction
     */
    @Bean
    public ConfigureRedisAction configureRedisAction() {
        return ConfigureRedisAction.NO_OP;
    }

    /**
     * Redisの設定
     *
     * @param factory
     * @return ReactiveRedisTemplate
     */
    @Bean
    public ReactiveRedisTemplate reactiveRedisTemplate(LettuceConnectionFactory factory) {
        StringRedisSerializer keySerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(this.objectMapper());
        RedisSerializationContext.RedisSerializationContextBuilder<String, Object> builder =
                RedisSerializationContext.newSerializationContext(keySerializer);
        RedisSerializationContext<String, Object> context = builder.value(valueSerializer).build();
        return new ReactiveRedisTemplate<>(factory, context);
    }

    /**
     * Redisの中に平文でデータを保存するための設定
     *
     * @return RedisCacheManagerBuilder
     */
    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
        return builder -> {
            var map = Arrays.stream(CacheKeyType.values())
                    .collect(
                            Collectors.toMap(
                                    e -> e.getKey()
                                    , e -> RedisCacheConfiguration
                                            .defaultCacheConfig()
                                            .entryTtl(Duration.ofSeconds(e.getTtl()))
                                            .disableCachingNullValues()
                                            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()))
                                            .serializeValuesWith(RedisSerializationContext.SerializationPair
                                                    .fromSerializer(new GenericJackson2JsonRedisSerializer(this.objectMapper())))
                            ));
            builder.withInitialCacheConfigurations(map);
        };
    }

    /**
     * redisにキャッシュする際のデータの書き読み込みする際のobjectMapperの設定
     *
     * LocalDateTime などの Date and Time API 関連のフィールドを扱う
     * 不明なプロパティがあっても無視
     * オブジェクト情報を追加する
     *
     * @return
     */
    private ObjectMapper objectMapper() {
        return new ObjectMapper()
                .registerModule(new JavaTimeModule())
                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .activateDefaultTyping(BasicPolymorphicTypeValidator.builder()
                                .allowIfBaseType(Object.class)
                                .build(),
                        ObjectMapper.DefaultTyping.EVERYTHING)
                ;
    }

    /**
     * キャッシュからのデシリアライズに失敗した場合は、
     * エラーにはせずに対象のメソッドをそのまま呼ぶ
     * その結果はキャッシュされる
     *
     * @return
     */
    @Override
    @Bean
    public CacheErrorHandler errorHandler() {

        return new SimpleCacheErrorHandler() {

            @Override
            public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                if (isSerializationError(exception)) {
                    return;
                }

                throw exception;
            }

            private boolean isSerializationError(RuntimeException exception) {
                if (exception instanceof SerializationException) {
                    return true;
                }

                Throwable cause = exception.getCause();
                return (cause != null && cause instanceof SerializationException);
            }
        };
    }
}

設定を追加したら以下のようにキャッシュされるようになりました。

[
    "クラス名",
    {
        "id": "0000000",
        "longValue": [
            "java.lang.Long",
            7261626
        ],
        "date": [
            "java.time.LocalDateTime",
            "2021-05-25T06:17:30.743"
        ],