Java案例如何封装缓存工具类?

wen java案例 13

本文目录导读:

Java案例如何封装缓存工具类?

  1. 设计思路与选型
  2. 代码实现示例(基于本地内存 + 支持过期)
  3. 生产环境建议(进阶封装)
  4. 关键设计要点总结
  5. 最终建议

在Java项目中封装一个缓存工具类,通常是为了屏蔽底层缓存实现(如本地内存、Redis等)的复杂性,提供统一、易用的API,下面我将从设计思路核心代码实现(基于本地内存 + 简单泛型)以及最佳实践三个维度为你详细讲解。


设计思路与选型

  1. 确定缓存类型

    • 本地缓存:使用 HashMap + ConcurrentHashMapCaffeineGuava Cache,速度快,但无法跨进程共享。
    • 分布式缓存:使用 Redis(Jedis/Redisson/Spring Data Redis)、Memcached
    • 多级缓存:本地缓存 + 分布式缓存组合(如 Caffeine + Redis)。
  2. 核心功能

    • put(key, value):存入数据。
    • get(key):获取数据。
    • remove(key):删除数据。
    • clear():清空缓存。
    • 可选put(key, value, expireTime) 支持过期时间。
  3. 设计模式

    • 策略模式:可切换不同的缓存实现(如本地 / Redis)。
    • 单例模式:工具类通常为无状态或全局唯一。
    • 泛型:支持任意类型的Key和Value。

代码实现示例(基于本地内存 + 支持过期)

这里使用 ConcurrentHashMap + 泛型 + 简单的过期清理机制,为你演示如何封装,这是一个生产可用的轻量级实现。

缓存数据实体(内部类)

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class CacheUtil<K, V> {
    // 缓存容器:存储键值对及其过期时间
    private final ConcurrentMap<K, CacheEntry<V>> cache = new ConcurrentHashMap<>();
    // 定时清理过期缓存的线程池
    private final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor();
    /**
     * 缓存条目,包含值和过期时间戳
     */
    private static class CacheEntry<V> {
        V value;
        long expireTime; // 过期时间戳(毫秒),0表示永不过期
        CacheEntry(V value, long expireTime) {
            this.value = value;
            this.expireTime = expireTime;
        }
        boolean isExpired() {
            return expireTime > 0 && System.currentTimeMillis() > expireTime;
        }
    }
    public CacheUtil() {
        // 启动定时清理任务:每5秒清理一次过期缓存
        cleaner.scheduleAtFixedRate(this::evictExpiredEntries, 5, 5, TimeUnit.SECONDS);
    }
    // ---------- 核心 API ----------
    /**
     * 存入缓存(永不过期)
     */
    public void put(K key, V value) {
        cache.put(key, new CacheEntry<>(value, 0));
    }
    /**
     * 存入缓存(指定过期时间)
     * @param key       键
     * @param value     值
     * @param expireAfterSeconds 过期秒数(>0)
     */
    public void put(K key, V value, long expireAfterSeconds) {
        if (expireAfterSeconds <= 0) {
            put(key, value);
            return;
        }
        long expireTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expireAfterSeconds);
        cache.put(key, new CacheEntry<>(value, expireTime));
    }
    /**
     * 获取缓存(如果过期则返回null并移除)
     */
    @SuppressWarnings("unchecked")
    public V get(K key) {
        CacheEntry<V> entry = cache.get(key);
        if (entry == null) {
            return null;
        }
        if (entry.isExpired()) {
            cache.remove(key);
            return null;
        }
        return entry.value;
    }
    /**
     * 删除缓存
     */
    public V remove(K key) {
        CacheEntry<V> removed = cache.remove(key);
        return removed != null ? removed.value : null;
    }
    /**
     * 清空全部缓存
     */
    public void clear() {
        cache.clear();
    }
    /**
     * 当前缓存数量(包括已过期但未清理的)
     */
    public int size() {
        return cache.size();
    }
    // ---------- 内部清理方法 ----------
    /**
     * 清理所有过期条目
     */
    private void evictExpiredEntries() {
        cache.entrySet().removeIf(entry -> entry.getValue().isExpired());
    }
    // 可考虑优雅关闭清理线程(在生产环境中需调用)
    public void shutdown() {
        cleaner.shutdown();
    }
}

使用示例

public class Example {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个 String → String 的缓存工具
        CacheUtil<String, String> cache = new CacheUtil<>();
        // 存入数据(永久有效)
        cache.put("name", "Java");
        System.out.println(cache.get("name")); // 输出: Java
        // 存入数据(5秒后过期)
        cache.put("token", "abc123", 5);
        System.out.println(cache.get("token")); // 输出: abc123
        // 等待6秒
        Thread.sleep(6000);
        System.out.println(cache.get("token")); // 输出: null(已过期)
        // 删除
        cache.remove("name");
        System.out.println(cache.get("name")); // 输出: null
        // 清空
        cache.clear();
    }
}

生产环境建议(进阶封装)

如果你需要在真正的生产环境中使用,更推荐以下两种方式:

基于 Caffeine(性能远高于 HashMap)

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class CaffeineCacheUtil<K, V> {
    private final Cache<K, V> cache;
    public CaffeineCacheUtil(int maxSize, int expireAfterWriteSeconds) {
        this.cache = Caffeine.newBuilder()
                .maximumSize(maxSize)                // 最大条目数
                .expireAfterWrite(expireAfterWriteSeconds, TimeUnit.SECONDS) // 写入后过期
                .recordStats()                       // 开启统计(可选)
                .build();
    }
    public void put(K key, V value) {
        cache.put(key, value);
    }
    public V get(K key) {
        return cache.getIfPresent(key);
    }
    // 带加载函数的get(推荐:如果key不存在,自动加载并缓存)
    public V get(K key, java.util.function.Function<? super K, ? extends V> loader) {
        return cache.get(key, loader);
    }
    public void remove(K key) {
        cache.invalidate(key);
    }
    public void clear() {
        cache.invalidateAll();
    }
}

基于 Spring Cache 注解(声明式缓存)

如果你使用 Spring Boot,可以直接使用 @Cacheable@CachePut 等注解,无需自己封装工具类:

@Service
public class UserService {
    @Cacheable(value = "users", key = "#userId", unless = "#result == null")
    public User getUserById(String userId) {
        // 模拟数据库查询
        return userRepository.findById(userId).orElse(null);
    }
}

通过配置 spring.cache.type=rediscaffeine,即可切换底层实现。


关键设计要点总结

设计点 说明
线程安全 使用 ConcurrentHashMapCaffeine,避免并发问题
过期机制 惰性删除(get时检查) + 定时清理(后台线程)
泛型 支持 <K, V> 任意类型,提高复用性
单一职责 工具类只负责缓存操作,不耦合业务逻辑
可切换性 建议封装接口(如 ICache),不同实现可灵活替换

最终建议

  • 小型项目 / 本地缓存:直接用上面的 Caffeine 封装(推荐)或基于 ConcurrentHashMap 的自实现。
  • 分布式项目:封装 Redis 工具类 或直接使用 Spring Data Redis。
  • 最简洁方案:如果你使用 Spring Boot,不要再自己封装工具类,直接使用 @Cacheable + CaffeineRedis 配置即可,这是最标准、最省力的方式。

如果你有特定场景(如需要本地+Redis多级缓存),可以在评论区告诉我,我可以为你提供对应的封装示例。

抱歉,评论功能暂时关闭!