/*
 * Copyright (c) 2018-2999 广州市蓝海创新科技有限公司 All rights reserved.
 *
 * https://www.mall4j.com/
 *
 * 未经允许，不可做商业用途！
 *
 * 版权所有，侵权必究！
 */

// 该类所属的包名，表明其位于商城项目的通用（common）配置（config）包下，主要用于对Redis缓存相关的配置进行定义和设置，通过Spring的配置类机制，
// 在满足一定条件（配置文件中spring.cache.type = redis时）生效，来实现Redis缓存在项目中的集成与定制化配置。
package com.yami.shop.common.config;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

/**
 * RedisCacheConfig类是一个Spring的配置类，用于配置Redis缓存相关的各种组件和行为，使得项目能够基于Redis实现高效的缓存功能，并且可以根据实际需求进行灵活的定制化配置。
 * 只有当项目的配置文件中指定spring.cache.type = redis时，这里定义的Redis缓存相关配置才会生效，用于控制缓存的存储、序列化、过期时间等关键特性。
 *
 * @author lgh
 */
@EnableCaching
// 使用@EnableCaching注解开启Spring的缓存功能，使得在项目中可以使用诸如@Cacheable、@CachePut、@CacheEvict等缓存相关的注解来方便地操作缓存，提高应用性能。
@Configuration
// 使用@Configuration注解表明该类是一个配置类，Spring会扫描这个类，并根据其中定义的@Bean方法来创建相应的Bean实例，注入到Spring容器中供其他组件使用。
public class RedisCacheConfig {

    /**
     * 创建并配置CacheManager的Bean方法，CacheManager是Spring缓存抽象的核心接口，用于管理缓存，在这里具体配置了基于Redis的缓存管理方式。
     * 通过传入RedisConnectionFactory（用于创建与Redis服务器的连接）和RedisSerializer（用于对缓存数据进行序列化和反序列化操作），
     * 构建了一个RedisCacheManager实例，该实例负责管理Redis缓存的各种配置和行为，如缓存的默认过期时间、特定缓存键的过期时间配置等。
     *
     * @param redisConnectionFactory 用于创建与Redis服务器连接的工厂类实例，通过它可以获取到与Redis服务器通信的连接对象，是操作Redis缓存的基础依赖。
     * @param redisSerializer 用于对缓存中存储的对象进行序列化和反序列化的序列化器，确保对象能够正确地在Java内存和Redis存储之间进行转换，保证数据的一致性和可存储性。
     * @return 返回配置好的RedisCacheManager实例，用于管理项目中的Redis缓存，将其注入到Spring容器中，以便其他组件可以获取并使用该缓存管理器来操作缓存。
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, RedisSerializer<Object> redisSerializer) {

        RedisCacheManager redisCacheManager = new RedisCacheManager(
                RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
                // 默认策略，未配置的 key 会使用这个
                this.getRedisCacheConfigurationWithTtl(3600, redisSerializer),
                // 指定 key 策略
                this.getRedisCacheConfigurationMap(redisSerializer)
        );
        redisCacheManager.setTransactionAware(true);
        return redisCacheManager;
    }

    /**
     * 用于获取特定缓存键对应的Redis缓存配置映射（Map）的私有方法，通过创建一个HashMap实例，并向其中添加特定缓存键（这里是"product"）与对应的Redis缓存配置信息，
     * 使得可以针对不同的缓存键设置不同的缓存过期时间等配置，方便根据业务需求对不同类型的数据缓存进行精细化管理。
     *
     * @param redisSerializer 用于对缓存数据进行序列化和反序列化操作的序列化器，确保缓存配置中涉及的数据能够正确地进行序列化处理，与整体的缓存数据格式保持一致。
     * @return 返回包含特定缓存键与对应Redis缓存配置信息的Map，用于在创建RedisCacheManager时设置指定键的缓存策略，以便根据不同的缓存键应用不同的缓存过期时间等规则。
     */
    private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap(RedisSerializer<Object> redisSerializer) {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(16);
        redisCacheConfigurationMap.put("product", this.getRedisCacheConfigurationWithTtl(1800, redisSerializer));
        return redisCacheConfigurationMap;
    }

    /**
     * 用于获取带有指定过期时间（TTL，Time To Live）的Redis缓存配置的私有方法，通过以默认的Redis缓存配置（RedisCacheConfiguration.defaultCacheConfig()）为基础，
     * 使用传入的序列化器（redisSerializer）来配置缓存值的序列化方式，并设置缓存条目的过期时间（通过Duration.ofSeconds(seconds)指定秒数），从而生成符合特定需求的Redis缓存配置信息。
     *
     * @param seconds 缓存条目的过期时间，以秒为单位，用于控制缓存数据在Redis中存储的有效时长，超过这个时间后，缓存数据将被自动清除，以保证缓存数据的时效性和内存空间的合理利用。
     * @param redisSerializer 用于对缓存中存储的对象进行序列化和反序列化的序列化器，确保缓存数据能够按照指定的格式进行转换，以便正确地存储在Redis中以及从Redis中读取还原。
     * @return 返回配置好的带有指定过期时间的Redis缓存配置信息，用于在创建RedisCacheManager或者设置特定缓存键的配置时使用，来定义缓存的具体行为，如数据的序列化和过期策略等。
     */
    private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds, RedisSerializer<Object> redisSerializer) {

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
                RedisSerializationContext
                       .SerializationPair
                       .fromSerializer(redisSerializer)
        ).entryTtl(Duration.ofSeconds(seconds));

        return redisCacheConfiguration;
    }

    /**
     * 创建并配置RedisTemplate的Bean方法，RedisTemplate是Spring Data Redis提供的用于操作Redis的核心模板类，
     * 它封装了与Redis进行交互的常见操作方法，如设置键值对、获取值、操作哈希等数据结构，在这里对其进行了详细的配置，包括设置连接工厂、键和值的序列化器等，
     * 使得可以方便地在项目中使用该模板类来操作Redis缓存，实现数据的读写等功能。
     *
     * @param redisConnectionFactory 用于创建与Redis服务器连接的工厂类实例，为RedisTemplate提供与Redis服务器通信的连接，是操作Redis的基础依赖，确保能够正确地访问Redis服务器。
     * @param redisSerializer 用于对缓存中存储的对象进行序列化和反序列化的序列化器，用于配置RedisTemplate中值的序列化方式，保证存入Redis的数据和从Redis读取还原的数据格式正确，与整体缓存数据的序列化策略保持一致。
     * @return 返回配置好的RedisTemplate实例，注入到Spring容器中，方便其他组件获取并使用它来进行与Redis缓存相关的各种操作，如向缓存中存入数据、从缓存中获取数据等操作。
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory, RedisSerializer<Object> redisSerializer) {

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(redisSerializer);
        redisTemplate.setHashValueSerializer(redisSerializer);
        redisTemplate.setEnableTransactionSupport(false);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * 自定义Redis序列化机制的Bean方法，通过创建一个自定义配置的ObjectMapper实例，对Jackson的序列化和反序列化相关特性进行了一系列配置，
     * 目的是为了避免与Spring MVC中默认的ObjectMapper配置产生冲突，同时满足项目中对Redis缓存数据序列化的特定需求，例如对日期时间类型的序列化处理、处理未知属性、空对象以及无效子类型等情况的处理方式，
     * 最后基于这个自定义的ObjectMapper创建并返回一个GenericJackson2JsonRedisSerializer实例，用于在Redis缓存中对对象进行序列化和反序列化操作。
     *
     * @return 返回配置好的GenericJackson2JsonRedisSerializer实例，作为Redis缓存的序列化器，注入到Spring容器中，供其他与Redis缓存相关的组件（如RedisCacheManager、RedisTemplate等）使用，
     * 确保缓存数据能够按照预期的方式进行序列化和反序列化，保证数据的正确存储和读取。
     */
    /**
     * 自定义redis序列化的机制,重新定义一个ObjectMapper.防止和MVC的冲突
     * https://juejin.im/post/5e869d426fb9a03c6148c97e
     */
    @Bean
    public RedisSerializer<Object> redisSerializer() {
        ObjectMapper objectMapper = JsonMapper.builder().disable(MapperFeature.USE_ANNOTATIONS).build();
        // 反序列化时候遇到不匹配的属性并不抛出异常，这样即使缓存数据中存在一些在当前类中没有定义的属性，也能正常进行反序列化操作，提高了兼容性和容错性。
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 序列化时候遇到空对象不抛出异常，避免因为缓存数据中存在空对象而导致序列化失败的情况，使得空对象也能按照预期进行序列化处理。
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // 反序列化的时候如果是无效子类型,不抛出异常，在处理复杂的继承结构或者类型变化时，即使遇到不符合预期的子类型情况，也能继续进行反序列化，增强了反序列化的稳定性。
        objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
        // 不使用默认的dateTime进行序列化，避免在处理日期时间类型数据时出现格式不符合项目需求的情况，方便按照项目自定义的方式对日期时间进行序列化。
        objectMapper.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
        // 使用JSR310提供的序列化类，里面包含了大量的JDK8时间序列化类，利用Java 8中对日期时间API的优化，更规范、准确地对日期时间类型数据进行序列化和反序列化操作。
        objectMapper.registerModule(new JavaTimeModule());
        // 启用反序列化所需的类型信息，在属性中添加@class，通过这种方式在反序列化时可以明确数据的类型信息，有助于正确还原对象类型，尤其是在处理多态类型数据时非常重要。
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);
        // 配置null值的序列化器，确保在缓存数据中存在null值时也能正确地进行序列化和反序列化操作，保证数据的完整性和一致性。
        GenericJackson2JsonRedisSerializer.registerNullValueSerializer(objectMapper, null);
        return new GenericJackson2JsonRedisSerializer(objectMapper);
    }

    /**
     * 创建并配置StringRedisTemplate的Bean方法，StringRedisTemplate是Spring Data Redis提供的专门用于操作Redis中字符串类型数据的模板类，
     * 相比于RedisTemplate，它更专注于对字符串类型的操作，在这里对其进行了简单配置，设置了禁止事务支持，并返回配置好的实例注入到Spring容器中，方便在项目中对Redis中的字符串数据进行操作，
     * 例如存储简单的字符串键值对、执行字符串相关的命令等操作。
     *
     * @param redisConnectionFactory 用于创建与Redis服务器连接的工厂类实例，为StringRedisTemplate提供与Redis服务器通信的连接，确保能够正确地访问Redis服务器进行字符串数据的操作。
     * @return 返回配置好的StringRedisTemplate实例，注入到Spring容器中，供其他组件获取并使用它来进行与Redis中字符串类型数据相关的各种操作，方便在项目中处理简单的字符串缓存数据等情况。
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate redisTemplate = new StringRedisTemplate(redisConnectionFactory);
        redisTemplate.setEnableTransactionSupport(false);
        return redisTemplate;
    }
}