Java本地缓存简单实现,支持SpEL表达式及主动、过期、定时删除
整体思路:
- 创建@EnableLocalCache注解,用于选择是否开启本地缓存
- 创建@LocalCache注解和@LocalCacheEvict注解用于缓存的构建、过期时间和清除
- 切面实现缓存的获取、写入和清除
- SpEL表达式的支持
- 缓存配置类,用于创建Bean对象
- 开启定时任务清除过期的缓存,支持自定义任务参数
项目示例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