基于JetCache整合实现一级、二级缓存方案(方案实现)

目录

一、整体方案说明

1.1 需求说明

1.2 整体方案实现组件结构图

二、Caffeine缓存实现

2.1 组件说明

2.2 组件结构图

2.3 组件Maven依赖

2.4 组件功能实现源码

2.4.1 CaffeineCacheManager扩展实现

2.4.2 CaffeineConfiguration配置类实现

2.4.3 涉及其他组件的类

2.4.3.1 缓存过期时间通用属性类

2.4.3.2 缓存配置类

三、Redis缓存实现

3.1 组件说明

3.2 组件结构图

3.3 组件Maven依赖

3.4 组件功能实现源码

3.4.1 实现Redis缓存功能

3.4.1.1 RedisCacheManager 类扩展实现

3.4.1.2 RedisBitMapUtils工具类

3.4.1.3 CacheRedisConfiguration 缓存配置类实现

3.4.1.4 EnableKuaFuCloudRedis Redis启动注解

3.4.2 实现Spring Session的Redis缓存

3.4.2.1 HttpSessionIdResolver SessionId 解析类扩展

3.4.2.2 RedisSessionSharingConfiguration 启动配置类

3.4.2.3 注解生效条件类RedisSessionSharingCondition

3.4.2.4 ConditionalOnRedisSessionSharing 注解类

四、JetCache缓存框架整合

4.1 组件说明

4.2 组件结构图

4.3 组件Maven依赖

4.4 组件功能实现源码

4.4.1 Mybatis二级缓存扩展

4.4.1.1 ExtMybatisCache 实现类

4.4.2 Spring Cache缓存扩展

4.4.2.1 JetCache缓存工厂类JetCacheCreateCacheFactory

4.4.2.2 Spring Cache扩展实现类JetCacheSpringCache

4.4.2.3 Spring CacheManager扩展实现类JetCacheSpringCacheManager

4.4.2.4 自定义缓存管理类KuaFuCloudCacheManager

4.4.3 Stamp 缓存签章接口封装

4.4.3.1 StampManager 缓存签章接口

4.4.3.2 抽象Stamp签章管理类AbstractStampManager

4.4.3.3 抽象缓存计数类AbstractCountStampManager

4.4.4 JetCache直接使用封装

4.4.4.1 JetCache单例工具类JetCacheUtils

4.4.5 JetCache注解方式启动封装

4.4.5.1 JetCache加载配置类JetCacheConfiguration

4.4.5.2 JetCache手动启动注解

4.4.6 Jpa Hibernate二级缓存扩展

4.4.6.1 DomainDataStorageAccess 接口实现

4.4.6.2 RegionFactoryTemplate 模板扩展

五、缓存 Start 启动组件封装

5.1 组件说明

5.2 组件结构图

5.3 组件Maven依赖

5.4 组件功能实现源码

5.4.1 组件启动类

5.4.2 SPI加载机制配置文件


一、整体方案说明

1.1 需求说明

1、基于JetCache 缓存框架,整合Caffeine和Redis,实现一级(进程级)、二级(远程分布式)缓存。

2、基于该缓存方案,实现对Spring Cache缓存的扩展。

3、实现对Jpa Hibernate、Mybatis两款ORM框架二级缓存的扩展。

4、基于Redis缓存实现对SpringBoot Session 的共享会话。

5、Stamp 签章缓存实现。

6、组件最终封装成SpringBoot start 组件,方便一键使用。

1.2 整体方案实现组件结构图

二、Caffeine缓存实现

2.1 组件说明

封装成cache-caffeine-sdk组件,该组件封装了Caffeine缓存的相关实现扩展,最终通过CaffeineConfiguration配置启动。

2.2 组件结构图

2.3 组件Maven依赖

<dependencies>
    <dependency>
        <groupId>${project.groupId}</groupId>
        <artifactId>cache-core</artifactId>
    </dependency>

    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
    </dependency>
    </dependencies>

2.4 组件功能实现源码

2.4.1 CaffeineCacheManager扩展实现

package cn.kuafu.cloud.base.cache.caffeine.enhance;

import cn.kuafu.cloud.base.assistant.core.definition.constants.SymbolConstants;
import cn.kuafu.cloud.base.cache.core.properties.Expire;
import cn.kuafu.cloud.base.cache.core.properties.CacheProperties;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.caffeine.CaffeineCacheManager;

import java.util.Map;

/**
 * @author ningzhaosheng
 * @date 2022/5/13 12:11:55
 * @description 扩展的 CaffeineCacheManager
 * 用于支持 Caffeine 缓存可以针对实体进行单独的过期时间设定
 */
public class KuaFuCloudCaffeineCacheManager extends CaffeineCacheManager {
    private static final Logger log = LoggerFactory.getLogger(KuaFuCloudCaffeineCacheManager.class);

    private final CacheProperties cacheProperties;

    // 构造方法
    public KuaFuCloudCaffeineCacheManager(CacheProperties cacheProperties) {
        this.cacheProperties = cacheProperties;
        this.setAllowNullValues(cacheProperties.getAllowNullValues());
    }

    // 构造方法
    public KuaFuCloudCaffeineCacheManager(CacheProperties cacheProperties, String... cacheNames) {
        super(cacheNames);
        this.cacheProperties = cacheProperties;
        this.setAllowNullValues(cacheProperties.getAllowNullValues());
    }

    // 覆盖CaffeineCacheManager 的 createNativeCaffeineCache方法,实现Caffeine创建本地缓存方法
    // 主要支持Duration 过期时间配置
    @Override
    protected Cache<Object, Object> createNativeCaffeineCache(String name) {
        Map<String, Expire> expires = cacheProperties.getExpires();
        if (MapUtils.isNotEmpty(expires)) {
            String key = StringUtils.replace(name, SymbolConstants.COLON, cacheProperties.getSeparator());
            if(expires.containsKey(key)) {
                Expire expire = expires.get(key);
                log.debug("[KuaFuCloud-Base-Cache] |- CACHE - Caffeine cache [{}] is setted to use CUSTEM exprie.", name);
                return Caffeine.newBuilder().expireAfterWrite(expire.getDuration(), expire.getUnit()).build();
            }
        }

        return super.createNativeCaffeineCache(name);
    }
}

2.4.2 CaffeineConfiguration配置类实现

package cn.kuafu.cloud.base.cache.caffeine.configuration;

import cn.kuafu.cloud.base.cache.caffeine.enhance.KuaFuCloudCaffeineCacheManager;
import cn.kuafu.cloud.base.cache.core.properties.CacheProperties;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

/**
 * @author ningzhaosheng
 * @date 2022/5/13 14:15:20
 * @description Caffeine配置
 */
@Configuration(proxyBeanMethods = false)
public class CaffeineConfiguration {
    private static final Logger log = LoggerFactory.getLogger(CaffeineConfiguration.class);

    @Autowired
    private CacheProperties cacheProperties;

    @PostConstruct
    public void postConstruct() {
        log.debug("[KuaFuCloud-Base-Cache] |- SDK [Cache Caffeine] Auto Configure.");
    }

    // 注入 Caffeine缓存类
    @Bean
    public Caffeine<Object, Object> caffeine() {
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                .expireAfterWrite(cacheProperties.getDuration(), cacheProperties.getUnit());

        log.trace("[KuaFuCloud-Base-Cache] |- Bean [Caffeine] Auto Configure.");

        return caffeine;
    }

    // 注入 CaffeineCacheManager 类
    @Bean
    @ConditionalOnMissingBean(CaffeineCacheManager.class)
    public CaffeineCacheManager caffeineCacheManager(Caffeine<Object, Object> caffeine) {
        KuaFuCloudCaffeineCacheManager caffeineCacheManager = new KuaFuCloudCaffeineCacheManager(cacheProperties);
        caffeineCacheManager.setCaffeine(caffeine);
        log.trace("[KuaFuCloud-Base-Cache] |- Bean [Caffeine Cache Manager] Auto Configure.");
        return caffeineCacheManager;
    }
}

2.4.3 涉及其他组件的类

2.4.3.1 缓存过期时间通用属性类
package cn.kuafu.cloud.base.cache.core.properties;

import org.apache.commons.lang3.ObjectUtils;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

/**
 * @author ningzhaosheng
 * @date 2022/5/12 22:00:42
 * @description 缓存过期时间通用属性
 */
public class Expire {
    /**
     * 统一缓存时长,默认:1
     */
    private Long duration = 1L;

    /**
     * 统一缓存时长单位,默认:小时。
     */
    private TimeUnit unit = TimeUnit.HOURS;

    /**
     * Redis缓存TTL设置,默认:1小时,单位小时
     * <p>
     * 使用Duration类型,配置参数形式如下:
     * "?ns" //纳秒
     * "?us" //微秒
     * "?ms" //毫秒
     * "?s" //秒
     * "?m" //分
     * "?h" //小时
     * "?d" //天
     */
    private Duration ttl;

    public Long getDuration() {
        return duration;
    }

    public void setDuration(Long duration) {
        this.duration = duration;
    }

    public TimeUnit getUnit() {
        return unit;
    }

    public void setUnit(TimeUnit unit) {
        this.unit = unit;
    }

    public Duration getTtl() {
        if (ObjectUtils.isEmpty(this.ttl)) {
            this.ttl = convertToDuration(this.duration, this.unit);
        }
        return ttl;
    }

    private Duration convertToDuration(Long duration, TimeUnit timeUnit) {
        switch (timeUnit) {
            case DAYS:
                return Duration.ofDays(duration);
            case HOURS:
                return Duration.ofHours(duration);
            case SECONDS:
                return Duration.ofSeconds(duration);
            default:
                return Duration.ofMinutes(duration);
        }
    }
}
2.4.3.2 缓存配置类
package cn.kuafu.cloud.base.cache.core.properties;

import cn.kuafu.cloud.base.cache.core.constants.CacheConstants;
import com.google.common.base.MoreObjects;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.HashMap;
import java.util.Map;

/**
 * @author ningzhaosheng
 * @date 2022/5/13 11:55:06
 * @description 缓存配置属性
 */
@ConfigurationProperties(prefix = CacheConstants.PROPERTY_PREFIX_CACHE)
public class CacheProperties extends Expire {
    /**
     * 是否允许存储空值
     */
    private Boolean allowNullValues = true;

    /**
     * 缓存名称转换分割符。默认值,"-"
     * <p>
     * 默认缓存名称采用 Redis Key 格式(使用 ":" 分割),使用 ":" 分割的字符串作为Map的Key,":"会丢失。
     * 指定一个分隔符,用于 ":" 分割符的转换
     */
    private String separator = "-";

    /**
     * 针对不同实体单独设置的过期时间,如果不设置,则统一使用默认时间。
     */
    private Map<String, Expire> expires = new HashMap<>();

    public Boolean getAllowNullValues() {
        return allowNullValues;
    }

    public void setAllowNullValues(Boolean allowNullValues) {
        this.allowNullValues = allowNullValues;
    }

    public Map<String, Expire> getExpires() {
        return expires;
    }

    public void setExpires(Map<String, Expire> expires) {
        this.expires = expires;
    }

    public String getSeparator() {
        return separator;
    }

    public void setSeparator(String separator) {
        this.separator = separator;
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
                .add("allowNullValues", allowNullValues)
                .add("separator", separator)
                .toString();
    }
}

三、Redis缓存实现

3.1 组件说明

封装成cache-redis-sdk组件。

1、该组件封装了Redis缓存的相关实现扩展,最终通过CacheRedisConfiguration配置启动。

2、基于Redis,实现SpringBoot Session共享,支持通过条件判断和注解形式选择是否开启session共享

3.2 组件结构图

3.3 组件Maven依赖

<dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>cache-core</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>compile</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
            <scope>compile</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>

3.4 组件功能实现源码

3.4.1 实现Redis缓存功能

3.4.1.1 RedisCacheManager 类扩展实现
package cn.kuafu.cloud.base.cache.redis.enhance;

import cn.kuafu.cloud.base.assistant.core.definition.constants.SymbolConstants;
import cn.kuafu.cloud.base.cache.core.properties.Expire;
import cn.kuafu.cloud.base.cache.core.properties.CacheProperties;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;

import java.util.Map;

/**
 * @author ningzhaosheng
 * @date 2022/5/13 12:07:49
 * @description 扩展的RedisCacheManager
 * 用于支持 Redis 缓存可以针对实体进行单独的过期时间设定
 */
public class KuaFuCloudRedisCacheManager extends RedisCacheManager {
    private static final Logger log = LoggerFactory.getLogger(KuaFuCloudRedisCacheManager.class);

    private CacheProperties cacheProperties;

    // 构造函数
    public KuaFuCloudRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, CacheProperties cacheProperties) {
        super(cacheWriter, defaultCacheConfiguration);
        this.cacheProperties = cacheProperties;
    }

    // 构造函数
    public KuaFuCloudRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, CacheProperties cacheProperties, String... initialCacheNames) {
        super(cacheWriter, defaultCacheConfiguration, initialCacheNames);
        this.cacheProperties = cacheProperties;
    }

    // 构造函数
    public KuaFuCloudRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, boolean allowInFlightCacheCreation, CacheProperties cacheProperties, String... initialCacheNames) {
        super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheNames);
        this.cacheProperties = cacheProperties;
    }

    // 构造函数
    public KuaFuCloudRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations, CacheProperties cacheProperties) {
        super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
        this.cacheProperties = cacheProperties;
    }

    // 构造函数
    public KuaFuCloudRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
        super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);
    }

     // 覆盖实现 RedisCacheManager 类的 createRedisCache方法,加入自定义缓存过期时间策略
    @Override
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
        Map<String, Expire> expires = cacheProperties.getExpires();
        if (MapUtils.isNotEmpty(expires)) {
            String key = StringUtils.replace(name, SymbolConstants.COLON, cacheProperties.getSeparator());
            if (expires.containsKey(key)) {
                Expire expire = expires.get(key);
                log.debug("[KuaFuCloud-Base-Cache] |- CACHE - Redis cache [{}] is setted to use CUSTEM exprie.", name);
                cacheConfig = cacheConfig.entryTtl(expire.getTtl());
            }
        }

        return super.createRedisCache(name, cacheConfig);
    }
}
3.4.1.2 RedisBitMapUtils工具类
package cn.kuafu.cloud.base.cache.redis.util;

import com.google.common.hash.Funnels;
import com.google.common.hash.Hashing;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;

/**
 * @author ningzhaosheng
 * @date 2023/2/16 16:24:06
 * @description <p>Description: Redis BitMap 工具类 </p>
 * <p>
 * · Redis的Bitmaps这个“数据结构”可以实现对位的操作。Bitmaps本身不是一种数据结构,实际上就是字符串,但是它可以对字符串的位进行操作
 * · 可把Bitmaps想象成一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在bitmaps中叫做偏移量
 * · 单个bitmaps的最大长度是512MB,即2^32个比特位
 * <p>
 * bitmaps的最大优势是节省存储空间。比如在一个以自增id代表不同用户的系统中,我们只需要512MB空间就可以记录40亿用户的某个单一信息,相比mysql节省了大量的空间
 * <p>
 * · 有两种类型的位操作:一类是对特定bit位的操作,比如设置/获取某个特定比特位的值。另一类是批量bit位操作,例如在给定范围内统计为1的比特位个数
 * <p>
 * 1.1 优点:
 * <p>
 * 节省空间:通过一个bit位来表示某个元素对应的值或者状态,其中key就是对应元素的值。实际上8个bit可以组成一个Byte,所以是及其节省空间的。
 * <p>
 * 效率高:setbit和getbit的时间复杂度都是O(1),其他位运算效率也高。
 * <p>
 * 1.2 缺点:
 * 本质上位只有0和1的区别,所以用位做业务数据记录,就不需要在意value的值。
 * @see <a herf="https://www.jianshu.com/p/305e65de1b13"></a>
 */
@Component
public class RedisBitMapUtils {
    private static StringRedisTemplate stringRedisTemplate;

    @Autowired
    @Qualifier(value = "stringRedisTemplate")
    public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
        RedisBitMapUtils.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 计算 Hash 值
     *
     * @param key bitmap结构的key
     * @return Hash 值
     */
    private static long hash(String key) {
        return Math.abs(Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(StandardCharsets.UTF_8)).asInt());
    }


    /**
     * 设置与 param 对应的二进制位的值,{@param param}会经过hash计算进行存储。
     *
     * @param key   bitmap数据结构的key
     * @param param 要设置偏移的key,该key会经过hash运算。
     * @param value true:即该位设置为1,否则设置为0
     * @return 返回设置该value之前的值。
     */
    public static Boolean setBit(String key, String param, boolean value) {
        return stringRedisTemplate.opsForValue().setBit(key, hash(param), value);
    }

    /**
     * 查询与指定 param 对应二进制位的值,{@param param}会经过hash计算进行存储。
     *
     * @param key   bitmap结构的key
     * @param param 要移除偏移的key,该key会经过hash运算。
     * @return 若偏移位上的值为1,那么返回true。
     */
    public static boolean getBit(String key, String param) {
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().getBit(key, hash(param)));
    }

    /**
     * 将指定offset偏移量的值设置为1;
     *
     * @param key    bitmap结构的key
     * @param offset 指定的偏移量。
     * @param value  true:即该位设置为1,否则设置为0
     * @return 返回设置该value之前的值。
     */
    public static Boolean setBit(String key, Long offset, boolean value) {
        return stringRedisTemplate.opsForValue().setBit(key, offset, value);
    }

    /**
     * 获取指定 offset 偏移量的值;
     *
     * @param key    bitmap结构的key
     * @param offset 指定的偏移量。
     * @return 若偏移位上的值为 1,那么返回true。
     */
    public static Boolean getBit(String key, long offset) {
        return stringRedisTemplate.opsForValue().getBit(key, offset);
    }

    /**
     * 统计对应的bitmap上value为1的数量
     *
     * @param key bitmap的key
     * @return value等于1的数量
     */
    public static Long bitCount(String key) {
        return stringRedisTemplate.execute((RedisCallback<Long>) connection -> connection.stringCommands().bitCount(key.getBytes(StandardCharsets.UTF_8)));
    }

    /**
     * 统计指定范围中value为1的数量
     *
     * @param key   bitMap中的key
     * @param start 该参数的单位是byte(1byte=8bit),{@code setBit(key,7,true);}进行存储时,单位是bit。那么只需要统计[0,1]便可以统计到上述set的值。
     * @param end   该参数的单位是byte。
     * @return 在指定范围[start*8,end*8]内所有value=1的数量
     */
    public static Long bitCount(String key, int start, int end) {
        return stringRedisTemplate.execute((RedisCallback<Long>) connection -> connection.stringCommands().bitCount(key.getBytes(), start, end));
    }

    /**
     * 对一个或多个保存二进制的字符串key进行元操作,并将结果保存到saveKey上。
     * <p>
     * bitop and saveKey key [key...],对一个或多个key逻辑并,结果保存到saveKey。
     * bitop or saveKey key [key...],对一个或多个key逻辑或,结果保存到saveKey。
     * bitop xor saveKey key [key...],对一个或多个key逻辑异或,结果保存到saveKey。
     * bitop xor saveKey key,对一个或多个key逻辑非,结果保存到saveKey。
     * <p>
     *
     * @param op      元操作类型;
     * @param saveKey 元操作后将结果保存到saveKey所在的结构中。
     * @param destKey 需要进行元操作的类型。
     * @return 1:返回元操作值。
     */
    public static Long bitOp(RedisStringCommands.BitOperation op, String saveKey, String... destKey) {
        byte[][] bytes = new byte[destKey.length][];
        for (int i = 0; i < destKey.length; i++) {
            bytes[i] = destKey[i].getBytes();
        }
        return stringRedisTemplate.execute((RedisCallback<Long>) connection -> connection.stringCommands().bitOp(op, saveKey.getBytes(), bytes));
    }

    /**
     * 对一个或多个保存二进制的字符串key进行元操作,并将结果保存到saveKey上,并返回统计之后的结果。
     *
     * @param op      元操作类型;
     * @param saveKey 元操作后将结果保存到saveKey所在的结构中。
     * @param destKey 需要进行元操作的类型。
     * @return 返回saveKey结构上value=1的所有数量值。
     */
    public static Long bitOpResult(RedisStringCommands.BitOperation op, String saveKey, String... destKey) {
        bitOp(op, saveKey, destKey);
        return bitCount(saveKey);
    }
}
3.4.1.3 CacheRedisConfiguration 缓存配置类实现
package cn.kuafu.cloud.base.cache.redis.configuration;

import cn.kuafu.cloud.base.cache.core.properties.CacheProperties;
import cn.kuafu.cloud.base.cache.redis.enhance.KuaFuCloudRedisCacheManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
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.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.annotation.PostConstruct;

/**
 * @author ningzhaosheng
 * @date 2022/5/13 14:26:19
 * @description Redis缓存配置
 */
@Configuration(proxyBeanMethods = false)
@Import(RedisSessionSharingConfiguration.class)
public class CacheRedisConfiguration {
    private static final Logger log = LoggerFactory.getLogger(CacheRedisConfiguration.class);

    @PostConstruct
    public void postConstruct() {
        log.debug("[KuaFuCloud-Base-Cache] |- SDK [Cache Redis] Auto Configure.");
    }

    private RedisSerializer<String> keySerializer() {
        return new StringRedisSerializer();
    }

    private RedisSerializer<Object> valueSerializer() {
        return new Jackson2JsonRedisSerializer<>(Object.class);
    }

    /**
     * 重新配置一个RedisTemplate
     *
     * @return {@link RedisTemplate}
     */
    @Bean(name = "redisTemplate")
    @ConditionalOnMissingBean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(keySerializer());
        redisTemplate.setHashKeySerializer(keySerializer());
        redisTemplate.setValueSerializer(valueSerializer());
        redisTemplate.setHashValueSerializer(valueSerializer());
        redisTemplate.setDefaultSerializer(valueSerializer());
        redisTemplate.afterPropertiesSet();

        log.trace("[KuaFuCloud-Base-Cache] |- Bean [Redis Template] Auto Configure.");

        return redisTemplate;
    }

    // 注入一个 StringRedisTemplate 
    @Bean(name = "stringRedisTemplate")
    @ConditionalOnMissingBean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
        stringRedisTemplate.afterPropertiesSet();

        log.trace("[KuaFuCloud-Base-Cache] |- Bean [String Redis Template] Auto Configure.");

        return stringRedisTemplate;
    }

    // 注入一个Redis 消息的核心监听容器
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        log.trace("[KuaFuCloud-Base-Cache] |- Bean [Redis Message Listener Container] Auto Configure.");
        return container;
    }

    //  注入Redis 缓存管理类
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory, CacheProperties cacheProperties) {
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);

        // 注意:这里 RedisCacheConfiguration 每一个方法调用之后,都会返回一个新的 RedisCacheConfiguration 对象,所以要注意对象的引用关系。
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(cacheProperties.getTtl());

        boolean allowNullValues = cacheProperties.getAllowNullValues();
        if (!allowNullValues) {
            // 注意:这里 RedisCacheConfiguration 每一个方法调用之后,都会返回一个新的 RedisCacheConfiguration 对象,所以要注意对象的引用关系。
            redisCacheConfiguration = redisCacheConfiguration.disableCachingNullValues();
        }

        KuaFuCloudRedisCacheManager kuaFuCloudRedisCacheManager = new KuaFuCloudRedisCacheManager(redisCacheWriter, redisCacheConfiguration, cacheProperties);
        kuaFuCloudRedisCacheManager.setTransactionAware(false);
        kuaFuCloudRedisCacheManager.afterPropertiesSet();
        log.trace("[KuaFuCloud-Base-Cache] |- Bean [Redis Cache Manager] Auto Configure.");
        return kuaFuCloudRedisCacheManager;
    }

   // 扫描 RedisBitMapUtils 工具类
    @Configuration(proxyBeanMethods = false)
    @ComponentScan({
            "cn.kuafu.cloud.base.cache.redis.util"
    })
    static class RedisUtilsConfiguration {

        @PostConstruct
        public void postConstruct() {
            log.debug("[KuaFuCloud-Base-Cache] |- SDK [Cache Redis Utils] Auto Configure.");
        }
    }
}
3.4.1.4 EnableKuaFuCloudRedis Redis启动注解
package cn.kuafu.cloud.base.cache.redis.annotation;

import cn.kuafu.cloud.base.cache.redis.configuration.CacheRedisConfiguration;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

/**
 * @author ningzhaosheng
 * @date 2023/2/16 16:12:04
 * @description 开启平台Redis
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CacheRedisConfiguration.class)
public @interface EnableKuaFuCloudRedis {
}

3.4.2 实现Spring Session的Redis缓存

在Web应用中,Session是一种用来存储用户状态信息的机制。用户通过登录认证后,服务器会为每个用户生成一个唯一的Session ID,并将这个Session ID与用户的状态信息关联起来,然后将Session ID返回给客户端保存在Cookie中。客户端在后续的请求中通过Cookie将Session ID发送给服务器,服务器根据Session ID找到对应的用户状态信息。

通常情况下,每个Web应用都会有自己的Session管理机制,这意味着不同的应用之间的Session是相互隔离的。但在某些场景下,我们希望多个Web应用之间可以实现Session共享,即一个Web应用生成的Session可以在另一个Web应用中被识别和使用。这样可以实现用户在不同的应用之间的无缝切换,提升用户体验。

Spring Session通过替换底层的HttpSession实现类,将Session存储到Redis中。具体而言,当用户访问Spring Boot应用时,Spring Session会将生成的Session ID写入Cookie,然后将Session的状态信息存储到Redis中。当用户发送后续的请求时,Spring Session会根据Cookie中的Session ID从Redis中读取Session的状态信息,并将其保存在HttpSession对象中,供应用程序使用。

3.4.2.1 HttpSessionIdResolver SessionId 解析类扩展
package cn.kuafu.cloud.base.cache.redis.session;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.session.web.http.CookieHttpSessionIdResolver;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.session.web.http.HttpSessionIdResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collections;
import java.util.List;

/**
 * @author ningzhaosheng
 * @date 2023/2/16 16:28:03
 * @description 扩展的 HttpSessionIdResolver, 以同时支持页面和接口的 Session 共享
 */
public class KuaFuCloudHttpSessionIdResolver implements HttpSessionIdResolver {
    private static final Logger log = LoggerFactory.getLogger(KuaFuCloudHttpSessionIdResolver.class);

    private static final String WRITTEN_SESSION_ID_ATTR = CookieHttpSessionIdResolver.class.getName()
            .concat(".WRITTEN_SESSION_ID_ATTR");

    private final String headerName;
    private CookieSerializer cookieSerializer = new DefaultCookieSerializer();

    /**
     * 构造函数
     *
     * @param headerName
     */
    public KuaFuCloudHttpSessionIdResolver(String headerName) {
        if (StringUtils.isBlank(headerName)) {
            throw new IllegalArgumentException("headerName cannot be null");
        }
        this.headerName = headerName;
    }

    private String resolveHeaderSessionId(HttpServletRequest request) {
        String headerValue = request.getHeader(this.headerName);
        if (StringUtils.isNotBlank(headerValue)) {
            log.debug("[KuaFuCloud-Base-Cache] |- Resolve http session id [{}] from header in request [{}]", headerValue, request.getRequestURI());
            return headerValue;
        }
        return null;
    }

    private List<String> resolveHeaderSessionIds(HttpServletRequest request) {
        String id = resolveHeaderSessionId(request);
        return StringUtils.isNotBlank(id) ? Collections.singletonList(id) : Collections.emptyList();
    }

    /**
     * 解析与当前请求相关联的sessionId。sessionId可能来自Cookie或请求头。
     * 实现该方法,替换Cookies序列化方式
     *
     * @param request
     * @return
     */
    @Override
    public List<String> resolveSessionIds(HttpServletRequest request) {

        List<String> idsInHeader = resolveHeaderSessionIds(request);
        if (CollectionUtils.isNotEmpty(idsInHeader)) {
            return idsInHeader;
        } else {
            return this.cookieSerializer.readCookieValues(request);
        }
    }

    private void changeSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
        this.cookieSerializer.writeCookieValue(new CookieSerializer.CookieValue(request, response, sessionId));
        response.setHeader(this.headerName, sessionId);
    }

    /**
     * 将给定的sessionId发送给客户端。
     * 这个方法是在创建一个新session时被调用,并告知客户端新sessionId是什么。
     *
     * @param request
     * @param response
     * @param sessionId
     */
    @Override
    public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {

        if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
            return;
        }


        String id = sessionId;
        String kuafuCloudSessionId = resolveHeaderSessionId(request);
        if (StringUtils.isNotBlank(kuafuCloudSessionId)) {
            id = kuafuCloudSessionId;
        }

        request.setAttribute(WRITTEN_SESSION_ID_ATTR, id);
        changeSessionId(request, response, id);
    }

    /**
     * 指示客户端结束当前session。当session无效时调用此方法,并
     * 应通知客户端sessionId不再有效。比如,它可能删除一个包含sessionId的Cookie,
     * 或者设置一个HTTP响应头,其值为空就表示客户端不再提交sessionId
     *
     * @param request
     * @param response
     */
    @Override
    public void expireSession(HttpServletRequest request, HttpServletResponse response) {
        changeSessionId(request, response, "");
    }

    /**
     * Sets the {@link CookieSerializer} to be used.
     *
     * @param cookieSerializer the cookieSerializer to set. Cannot be null.
     */
    public void setCookieSerializer(CookieSerializer cookieSerializer) {
        if (cookieSerializer == null) {
            throw new IllegalArgumentException("cookieSerializer cannot be null");
        }
        this.cookieSerializer = cookieSerializer;
    }
}
3.4.2.2 RedisSessionSharingConfiguration 启动配置类
package cn.kuafu.cloud.base.cache.redis.configuration;

import cn.kuafu.cloud.base.cache.redis.annotation.ConditionalOnRedisSessionSharing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.FlushMode;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

import javax.annotation.PostConstruct;

/**
 * @author ningzhaosheng
 * @date 2022/6/11 18:18:35
 * @description 基于 Redis 的 Session 共享配置,
 * @EnableRedisHttpSession 注解开启SpringBoot Session
 * @ConditionalOnRedisSessionSharing 自定义条件注解,满足条件该配置类才启动加载
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnRedisSessionSharing
public class RedisSessionSharingConfiguration {
    private static final Logger log = LoggerFactory.getLogger(RedisSessionSharingConfiguration.class);

    @PostConstruct
    public void postConstruct() {
        log.debug("[KuaFuCloud-Base-Cache] |- SDK [Cache Redis Session Sharing] Auto Configure.");
    }


    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    @EnableRedisHttpSession(flushMode = FlushMode.IMMEDIATE)
    static class KuaFuCloudRedisHttpSessionConfiguration {
        /**
         *  注入CookieSerializer 实现Cookies 序列化
         * @return
         */
        @Bean
        public CookieSerializer cookieSerializer() {
            DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
            cookieSerializer.setUseHttpOnlyCookie(false);
            cookieSerializer.setSameSite(null);
            cookieSerializer.setCookiePath("/");
            cookieSerializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
            log.trace("[KuaFuCloud-Base-Cache] |- Bean [Cookie Serializer] Auto Configure.");
            return cookieSerializer;
        }
    }
}
3.4.2.3 注解生效条件类RedisSessionSharingCondition
package cn.kuafu.cloud.base.cache.redis.condition;

import cn.kuafu.cloud.base.assistant.core.context.PropertyFinder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * @author ningzhaosheng
 * @date 2022/6/11 18:16:11
 * @description 开启基于 Redis 的 Session 共享条件
 */
public class RedisSessionSharingCondition implements Condition {
    private static final Logger log = LoggerFactory.getLogger(RedisSessionSharingCondition.class);

    @SuppressWarnings("NullableProblems")
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        String property = PropertyFinder.getSessionStoreType(conditionContext.getEnvironment());
        boolean result = StringUtils.isNotBlank(property) && StringUtils.equalsIgnoreCase(property, "redis");
        log.debug("[KuaFuCloud-Base-Cache] |- Condition [Redis Session Sharing] value is [{}]", result);
        return result;
    }
}
3.4.2.4 ConditionalOnRedisSessionSharing 注解类
package cn.kuafu.cloud.base.cache.redis.annotation;

import cn.kuafu.cloud.base.cache.redis.condition.RedisSessionSharingCondition;
import org.springframework.context.annotation.Conditional;

import java.lang.annotation.*;

/**
 * @author ningzhaosheng
 * @date 2022/6/11 18:15:21
 * @description 基于 Redis Session 共享条件注解
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(RedisSessionSharingCondition.class)
public @interface ConditionalOnRedisSessionSharing {
}

四、JetCache缓存框架整合

4.1 组件说明

封装成cache-jetcache-sdk组件。

1、集成Caffeine一级缓存

2、集成Redis二级缓存

3、扩展Spring Cache 缓存

4、扩展Mybatis 二级缓存

5、Stamp 签章缓存支持

4.2 组件结构图

4.3 组件Maven依赖

<dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>assistant-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>cache-core</artifactId>
        </dependency>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>cache-redis-sdk</artifactId>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>cache-caffeine-sdk</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alicp.jetcache</groupId>
            <artifactId>jetcache-starter-redis-lettuce</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <scope>compile</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>

4.4 组件功能实现源码

4.4.1 Mybatis二级缓存扩展

4.4.1.1 ExtMybatisCache 实现类
package cn.kuafu.cloud.base.cache.jetcache.enhance;

import cn.hutool.extra.spring.SpringUtil;
import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author ningzhaosheng
 * @date 2022/5/13 9:57:24
 * @description 扩展的Mybatis二级缓存
 */
public class ExtMybatisCache implements Cache {

    private static final Logger log = LoggerFactory.getLogger(ExtMybatisCache.class);

    private final String id;
    private final com.alicp.jetcache.Cache<Object, Object> cache;
    private final AtomicInteger counter = new AtomicInteger(0);

    public ExtMybatisCache(String id) {
        this.id = id;
        JetCacheCreateCacheFactory jetCacheCreateCacheFactory = SpringUtil.getBean("jetCacheCreateCacheFactory");
        this.cache = jetCacheCreateCacheFactory.create(this.id);
    }

    /**
     * 获取缓存对象标识
     * @return
     */
    @Override
    public String getId() {
        return this.id;
    }

    /**
     * 往缓存中放数据
     * @param key
     * @param value
     */
    @Override
    public void putObject(Object key, Object value) {
        cache.put(key, value);
        counter.incrementAndGet();
        log.debug("[KuaFuCloud-Base-Cache] |- CACHE - Put data into Mybatis Cache, with key: [{}]", key);
    }

    /**
     * 获取缓存数据
     * @param key
     * @return
     */
    @Override
    public Object getObject(Object key) {
        Object obj = cache.get(key);
        log.debug("[KuaFuCloud-Base-Cache] |- CACHE - Get data from Mybatis Cache, with key: [{}]", key);
        return obj;
    }

    /**
     * 删除缓存数据
     * @param key
     * @return
     */
    @Override
    public Object removeObject(Object key) {
        Object obj = cache.remove(key);
        counter.decrementAndGet();
        log.debug("[KuaFuCloud-Base-Cache] |- CACHE - Remove data from Mybatis Cache, with key: [{}]", key);
        return obj;
    }

    /**
     * 清空缓存数据
     */
    @Override
    public void clear() {
        cache.close();
        log.debug("[KuaFuCloud-Base-Cache] |- CACHE - Clear Mybatis Cache.");
    }

    /**
     * 获取缓存数据的数量
     * @return
     */
    @Override
    public int getSize() {
        return counter.get();
    }
}

4.4.2 Spring Cache缓存扩展

4.4.2.1 JetCache缓存工厂类JetCacheCreateCacheFactory
package cn.kuafu.cloud.base.cache.jetcache.enhance;

import com.alicp.jetcache.Cache;
import com.alicp.jetcache.CacheManager;
import com.alicp.jetcache.anno.CacheType;
import com.alicp.jetcache.template.QuickConfig;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;

import java.time.Duration;

/**
 * @author ningzhaosheng
 * @date 2022/8/23 16:58:02
 * @description JetCache 手动创建Cache 工厂
 */
public class JetCacheCreateCacheFactory {
    private final CacheManager cacheManager;

    public JetCacheCreateCacheFactory(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    public <K, V> Cache<K, V> create(String name) {
        return create(name, Duration.ofHours(2L));
    }

    public <K, V> Cache<K, V> create(String name, Duration expire) {
        return create(name, expire, true);
    }

    public <K, V> Cache<K, V> create(String name, Duration expire, Boolean cacheNullValue) {
        return create(name, expire, cacheNullValue, null);
    }

    public <K, V> Cache<K, V> create(String name, Duration expire, Boolean cacheNullValue, Boolean syncLocal) {
        return create(name, CacheType.BOTH, expire, cacheNullValue, syncLocal);
    }

    public <K, V> Cache<K, V> create(String name, CacheType cacheType) {
        return create(name, cacheType, null);
    }

    public <K, V> Cache<K, V> create(String name, CacheType cacheType, Duration expire) {
        return create(name, cacheType, expire, true);
    }

    public <K, V> Cache<K, V> create(String name, CacheType cacheType, Duration expire, Boolean cacheNullValue) {
        return create(name, cacheType, expire, cacheNullValue, null);
    }

    public <K, V> Cache<K, V> create(String name, CacheType cacheType, Duration expire, Boolean cacheNullValue, Boolean syncLocal) {
        return create(null, name, cacheType, expire, cacheNullValue, syncLocal);
    }

    public <K, V> Cache<K, V> create(String area, String name, CacheType cacheType, Duration expire, Boolean cacheNullValue, Boolean syncLocal) {
        return create(area, name, cacheType, expire, cacheNullValue, syncLocal, null);
    }

    public <K, V> Cache<K, V> create(String area, String name, CacheType cacheType, Duration expire, Boolean cacheNullValue, Boolean syncLocal, Duration localExpire) {
        return create(area, name, cacheType, expire, cacheNullValue, syncLocal, localExpire, null);
    }

    public <K, V> Cache<K, V> create(String area, String name, CacheType cacheType, Duration expire, Boolean cacheNullValue, Boolean syncLocal, Duration localExpire, Integer localLimit) {
        return create(area, name, cacheType, expire, cacheNullValue, syncLocal, localExpire, localLimit, false);
    }

    public <K, V> Cache<K, V> create(String area, String name, CacheType cacheType, Duration expire, Boolean cacheNullValue, Boolean syncLocal, Duration localExpire, Integer localLimit, Boolean useAreaInPrefix) {
        return create(area, name, cacheType, expire, cacheNullValue, syncLocal, localExpire, localLimit, useAreaInPrefix, false, null);
    }

    public <K, V> Cache<K, V> create(String area, String name, CacheType cacheType, Duration expire, Boolean cacheNullValue, Boolean syncLocal, Duration localExpire, Integer localLimit, Boolean useAreaInPrefix, Boolean penetrationProtect, Duration penetrationProtectTimeout) {
        QuickConfig.Builder builder = StringUtils.isEmpty(area) ? QuickConfig.newBuilder(name) : QuickConfig.newBuilder(area, name);
        builder.cacheType(cacheType);
        builder.expire(expire);
        if (cacheType == CacheType.BOTH) {
            builder.syncLocal(syncLocal);
        }
        builder.localExpire(localExpire);
        builder.localLimit(localLimit);
        builder.cacheNullValue(cacheNullValue);
        builder.useAreaInPrefix(useAreaInPrefix);
        if (ObjectUtils.isNotEmpty(penetrationProtect)) {
            builder.penetrationProtect(penetrationProtect);
            if (BooleanUtils.isTrue(penetrationProtect) && ObjectUtils.isNotEmpty(penetrationProtectTimeout)) {
                builder.penetrationProtectTimeout(penetrationProtectTimeout);
            }
        }

        QuickConfig quickConfig = builder.build();
        return create(quickConfig);
    }


    @SuppressWarnings("unchecked")
    private <K, V> Cache<K, V> create(QuickConfig quickConfig) {
        return cacheManager.getOrCreateCache(quickConfig);
    }
}
4.4.2.2 Spring Cache扩展实现类JetCacheSpringCache
package cn.kuafu.cloud.base.cache.jetcache.enhance;

import cn.hutool.crypto.SecureUtil;
import cn.kuafu.cloud.base.assistant.core.json.jackson2.utils.JacksonUtils;
import com.alicp.jetcache.Cache;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.lang.Nullable;

import java.util.concurrent.Callable;

/**
 * @author ningzhaosheng
 * @date 2022/8/23 16:54:36
 * @description 基于 JetCache 的 Spring Cache 扩展
 */
public class JetCacheSpringCache extends AbstractValueAdaptingCache {
    private static final Logger log = LoggerFactory.getLogger(JetCacheSpringCache.class);

    private final String cacheName;
    private final Cache<Object, Object> cache;

    public JetCacheSpringCache(String cacheName, Cache<Object, Object> cache, boolean allowNullValues) {
        super(allowNullValues);
        this.cacheName = cacheName;
        this.cache = cache;
    }


    @Override
    public String getName() {
        return this.cacheName;
    }

    @Override
    public final Cache<Object, Object> getNativeCache() {
        return this.cache;
    }

    @Override
    @Nullable
    protected Object lookup(Object key) {
        Object value = cache.get(key);
        if (ObjectUtils.isNotEmpty(value)) {
            log.trace("[KuaFuCloud-Base-Cache] |- CACHE - Lookup data in KuaFuCloud-Base-Cache cache, value is : [{}]", JacksonUtils.toJson(value));
            return value;
        }

        return null;
    }

    @SuppressWarnings("unchecked")
    @Override
    @Nullable
    public <T> T get(Object key, Callable<T> valueLoader) {
        log.trace("[KuaFuCloud-Base-Cache] |- CACHE - Get data in KuaFuCloud-Base-Cache cache, key: {}", key);

        return (T) fromStoreValue(cache.computeIfAbsent(key, k -> {
            try {
                return toStoreValue(valueLoader.call());
            } catch (Throwable ex) {
                throw new ValueRetrievalException(key, valueLoader, ex);
            }
        }));
    }

    @Override
    @Nullable
    public void put(Object key, @Nullable Object value) {
        log.trace("[KuaFuCloud-Base-Cache] |- CACHE - Put data in KuaFuCloud-Base-Cache cache, key: {}", key);
        cache.put(key, this.toStoreValue(value));
    }


    @Override
    @Nullable
    public ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
        log.trace("[KuaFuCloud-Base-Cache] |- CACHE - PutIfPresent data in KuaFuCloud-Base-Cache cache, key: {}", key);
        Object existing = cache.putIfAbsent(key, toStoreValue(value));
        return toValueWrapper(existing);
    }

    @Override
    public void evict(Object key) {
        log.trace("[KuaFuCloud-Base-Cache] |- CACHE - Evict data in KuaFuCloud-Base-Cache cache, key: {}", key);
        cache.remove(key);
    }

    @Override
    public boolean evictIfPresent(Object key) {
        log.trace("[KuaFuCloud-Base-Cache] |- CACHE - EvictIfPresent data in KuaFuCloud-Base-Cache cache, key: {}", key);
        return cache.remove(key);
    }

    @Override
    public void clear() {
        log.trace("[KuaFuCloud-Base-Cache] |- CACHE - Clear data in KuaFuCloud-Base-Cache cache.");
        cache.close();
    }
}
4.4.2.3 Spring CacheManager扩展实现类JetCacheSpringCacheManager
package cn.kuafu.cloud.base.cache.jetcache.enhance;

import cn.kuafu.cloud.base.assistant.core.definition.constants.SymbolConstants;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.lang.Nullable;

import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author ningzhaosheng
 * @date 2022/8/23 17:01:32
 * @description 基于 JetCache 的 Spring Cache Manager 扩展
 */
public class JetCacheSpringCacheManager implements CacheManager {
    private static final Logger log = LoggerFactory.getLogger(JetCacheSpringCacheManager.class);

    private boolean dynamic = true;
    private boolean allowNullValues = true;

    private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>(16);

    private final JetCacheCreateCacheFactory jetCacheCreateCacheFactory;

    public JetCacheSpringCacheManager(JetCacheCreateCacheFactory jetCacheCreateCacheFactory) {
        this.jetCacheCreateCacheFactory = jetCacheCreateCacheFactory;
    }

    public JetCacheSpringCacheManager(JetCacheCreateCacheFactory jetCacheCreateCacheFactory, String... cacheNames) {
        this.jetCacheCreateCacheFactory = jetCacheCreateCacheFactory;
        setCacheNames(Arrays.asList(cacheNames));
    }

    public void setAllowNullValues(boolean allowNullValues) {
        this.allowNullValues = allowNullValues;
    }


    public boolean isAllowNullValues() {
        return allowNullValues;
    }


    private void setCacheNames(@Nullable Collection<String> cacheNames) {
        if (cacheNames != null) {
            for (String name : cacheNames) {
                this.cacheMap.put(name, createJetCache(name));
            }
            this.dynamic = false;
        } else {
            this.dynamic = true;
        }
    }

    protected Cache createJetCache(String name) {
        com.alicp.jetcache.Cache<Object, Object> cache = jetCacheCreateCacheFactory.create(name);
        log.debug("[KuaFuCloud-Base-Cache] |- CACHE - KuaFuCloud cache [{}] is CREATED.", name);
        return new JetCacheSpringCache(name, cache, allowNullValues);
    }

    protected Cache createJetCache(String name, Duration expire) {
        com.alicp.jetcache.Cache<Object, Object> cache = jetCacheCreateCacheFactory.create(name, expire, allowNullValues, true);
        log.debug("[KuaFuCloud-Base-Cache] |- CACHE - KuaFuCloud cache [{}] with expire is CREATED.", name);
        return new JetCacheSpringCache(name, cache, allowNullValues);
    }

    private String availableCacheName(String name) {
        if (StringUtils.endsWith(name, SymbolConstants.COLON)) {
            return name;
        } else {
            return name + SymbolConstants.COLON;
        }
    }


    @Override
    @Nullable
    public Cache getCache(String name) {
        String usedName = availableCacheName(name);
        return this.cacheMap.computeIfAbsent(usedName, cacheName ->
                this.dynamic ? createJetCache(cacheName) : null);
    }

    @Override
    public Collection<String> getCacheNames() {
        return Collections.unmodifiableSet(this.cacheMap.keySet());
    }
}
4.4.2.4 自定义缓存管理类KuaFuCloudCacheManager
package cn.kuafu.cloud.base.cache.jetcache.enhance;

import cn.kuafu.cloud.base.assistant.core.definition.constants.SymbolConstants;
import cn.kuafu.cloud.base.cache.core.properties.CacheProperties;
import cn.kuafu.cloud.base.cache.core.properties.Expire;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;

import java.util.Map;

/**
 * @author ningzhaosheng
 * @date 2022/8/23 16:59:23
 * @description 自定义 缓存管理器
 */
public class KuaFuCloudCacheManager extends JetCacheSpringCacheManager {
    private static final Logger log = LoggerFactory.getLogger(KuaFuCloudCacheManager.class);

    private final CacheProperties cacheProperties;

    public KuaFuCloudCacheManager(JetCacheCreateCacheFactory jetCacheCreateCacheFactory, CacheProperties cacheProperties) {
        super(jetCacheCreateCacheFactory);
        this.cacheProperties = cacheProperties;
        this.setAllowNullValues(cacheProperties.getAllowNullValues());
    }

    public KuaFuCloudCacheManager(JetCacheCreateCacheFactory jetCacheCreateCacheFactory, CacheProperties cacheProperties, String... cacheNames) {
        super(jetCacheCreateCacheFactory, cacheNames);
        this.cacheProperties = cacheProperties;
    }

    /**
     * 覆盖createJetCache 方法,加入过期时间策略
     * @param name
     * @return
     */
    @Override
    protected Cache createJetCache(String name) {
        Map<String, Expire> expires = cacheProperties.getExpires();
        if (MapUtils.isNotEmpty(expires)) {
            String key = StringUtils.replace(name, SymbolConstants.COLON, cacheProperties.getSeparator());
            if (expires.containsKey(key)) {
                Expire expire = expires.get(key);
                log.debug("[KuaFuCloud-Base-Cache] |- CACHE - Cache [{}] is set to use CUSTOM expire.", name);
                return super.createJetCache(name, expire.getTtl());
            }
        }
        return super.createJetCache(name);
    }
}

4.4.3 Stamp 缓存签章接口封装

4.4.3.1 StampManager 缓存签章接口
package cn.kuafu.cloud.base.cache.jetcache.stamp;

import cn.kuafu.cloud.base.cache.core.exception.StampDeleteFailedException;
import cn.kuafu.cloud.base.cache.core.exception.StampHasExpiredException;
import cn.kuafu.cloud.base.cache.core.exception.StampMismatchException;
import cn.kuafu.cloud.base.cache.core.exception.StampParameterIllegalException;
import com.alicp.jetcache.AutoReleaseLock;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.InitializingBean;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

/**
 * @author ningzhaosheng
 * @date 2022/5/13 10:14:28
 * @description Stamp 服务接口
 * 此Stamp非OAuth2 Stamp。而是用于在特定条件下生成后,在一定时间就会消除的标记性Stamp。
 * 例如,幂等、短信验证码、Auth State等,用时生成,然后进行验证,之后再删除的标记Stamp。
 * @param <K> 签章缓存对应Key值的类型。
 * @param <V> 签章缓存存储数据,对应的具体存储值的类型,
 */
public interface StampManager<K,V> extends InitializingBean {
    /**
     * 过期时间
     *
     * @return {@link Duration}
     */
    Duration getExpire();

    /**
     * 保存与Key对应的Stamp签章值
     *
     * @param key              存储Key
     * @param value            与Key对应的Stamp
     * @param expireAfterWrite 过期时间
     * @param timeUnit         过期时间单位
     */
    void put(K key, V value, long expireAfterWrite, TimeUnit timeUnit);

    /**
     * 保存与Key对应的Stamp签章值
     *
     * @param key    存储Key
     * @param value  与Key对应的Stamp值
     * @param expire 过期时间{@link Duration}
     */
    default void put(K key, V value, Duration expire) {
        put(key, value, expire.toMillis(), TimeUnit.MILLISECONDS);
    }

    /**
     * 保存与Key对应的Stamp签章值
     *
     * @param key   存储Key
     * @param value 与Key对应的Stamp值
     */
    default void put(K key, V value) {
        put(key, value, getExpire());
    }

    /**
     * 生成缓存值策略方法,该方法负责生成具体存储的值。
     *
     * @param key 签章存储Key值
     * @return {@link String}
     */
    V nextStamp(K key);

    /**
     * 创建具体的Stamp签章值,并存储至本地缓存
     *
     * @param key              签章存储Key值
     * @param expireAfterWrite 写入之后过期时间。注意:该值每次写入都会覆盖。如果有一个时间周期内的反复存取操作,需要手动计算时间差。
     * @param timeUnit         时间单位
     * @return 创建的签章值
     */
    default V create(K key, long expireAfterWrite, TimeUnit timeUnit) {
        V value = this.nextStamp(key);
        this.put(key, value, expireAfterWrite, timeUnit);
        return value;
    }

    /**
     * 创建具体的Stamp签章值,并存储至本地缓存
     *
     * @param key    签章存储Key值
     * @param expire 过期时间{@link Duration}
     * @return 创建的签章值
     */
    default V create(K key, Duration expire) {
        return create(key, expire.toMillis(), TimeUnit.MILLISECONDS);
    }

    /**
     * 创建具体的Stamp签章值,并存储至本地缓存
     *
     * @param key 与签章存储Key值
     * @return 创建的签章值
     */
    default V create(K key) {
        return create(key, getExpire());
    }

    /**
     * 校验Stamp值,与本地存储的Stamp 是否匹配
     *
     * @param key   与Stamp对应的Key值
     * @param value 外部传入的Stamp值
     * @return ture 匹配,false 不匹配
     * @throws StampParameterIllegalException 传入Stamp错误
     * @throws StampHasExpiredException       本地数据中没有Stamp或者Stamp已经过期。
     * @throws StampMismatchException         Stamp与本地存储值不匹配
     */
    boolean check(K key, V value) throws StampParameterIllegalException, StampHasExpiredException, StampMismatchException;

    /**
     * 根据key读取Stamp
     *
     * @param key 存储数据Key值
     * @return 存储的Stamp值
     */
    V get(K key);

    /**
     * 删除与Key对应的Stamp
     *
     * @param key 存储数据Key值
     * @throws StampDeleteFailedException Stamp删除错误
     */
    void delete(K key) throws StampDeleteFailedException;

    default boolean containKey(K key) {
        V value = get(key);
        return ObjectUtils.isNotEmpty(value);
    }

    /**
     * 锁定值
     * <p>
     * 非堵塞的尝试获取一个锁,如果对应的key还没有锁,返回一个AutoReleaseLock,否则立即返回空。如果Cache实例是本地的,它是一个本地锁,在本JVM中有效;如果是redis等远程缓存,它是一个不十分严格的分布式锁。锁的超时时间由expire和timeUnit指定。多级缓存的情况会使用最后一级做tryLock操作。
     *
     * @param key      存储Key
     * @param expire   过期时间
     * @param timeUnit 过期时间单位
     * @return {@link AutoReleaseLock}
     * @see <a href="https://github.com/alibaba/jetcache/wiki/CacheAPI_CN">JetCache Wiki</a>
     */
    AutoReleaseLock lock(K key, long expire, TimeUnit timeUnit);

    /**
     * 锁定值
     * <p>
     * 非堵塞的尝试获取一个锁,如果对应的key还没有锁,返回一个AutoReleaseLock,否则立即返回空。如果Cache实例是本地的,它是一个本地锁,在本JVM中有效;如果是redis等远程缓存,它是一个不十分严格的分布式锁。锁的超时时间由expire和timeUnit指定。多级缓存的情况会使用最后一级做tryLock操作。
     *
     * @param key    存储Key
     * @param expire 过期时间{@link Duration}
     * @return {@link AutoReleaseLock}
     * @see <a href="https://github.com/alibaba/jetcache/wiki/CacheAPI_CN">JetCache Wiki</a>
     */
    default AutoReleaseLock lock(K key, Duration expire) {
        return lock(key, expire.toMillis(), TimeUnit.MILLISECONDS);
    }

    /**
     * 锁定值
     * <p>
     * 非堵塞的尝试获取一个锁,如果对应的key还没有锁,返回一个AutoReleaseLock,否则立即返回空。如果Cache实例是本地的,它是一个本地锁,在本JVM中有效;如果是redis等远程缓存,它是一个不十分严格的分布式锁。锁的超时时间由expire和timeUnit指定。多级缓存的情况会使用最后一级做tryLock操作。
     * *
     *
     * @param key 存储Key
     * @return {@link AutoReleaseLock}
     * @see <a href="https://github.com/alibaba/jetcache/wiki/CacheAPI_CN">JetCache Wiki</a>
     */
    default AutoReleaseLock lock(K key) {
        return lock(key, getExpire());
    }

    /**
     * 锁定并执行操作
     * <p>
     * 非堵塞的尝试获取一个锁,如果对应的key还没有锁,返回一个AutoReleaseLock,否则立即返回空。如果Cache实例是本地的,它是一个本地锁,在本JVM中有效;如果是redis等远程缓存,它是一个不十分严格的分布式锁。锁的超时时间由expire和timeUnit指定。多级缓存的情况会使用最后一级做tryLock操作。
     *
     * @param key      存储Key
     * @param expire   过期时间
     * @param timeUnit 过期时间单位
     * @param action   需要执行的操作 {@link Runnable}
     * @return 是否执行成功
     * @see <a href="https://github.com/alibaba/jetcache/wiki/CacheAPI_CN">JetCache Wiki</a>
     */
    boolean lockAndRun(K key, long expire, TimeUnit timeUnit, Runnable action);

    /**
     * 锁定并执行操作
     * <p>
     * 非堵塞的尝试获取一个锁,如果对应的key还没有锁,返回一个AutoReleaseLock,否则立即返回空。如果Cache实例是本地的,它是一个本地锁,在本JVM中有效;如果是redis等远程缓存,它是一个不十分严格的分布式锁。锁的超时时间由expire和timeUnit指定。多级缓存的情况会使用最后一级做tryLock操作。
     *
     * @param key    存储Key
     * @param expire 过期时间{@link Duration}
     * @param action 需要执行的操作 {@link Runnable}
     * @return 是否执行成功
     * @see <a href="https://github.com/alibaba/jetcache/wiki/CacheAPI_CN">JetCache Wiki</a>
     */
    default boolean lockAndRun(K key, Duration expire, Runnable action) {
        return lockAndRun(key, expire.toMillis(), TimeUnit.MILLISECONDS, action);
    }

    /**
     * 锁定并执行操作
     * <p>
     * 非堵塞的尝试获取一个锁,如果对应的key还没有锁,返回一个AutoReleaseLock,否则立即返回空。如果Cache实例是本地的,它是一个本地锁,在本JVM中有效;如果是redis等远程缓存,它是一个不十分严格的分布式锁。锁的超时时间由expire和timeUnit指定。多级缓存的情况会使用最后一级做tryLock操作。
     *
     * @param key    存储Key
     * @param action 需要执行的操作 {@link Runnable}
     * @return 是否执行成功
     * @see <a href="https://github.com/alibaba/jetcache/wiki/CacheAPI_CN">JetCache Wiki</a>
     */
    default boolean lockAndRun(K key, Runnable action) {
        return lockAndRun(key, getExpire(), action);
    }
}

4.4.3.2 抽象Stamp签章管理类AbstractStampManager
package cn.kuafu.cloud.base.cache.jetcache.stamp;

import cn.kuafu.cloud.base.cache.core.exception.StampDeleteFailedException;
import cn.kuafu.cloud.base.cache.core.exception.StampHasExpiredException;
import cn.kuafu.cloud.base.cache.core.exception.StampMismatchException;
import cn.kuafu.cloud.base.cache.core.exception.StampParameterIllegalException;
import cn.kuafu.cloud.base.cache.jetcache.utils.JetCacheUtils;
import com.alicp.jetcache.AutoReleaseLock;
import com.alicp.jetcache.Cache;
import com.alicp.jetcache.anno.CacheType;
import org.apache.commons.lang3.ObjectUtils;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

/**
 * @param <K> 签章缓存对应Key值的类型。
 * @param <V> 签章缓存存储数据,对应的具体存储值的类型,
 * @author ningzhaosheng
 * @date 2022/5/13 10:16:52
 * @description 抽象Stamp管理
 */
public abstract class AbstractStampManager<K, V> implements StampManager<K, V> {
    private static final Duration DEFAULT_EXPIRE = Duration.ofMinutes(30);

    private String cacheName;
    private CacheType cacheType;
    private Duration expire;
    private Cache<K, V> cache;

    public AbstractStampManager(String cacheName) {
        this(cacheName, CacheType.BOTH);
    }

    public AbstractStampManager(String cacheName, CacheType cacheType) {
        this(cacheName, cacheType, DEFAULT_EXPIRE);
    }

    public AbstractStampManager(String cacheName, CacheType cacheType, Duration expire) {
        this.cacheName = cacheName;
        this.cacheType = cacheType;
        this.expire = expire;
        this.cache = JetCacheUtils.create(this.cacheName, this.cacheType, this.expire);
    }

    /**
     * 指定数据存储缓存
     *
     * @return {@link Cache}
     */
    protected Cache<K, V> getCache() {
        return this.cache;
    }

    @Override
    public Duration getExpire() {
        return this.expire;
    }

    public void setExpire(Duration expire) {
        this.expire = expire;
    }

    @Override
    public boolean check(K key, V value) {
        if (ObjectUtils.isEmpty(value)) {
            throw new StampParameterIllegalException("Parameter Stamp value is null");
        }

        V storedStamp = this.get(key);
        if (ObjectUtils.isEmpty(storedStamp)) {
            throw new StampHasExpiredException("Stamp is invalid!");
        }

        if (ObjectUtils.notEqual(storedStamp, value)) {
            throw new StampMismatchException("Stamp is mismathch!");
        }

        return true;
    }

    @Override
    public V get(K key) {
        return this.getCache().get(key);
    }

    @Override
    public void delete(K key) throws StampDeleteFailedException {
        boolean result = this.getCache().remove(key);
        if (!result) {
            throw new StampDeleteFailedException("Delete Stamp From Storage Failed");
        }
    }

    @Override
    public void put(K key, V value, long expireAfterWrite, TimeUnit timeUnit) {
        this.getCache().put(key, value, expireAfterWrite, timeUnit);
    }

    @Override
    public AutoReleaseLock lock(K key, long expire, TimeUnit timeUnit) {
        return this.getCache().tryLock(key, expire, timeUnit);
    }

    @Override
    public boolean lockAndRun(K key, long expire, TimeUnit timeUnit, Runnable action) {
        return this.getCache().tryLockAndRun(key, expire, timeUnit, action);
    }
}

4.4.3.3 抽象缓存计数类AbstractCountStampManager
package cn.kuafu.cloud.base.cache.jetcache.stamp;

import cn.hutool.crypto.SecureUtil;
import cn.kuafu.cloud.base.cache.core.exception.MaximumLimitExceededException;
import com.alicp.jetcache.anno.CacheType;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

import java.time.Duration;

/**
 * @author ningzhaosheng
 * @date 2022/8/22 18:09:50
 * @description 计数类型的缓存
 * 这里的泛型使用了 Long 主要是为了兼顾存储 System.currentTimeMillis()。否则类型不一致,还要建两个 Stamp
 */
public abstract class AbstractCountStampManager extends AbstractStampManager<String, Long> {
    private static final Logger log = LoggerFactory.getLogger(AbstractCountStampManager.class);

    public AbstractCountStampManager(String cacheName) {
        super(cacheName);
    }

    public AbstractCountStampManager(String cacheName, CacheType cacheType) {
        super(cacheName, cacheType);
    }

    public AbstractCountStampManager(String cacheName, CacheType cacheType, Duration expire) {
        super(cacheName, cacheType, expire);
    }
    /**
     * 在缓存有效期内进行计数
     *
     * @param identity 缓存 Key 的区分标识
     * @param maxTimes 允许的最大限制次数
     * @return 当前错误次数
     * @throws MaximumLimitExceededException 超出最大限制次数错误
     */
    public int counting(String identity, int maxTimes) throws MaximumLimitExceededException {
        return counting(identity, maxTimes, null);
    }

    /**
     * 在缓存有效期内进行计数
     *
     * @param identity 缓存 Key 的区分标识
     * @param maxTimes 允许的最大限制次数
     * @param expire   过期时间
     * @return 当前错误次数
     * @throws MaximumLimitExceededException 超出最大限制次数错误
     */
    public int counting(String identity, int maxTimes, Duration expire) throws MaximumLimitExceededException {
        return counting(identity, maxTimes, expire, false);
    }

    /**
     * 在缓存有效期内进行计数
     *
     * @param identity 缓存 Key 的区分标识
     * @param maxTimes 允许的最大限制次数
     * @param expire   过期时间
     * @param function 用于在日志中区分是哪个功能在调用。
     * @return 当前错误次数
     * @throws MaximumLimitExceededException 超出最大限制次数错误
     */
    public int counting(String identity, int maxTimes, Duration expire, String function) throws MaximumLimitExceededException {
        return counting(identity, maxTimes, expire, false, function);
    }

    /**
     * 在缓存有效期内进行计数
     *
     * @param identity 缓存 Key 的区分标识
     * @param maxTimes 允许的最大限制次数
     * @param expire   过期时间
     * @param useMd5   是否用 MD5 对区分标识进行混淆加密
     * @return 当前错误次数
     * @throws MaximumLimitExceededException 超出最大限制次数错误
     */
    public int counting(String identity, int maxTimes, Duration expire, boolean useMd5) throws MaximumLimitExceededException {
        return counting(identity, maxTimes, expire, useMd5, "AbstractCountStampManager");
    }

    /**
     * 在缓存有效期内进行计数
     *
     * @param identity 缓存 Key 的区分标识
     * @param maxTimes 允许的最大限制次数
     * @param expire   过期时间
     * @param useMd5   是否用 MD5 对区分标识进行混淆加密
     * @param function 用于在日志中区分是哪个功能在调用。
     * @return 当前错误次数
     * @throws MaximumLimitExceededException 超出最大限制次数错误
     */
    public int counting(String identity, int maxTimes, Duration expire, boolean useMd5, String function) throws MaximumLimitExceededException {
        Assert.notNull(identity, "identity cannot be null");

        String key = useMd5 ? SecureUtil.md5(identity) : identity;
        String expireKey = key + "_expire";

        Long index = get(key);

        if (ObjectUtils.isEmpty(index)) {
            index = 0L;
        }

        if (index == 0) {
            // 第一次读取剩余次数,因为缓存中还没有值,所以先创建缓存,同时缓存中计数为1。
            if (ObjectUtils.isNotEmpty(expire) && !expire.isZero()) {
                // 如果传入的 expire 不为零,那么就用 expire 参数值
                create(key, expire);
                put(expireKey, System.currentTimeMillis(), expire);
            } else {
                // 如果没有传入 expire 值,那么就默认使用 StampManager 自身配置的过期时间
                create(key);
                put(expireKey, System.currentTimeMillis());
            }
        } else {
            // 不管是注解上配置Duration值还是StampProperties中配置的Duration值,是不会变的
            // 所以第一次存入expireKey对应的System.currentTimeMillis()时间后,这个值也不应该变化。
            // 因此,这里只更新访问次数的标记值
            Duration newDuration = calculateRemainingTime(expire, expireKey, function);
            put(key, index + 1L, newDuration);

            // times 计数相当于数组的索引是 从0~n,所以需要
            if (index == maxTimes - 1) {
                throw new MaximumLimitExceededException("Requests are too frequent. Please try again later!");
            }
        }

        int times = new Long(index + 1L).intValue();
        log.debug("[KuaFuCloud-Base-Cache] |- {} has been recorded [{}] times.", function, times);
        return times;
    }

    /**
     * 计算剩余过期时间
     * <p>
     * 每次create或者put,缓存的过期时间都会被覆盖。(注意:Jetcache put 方法的参数名:expireAfterWrite)。
     * 因为Jetcache没有Redis的incr之类的方法,那么每次放入Times值,都会更新过期时间,实际操作下来是变相的延长了过期时间。
     *
     * @param configuredDuration 注解上配置的、且可以正常解析的Duration值
     * @param expireKey          时间标记存储Key值。
     * @return 还剩余的过期时间 {@link Duration}
     */
    private Duration calculateRemainingTime(Duration configuredDuration, String expireKey, String function) {
        Long begin = get(expireKey);
        Long current = System.currentTimeMillis();
        long interval = current - begin;

        log.debug("[KuaFuCloud-Base-Cache] |- {} operation interval [{}] millis.", function, interval);

        Duration duration;
        if (!configuredDuration.isZero()) {
            duration = configuredDuration.minusMillis(interval);
        } else {
            duration = getExpire().minusMillis(interval);
        }

        return duration;
    }
}

4.4.4 JetCache直接使用封装

4.4.4.1 JetCache单例工具类JetCacheUtils
package cn.kuafu.cloud.base.cache.jetcache.utils;

import cn.kuafu.cloud.base.cache.jetcache.enhance.JetCacheCreateCacheFactory;
import com.alicp.jetcache.Cache;
import com.alicp.jetcache.anno.CacheType;
import org.apache.commons.lang3.ObjectUtils;

import java.time.Duration;

/**
 * @author ningzhaosheng
 * @date 2022/8/25 18:17:59
 * @description JetCache 单例工具类
 */
public class JetCacheUtils {
    private static volatile JetCacheUtils instance;
    private JetCacheCreateCacheFactory jetCacheCreateCacheFactory;

    private JetCacheUtils() {

    }

    private void init(JetCacheCreateCacheFactory jetCacheCreateCacheFactory) {
        this.jetCacheCreateCacheFactory = jetCacheCreateCacheFactory;
    }

    private JetCacheCreateCacheFactory getJetCacheCreateCacheFactory() {
        return jetCacheCreateCacheFactory;
    }

    public static JetCacheUtils getInstance() {
        if (ObjectUtils.isEmpty(instance)) {
            synchronized (JetCacheUtils.class) {
                if (ObjectUtils.isEmpty(instance)) {
                    instance = new JetCacheUtils();
                }
            }
        }
        return instance;
    }

    public static void setJetCacheCreateCacheFactory(JetCacheCreateCacheFactory jetCacheCreateCacheFactory) {
        getInstance().init(jetCacheCreateCacheFactory);
    }

    public static <K, V> Cache<K, V> create(String name, Duration expire) {
        return create(name, expire, true);
    }

    public static <K, V> Cache<K, V> create(String name, Duration expire, Boolean cacheNullValue) {
        return create(name, expire, cacheNullValue, null);
    }

    public static <K, V> Cache<K, V> create(String name, Duration expire, Boolean cacheNullValue, Boolean syncLocal) {
        return create(name, CacheType.BOTH, expire, cacheNullValue, syncLocal);
    }

    public static <K, V> Cache<K, V> create(String name, CacheType cacheType) {
        return create(name, cacheType, null);
    }

    public static <K, V> Cache<K, V> create(String name, CacheType cacheType, Duration expire) {
        return create(name, cacheType, expire, true);
    }

    public static <K, V> Cache<K, V> create(String name, CacheType cacheType, Duration expire, Boolean cacheNullValue) {
        return create(name, cacheType, expire, cacheNullValue, null);
    }

    public static <K, V> Cache<K, V> create(String name, CacheType cacheType, Duration expire, Boolean cacheNullValue, Boolean syncLocal) {
        return getInstance().getJetCacheCreateCacheFactory().create(name, cacheType, expire, cacheNullValue, syncLocal);
    }
}

4.4.5 JetCache注解方式启动封装

4.4.5.1 JetCache加载配置类JetCacheConfiguration
package cn.kuafu.cloud.base.cache.jetcache.configuration;

import cn.kuafu.cloud.base.cache.caffeine.configuration.CaffeineConfiguration;
import cn.kuafu.cloud.base.cache.core.properties.CacheProperties;
import cn.kuafu.cloud.base.cache.jetcache.enhance.JetCacheCreateCacheFactory;
import cn.kuafu.cloud.base.cache.jetcache.enhance.KuaFuCloudCacheManager;
import cn.kuafu.cloud.base.cache.jetcache.utils.JetCacheUtils;
import cn.kuafu.cloud.base.cache.redis.configuration.CacheRedisConfiguration;
import com.alicp.jetcache.CacheManager;
import com.alicp.jetcache.autoconfigure.JetCacheAutoConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;

import javax.annotation.PostConstruct;

/**
 * @author ningzhaosheng
 * @date 2022/5/13 10:20:09
 * @description 新增JetCache配置,解决JetCache依赖循环问题
 */
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(CacheProperties.class)
@Import({CaffeineConfiguration.class, CacheRedisConfiguration.class})
@AutoConfigureAfter(JetCacheAutoConfiguration.class)
public class JetCacheConfiguration {

    private static final Logger log = LoggerFactory.getLogger(JetCacheConfiguration.class);

    @PostConstruct
    public void postConstruct() {
        log.debug("[KuaFuCloud-Base-Cache] |- SDK [Cache JetCache] Auto Configure.");
    }

    @Bean
    @ConditionalOnClass(CacheManager.class)
    public JetCacheCreateCacheFactory jetCacheCreateCacheFactory(CacheManager jcCacheManager) {
        JetCacheCreateCacheFactory jetCacheCreateCacheFactory = new JetCacheCreateCacheFactory(jcCacheManager);
        JetCacheUtils.setJetCacheCreateCacheFactory(jetCacheCreateCacheFactory);
        log.trace("[KuaFuCloud-Base-Cache] |- Bean [Jet Cache Create Cache Factory] Auto Configure.");
        return jetCacheCreateCacheFactory;
    }

    @Bean
    @Primary
    @ConditionalOnMissingBean
    public KuaFuCloudCacheManager kuaFuCloudCacheManager(JetCacheCreateCacheFactory jetCacheCreateCacheFactory, CacheProperties cacheProperties) {
        KuaFuCloudCacheManager kuaFuCloudCacheManager = new KuaFuCloudCacheManager(jetCacheCreateCacheFactory, cacheProperties);
        kuaFuCloudCacheManager.setAllowNullValues(cacheProperties.getAllowNullValues());
        log.trace("[KuaFuCloud-Base-Cache] |- Bean [Jet Cache KuaFuCloud Cache Manager] Auto Configure.");
        return kuaFuCloudCacheManager;
    }
}

4.4.5.2 JetCache手动启动注解
package cn.kuafu.cloud.base.cache.jetcache.annotation;

import cn.kuafu.cloud.base.cache.jetcache.configuration.JetCacheConfiguration;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

/**
 * @author ningzhaosheng
 * @date 2022/5/13 10:42:52
 * @description 手动开启JetCache注入
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(JetCacheConfiguration.class)
public @interface EnableKuaFuCloudJetCache {

}

4.4.6 Jpa Hibernate二级缓存扩展

4.4.6.1 DomainDataStorageAccess 接口实现
package cn.kuafu.cloud.base.data.jpa.hibernate.cache.spi;

import cn.hutool.crypto.SecureUtil;
import cn.kuafu.cloud.base.assistant.core.definition.constants.BaseConstants;
import cn.kuafu.cloud.base.assistant.core.definition.constants.SymbolConstants;
import cn.kuafu.cloud.base.assistant.core.context.TenantContextHolder;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.cache.spi.support.DomainDataStorageAccess;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;

/**
 * @author ningzhaosheng
 * @date 2022/5/14 18:28:20
 * @description 自定义Hibernate二级缓存DomainDataStorageAccess
 */
public class KuaFuCloudDomainDataStorageAccess implements DomainDataStorageAccess {
    private static final Logger log = LoggerFactory.getLogger(KuaFuCloudDomainDataStorageAccess.class);

    private Cache cache;

    public KuaFuCloudDomainDataStorageAccess() {

    }

    public KuaFuCloudDomainDataStorageAccess(Cache cache) {
        this.cache = cache;
    }

    private String secure(Object key) {
        String original = String.valueOf(key);
        if (StringUtils.isNotBlank(original) && StringUtils.startsWith(original, "sql:")) {
            String recent = SecureUtil.md5(original);
            log.trace("[KuaFuCloud-Base-Data] |- SPI - Secure the sql type key [{}] to [{}]", original, recent);
            return recent;
        }
        return original;
    }

    private String getTenantId() {
        String tenantId = TenantContextHolder.getTenantId();
        String result = StringUtils.isNotBlank(tenantId) ? tenantId : BaseConstants.DEFAULT_TENANT_ID;
        log.trace("[KuaFuCloud-Base-Data] |- SPI - Tenant identifier for jpa second level cache is : [{}]", result);
        return StringUtils.toRootLowerCase(result);
    }

    private String wrapper(Object key) {
        String original = secure(key);
        String tenantId = getTenantId();

        String result = tenantId + SymbolConstants.COLON + original;
        log.trace("[KuaFuCloud-Base-Data] |- SPI - Current cache key is : [{}]", result);
        return result;
    }

    private Object get(Object key) {
        Cache.ValueWrapper value = cache.get(key);

        if (ObjectUtils.isNotEmpty(value)) {
            return value.get();
        }
        return null;
    }

    @Override
    public boolean contains(Object key) {
        String wrapperKey = wrapper(key);
        Object value = this.get(wrapperKey);
        log.trace("[KuaFuCloud-Base-Data] | - SPI check is key : [{}] exist.", wrapperKey);
        return ObjectUtils.isNotEmpty(value);
    }

    @Override
    public Object getFromCache(Object key, SharedSessionContractImplementor session) {
        String wrapperKey = wrapper(key);
        Object value = this.get(wrapperKey);
        log.trace("[KuaFuCloud-Base-Data] | - SPI get from cache key is : [{}], value is : [{}]", wrapperKey, value);
        return value;
    }

    @Override
    public void putIntoCache(Object key, Object value, SharedSessionContractImplementor session) {
        String wrapperKey = wrapper(key);
        log.trace("[KuaFuCloud-Base-Data] | - SPI put into cache key is : [{}], value is : [{}]", wrapperKey, value);
        cache.put(wrapperKey, value);
    }

    @Override
    public void removeFromCache(Object key, SharedSessionContractImplementor session) {
        String wrapperKey = wrapper(key);
        log.trace("[KuaFuCloud-Base-Data] | - SPI remove from cache key is : [{}]", wrapperKey);
        cache.evict(wrapperKey);
    }

    @Override
    public void evictData(Object key) {
        String wrapperKey = wrapper(key);
        log.trace("[KuaFuCloud-Base-Data] | - SPI evict key : [{}] from cache.", wrapperKey);
        cache.evict(wrapperKey);
    }

    @Override
    public void clearCache(SharedSessionContractImplementor session) {
        this.evictData();
    }

    @Override
    public void evictData() {
        log.trace("[KuaFuCloud-Base-Data] | - SPI clear all cache data.");
        cache.clear();
    }

    @Override
    public void release() {
        log.trace("[KuaFuCloud-Base-Data] | - SPI cache release.");
        cache.invalidate();
    }
}

4.4.6.2 RegionFactoryTemplate 模板扩展
package cn.kuafu.cloud.base.data.jpa.hibernate.cache.spi;

import cn.hutool.extra.spring.SpringUtil;
import org.hibernate.boot.spi.SessionFactoryOptions;
import org.hibernate.cache.cfg.spi.DomainDataRegionBuildingContext;
import org.hibernate.cache.cfg.spi.DomainDataRegionConfig;
import org.hibernate.cache.spi.support.DomainDataStorageAccess;
import org.hibernate.cache.spi.support.RegionFactoryTemplate;
import org.hibernate.cache.spi.support.StorageAccess;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.springframework.cache.CacheManager;

import java.util.Map;

/**
 * @author ningzhaosheng
 * @date 2022/5/14 18:30:35
 * @description 自定义Hibernate二级缓存RegionFactory
 */
public class KuaFuCloudRegionFactory extends RegionFactoryTemplate {
    private CacheManager cacheManager;

    @Override
    protected StorageAccess createQueryResultsRegionStorageAccess(String regionName, SessionFactoryImplementor sessionFactory) {
        return new KuaFuCloudDomainDataStorageAccess(cacheManager.getCache(regionName));
    }

    @Override
    protected StorageAccess createTimestampsRegionStorageAccess(String regionName, SessionFactoryImplementor sessionFactory) {
        return new KuaFuCloudDomainDataStorageAccess(cacheManager.getCache(regionName));
    }

    @Override
    protected DomainDataStorageAccess createDomainDataStorageAccess(DomainDataRegionConfig regionConfig, DomainDataRegionBuildingContext buildingContext) {
        return new KuaFuCloudDomainDataStorageAccess(cacheManager.getCache(regionConfig.getRegionName()));
    }

    @Override
    protected void prepareForUse(SessionFactoryOptions settings, Map configValues) {
        this.cacheManager = SpringUtil.getBean("kuaFuCloudCacheManager");
    }

    @Override
    protected void releaseFromUse() {
        cacheManager = null;
    }
}

五、缓存 Start 启动组件封装

5.1 组件说明

借鉴SpringBoot Start 启动风格,基于Spring SPI机制加载各配置类,封装成cache-spring-boot-starter组件

5.2 组件结构图

5.3 组件Maven依赖

<dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>cache-redisson-sdk</artifactId>
        </dependency>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>cache-jetcache-sdk</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
    </dependencies>

5.4 组件功能实现源码

5.4.1 组件启动类

package cn.kuafu.cloud.base.cache.starter.autoconfigure;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

/**
 * @author ningzhaosheng
 * @date 2022/5/13 15:12:18
 * @description Cache 配置
 */
@Configuration(proxyBeanMethods = false)
public class AutoConfiguration {
    private static final Logger log = LoggerFactory.getLogger(AutoConfiguration.class);

    @PostConstruct
    public void postConstruct() {
        log.info("[KuaFuCloud-Base-Cache] |- Starter [Base Cache Starter] Auto Configure.");
    }
}

5.4.2 SPI加载机制配置文件

cn.kuafu.cloud.base.cache.starter.autoconfigure.AutoConfiguration
cn.kuafu.cloud.base.cache.jetcache.configuration.JetCacheConfiguration

JetCache缓存框架整合Caffeine、Redis,实现一级缓存、二级缓存,进而扩展JPA Hibernate、Mybatis两款ORM缓存框架的二级缓存,扩展Spring Cache缓存,实现Spring Session 共享等功能的缓存方案就实现完成了。如果对缓存扩展点和缓存原理不熟悉,可以参考文章基于JetCache整合实现一级、二级缓存方案(前置基础知识与原理)-CSDN博客

好了,本次分享就到这里,如果帮助到大家,欢迎大家点赞+关注+收藏,有疑问也欢迎大家评论留言!

  • 27
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
JetCache 是一个基于 Java 的开源缓存库,它提供了对方法级缓存的支持。使用 JetCache 可以方便地将方法的结果缓存起来,提高应用程序的性能和响应速度。 下面是使用 JetCache 的基本步骤: 1. 添加 JetCache 依赖:将 JetCache 的依赖项添加到项目的构建文件中(如 Maven 或 Gradle)。 2. 配置缓存:在应用程序的配置文件中,配置需要使用的缓存类型和相关属性。例如,可以配置内存缓存、Redis 缓存等。 3. 注解方法:在需要进行缓存的方法上添加 JetCache 的注解,如 `@Cached`、`@CacheRemove` 等。这些注解可以指定缓存的 key、过期时间、条件等。 4. 使用缓存:在调用被注解的方法时,JetCache 会根据注解的配置自动处理缓存。如果缓存中存在所需数据,则直接返回缓存数据;否则,执行方法并将结果放入缓存。 下面是一个简单的示例: ```java import io.github.jiashunx.cache.Cache; import io.github.jiashunx.cache.annotation.Cached; public class MyService { private Cache<String, String> cache; // 构造函数或依赖注入注入 Cache 实例 @Cached(name = "myCache", key = "#param", expire = 600) public String getData(String param) { // 从数据库或其他数据源中获取数据 // ... return data; } } ``` 在上述示例中,`MyService` 使用了 JetCache 的 `@Cached` 注解对 `getData` 方法进行了缓存配置。缓存的名称为 "myCache",缓存的 key 使用方法参数 `param`,缓存的过期时间为 600 秒。当调用 `getData` 方法时,JetCache 会自动处理缓存逻辑,如果缓存中存在对应的数据,则直接返回缓存数据;否则,执行方法并将结果放入缓存。 这只是 JetCache 的基本用法,JetCache 还提供了其他更复杂的缓存策略和配置选项,可以根据具体需求进行配置和使用。 希望这个回答对您有帮助!如有更多问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值