当前位置: 首页 > news >正文

Java本地缓存简单实现,支持SpEL表达式及主动、过期、定时删除

整体思路:

  1. 创建@EnableLocalCache注解,用于选择是否开启本地缓存
  2. 创建@LocalCache注解和@LocalCacheEvict注解用于缓存的构建、过期时间和清除
  3. 切面实现缓存的获取、写入和清除
  4. SpEL表达式的支持
  5. 缓存配置类,用于创建Bean对象
  6. 开启定时任务清除过期的缓存,支持自定义任务参数

项目示例Gitee地址: Java实现本地缓存demo

效果展示:


代码实现:

 1.引入基本依赖:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<version>2.7.10</version>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
	<version>2.7.10</version>
</dependency>
<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<version>1.18.22</version>
</dependency>

2.创建注解:

2.1 @EnableLocalCache 开启缓存,@Import(LocalCacheConfig.class)导入缓存配置类
package com.gooluke.localcache.annotation;

import com.gooluke.localcache.config.LocalCacheConfig;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

/**
 * @author gooluke
 * 开启本地缓存
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(LocalCacheConfig.class)
@Documented
public @interface EnableLocalCache {
}
2.2 @LocalCache 使用缓存
package com.gooluke.localcache.annotation;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * @author gooluke
 * 本地缓存注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LocalCache {

    /**
     * 缓存key
     * 支持SpEL表达式
     */
    String key() default "";

    /**
     * 缓存过期时间 0-表示永不过期
     */
    long timeout() default 0;

    /**
     * 缓存过期时间单位
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

}
2.3 @LocalCacheEvict 清除缓存
package com.gooluke.localcache.annotation;

import java.lang.annotation.*;

/**
 * @author gooluke
 * 删除缓存注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LocalCacheEvict {

    /**
     * 要清除缓存的key
     * 支持SpEL表达式
     */
    String key();

}

3.切面类

3.1 LocalCacheAspect 缓存切面类
package com.gooluke.localcache.aop;

import com.gooluke.localcache.annotation.LocalCache;
import com.gooluke.localcache.cache.LocalCacheManager;
import com.gooluke.localcache.utils.SpELUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;

@Aspect
@AllArgsConstructor
@Slf4j
public class LocalCacheAspect {

    private final LocalCacheManager cacheManager;

    @Around("@annotation(localCache)")
    public Object cacheMethod(ProceedingJoinPoint joinPoint, LocalCache localCache) throws Throwable {
        // 获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取方法的返回类型
        Class<?> returnType = signature.getReturnType();
        // 获取方法的全路径限定名(包括包名、类名和方法名)
        String methodName = signature.getMethod().toString();
        // 构建缓存键
        String cacheKey = (localCache.key() != null && !localCache.key().isEmpty()) ? SpELUtil.parseSpEl(localCache.key(), joinPoint) : methodName;
        // 尝试从缓存中获取数据
        Object cachedValue = cacheManager.getCache(cacheKey);
        if (cachedValue != null) {
            // 如果缓存中有数据,则直接返回
            return returnType.cast(cachedValue); // 强制转换为方法的返回类型
        } else {
            long timeout = localCache.timeout();
            // 如果没有缓存,则执行目标方法
            Object proceed = joinPoint.proceed();
            // 将结果放入缓存
            if (timeout == 0) {
                cacheManager.addCache(cacheKey, proceed);
            } else {
                cacheManager.addCache(cacheKey, proceed, timeout, localCache.timeUnit());
            }
            return proceed;
        }
    }
}
3.2 LocalCacheEvictAspect 清除缓存的切面类
package com.gooluke.localcache.aop;

import com.gooluke.localcache.annotation.LocalCacheEvict;
import com.gooluke.localcache.cache.LocalCacheManager;
import com.gooluke.localcache.utils.SpELUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;

@Aspect
@AllArgsConstructor
@Slf4j
public class LocalCacheEvictAspect {

    private final LocalCacheManager cacheManager;

    @Around("@annotation(localCacheEvict)")
    public Object cacheMethod(ProceedingJoinPoint joinPoint, LocalCacheEvict localCacheEvict) throws Throwable {
        // 获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取方法的全路径限定名(包括包名、类名和方法名)
        String methodName = signature.getMethod().toString();
        // 构建缓存键
        String cacheKey = (localCacheEvict.key() != null && !localCacheEvict.key().isEmpty()) ? SpELUtil.parseSpEl(localCacheEvict.key(), joinPoint) : methodName;
        //删除缓存
        cacheManager.deleteCache(cacheKey);
        return joinPoint.proceed();
    }
}

4.SpEL工具类

package com.gooluke.localcache.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.lang.reflect.Method;
import java.util.Optional;

/**
 * @author gooluke
 * datetime 2024/12/5 19:19
 */
public class SpELUtil {
    /**
     * 用于SpEL表达式解析
     */
    private static final ExpressionParser parser = new SpelExpressionParser();
    /**
     * 解析、获取参数名
     */
    private static final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

    public static String parseSpEl(String spEl, ProceedingJoinPoint joinPoint) {
        if (spEl == null || spEl.isEmpty()) {
            return "";
        }
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Object[] args = joinPoint.getArgs();
        //这里是因为args里没有参数名,只有值,所以只能从DefaultParameterNameDiscoverer去获取参数名
        String[] params = Optional.ofNullable(parameterNameDiscoverer.getParameterNames(method)).orElse(new String[]{});
        EvaluationContext context = new StandardEvaluationContext();//el解析需要的上下文对象
        for (int i = 0; i < params.length; i++) {
            context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去-基于参数名称
            context.setVariable("p" + i, args[i]);//所有参数都作为原材料扔进去-基于参数位置
        }
        Expression expression = parser.parseExpression(spEl);
        return expression.getValue(context, String.class);
    }

    public static String getMethodKey(Method method) {
        return method.getDeclaringClass() + "#" + method.getName();
    }
}

5.缓存管理类LocalCacheManager&&缓存对象LocalCacheItem

5.1 LocalCacheManager 缓存管理器
package com.gooluke.localcache.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

public class LocalCacheManager {

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

    private final Map<String, LocalCacheItem<Object>> cacheMap = new ConcurrentHashMap<>();

    /**
     * 添加不过期缓存
     */
    public void addCache(String name, Object object) {
        addCache(name, object, 0, null);
    }

    public void addCache(String name, Object object, long timeout, TimeUnit timeUnit) {
        if (name == null) {
            return;
        }
        cacheMap.put(name, new LocalCacheItem<>(object, timeout, timeUnit));
    }

    public void deleteCache(String name) {
        cacheMap.remove(name);
    }

    public Object getCache(String name) {
        if (name == null) {
            return null;
        }
        LocalCacheItem<Object> cache = cacheMap.getOrDefault(name, null);
        if (cache != null) {
            if (cache.isExpired()) {
                deleteCache(name);
                return null;
            } else {
                return cache.getValue();
            }
        }
        return null;
    }

    /**
     * 清理所有已过期的缓存项
     */
    public void cleanupExpired() {
        try {
            log.info("清除前总缓存数量:" + cacheMap.size());
            // 移除过期的缓存项
            cacheMap.entrySet().removeIf(entry -> entry.getValue().isExpired());
            log.info("清除过期缓存后总缓存数量:" + cacheMap.size());
        } catch (Exception e) {
            log.error("定时清除本地缓存异常:", e);
        }
    }

}
5.2 LocalCacheItem 本地缓存对象
package com.gooluke.localcache.cache;

import lombok.Getter;

import java.util.concurrent.TimeUnit;

public class LocalCacheItem<T> {
    @Getter
    private final T value;
    private final long expiryTime; // 过期时间戳

    public LocalCacheItem(T value, long timeout, TimeUnit timeUnit) {
        this.value = value;
        this.expiryTime = timeout == 0 ? 0 : System.currentTimeMillis() + timeUnit.toMillis(timeout);
    }

    public boolean isExpired() {
        return expiryTime != 0 && System.currentTimeMillis() > expiryTime;
    }
}

6.缓存配置类LocalCacheConfig

package com.gooluke.localcache.config;

import com.gooluke.localcache.aop.LocalCacheAspect;
import com.gooluke.localcache.aop.LocalCacheEvictAspect;
import com.gooluke.localcache.cache.LocalCacheManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class LocalCacheConfig implements ApplicationRunner, DisposableBean {

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

    private final LocalCacheManager localCacheManager = new LocalCacheManager();

    private final ScheduledExecutorService CACHE_CLEANUP_SCHEDULER = Executors.newScheduledThreadPool(1);

    @Value("${cache.cleanup.initialDelay:0}")
    private long initialDelay;

    @Value("${cache.cleanup.period:60}")
    private long period;


    @Bean
    public LocalCacheAspect localCacheAspect() {
        return new LocalCacheAspect(this.localCacheManager);
    }

    @Bean
    public LocalCacheEvictAspect localCacheEvictAspect() {
        return new LocalCacheEvictAspect(this.localCacheManager);
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("CACHE_CLEANUP_SCHEDULER task initialDelay:{},period:{}", initialDelay, period);
        CACHE_CLEANUP_SCHEDULER.scheduleAtFixedRate(localCacheManager::cleanupExpired, initialDelay, period, TimeUnit.SECONDS);
    }

    @Override
    public void destroy() throws Exception {
        log.info("关闭CACHE_CLEANUP_SCHEDULER");
        CACHE_CLEANUP_SCHEDULER.shutdown();
    }
}

7.主启动类开启本地缓存

package com.gooluke.localcache;

import com.gooluke.localcache.annotation.EnableLocalCache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author gooluke
 */
@SpringBootApplication
@EnableLocalCache//开启本地缓存
public class LocalCacheDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(LocalCacheDemoApplication.class, args);
    }

}

8.支持配置定时清除过期缓存的任务参数,不配置默认延迟0s,间隔60s

#延迟启动
cache.cleanup.initialDelay=10
#缓存清理间隔
cache.cleanup.period=30

相关文章:

  • 解锁日常养生密码,拥抱健康生活
  • RabbitMQ学习笔记
  • 腾讯云大模型知识引擎LKE+DeepSeek结合工作流升级智能客服
  • Python模块的版本管理与文档编写
  • 初学者对爬虫的实例(抖音/b站)python篇
  • 实现一键不同环境迁移ES模板
  • 在IDEA中进行git回滚操作:Reset current branch to here‌或Reset HEAD
  • pyqt联合designer的运用和设置
  • 2025年全球生成式AI消费应用发展趋势报告
  • Lucas定理介绍及证明
  • 深入理解 Java 中的 CopyOnWrite 机制
  • 解决电脑问题(1)——硬件问题
  • manus本地部署使用体验
  • 「DataX」数据迁移-IDEA运行DataX方法总结
  • 实现插入排序
  • 【leetcode hot 100 234】回文链表
  • manus对比ChatGPT-Deep reaserch进行研究类学术相关数据分析!谁更胜一筹?
  • K8s 端口转发实战指南(Practical Guide to k8s Port Forwarding)
  • 软件性能测试深度解析(进阶篇):JMeter高阶应用与性能工程体系构建
  • Spring Framework中的IoC容器
  • css 网站模板/免费观看b站的广告网站平台
  • 网站费用标准/找百度
  • 如何能让企业做网站的打算/合肥网站推广公司排名
  • 专业的建站/谷歌seo建站
  • 台州网站seo外包/霸屏seo服务
  • 哪个网站做国际生意/关键词优化平台有哪些