@CacheEvict的改进,实现缓存批量删除
最近发了关于一篇关于Ehcache缓存的文章,发现评论有问到如果方法参数是一个集合,注解@CacheEvict怎么实现批量删除缓存?当时没有考虑到这种情况也就不了了之了。马上查了一下资料发现@CacheEvict只支持删除单个key,要想批量删除只能自己去实现,于是抽时间研究了下在执行方法之前通过AOP拿到参数列表,批量删除缓存后再执行目标方法。完美解决了缓存批量删除问题,在这里分享给大家,让大家体验下AOP运用场景。
1、批量删除缓存实现
1.1、AOP环境集成
1、引入AOP
支持jar
包依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.5.6</version>
</dependency>
2、Pointcut
切入点定义。这里我们采用自定义注解的方式定义Advice
将要发生的地方(个人觉得自定义注解比较方便,execution
不太友好)。例如基于批量删除缓存这个功能我们可以自定义的注解为@CacheBatchEvict
。
/**
* @description: 缓存批量清除注解
* @author: laizhenghua
* @date: 2022/5/22 22:25
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheBatchEvict {
/**
* 指定缓存组件
*/
String[] cacheNames() default {};
/**
* key需要指定一个spEL表达式,通过spEL表达式获取方法参数
*/
String key() default "";
/**
* 是否清除所有
*/
boolean isClearAll() default false;
}
假如某批量删除数据方法,集合元素都是缓存的key,删除数据的同时我们也想批量删除缓存,就可以使用这个注解代替@CacheEvict
。
3、切面类定义
/**
* @description:
* @author: laizhenghua
* @date: 2022/5/22 22:41
*/
@Aspect
@Component(value = "cacheAspect")
public class CacheAspect {
private final org.apache.logging.log4j.Logger log = Logger.getLogger(CacheAspect.class);
@Autowired
private CacheManager cacheManager;
/**
* 定义切入点
*/
@Pointcut("@annotation(com.laizhenghua.cache.annotation.CacheBatchEvict)")
public void pointcut() {
}
@Around("pointcut()")
public Object proceed(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 1.获取注解参数值 SpEL表达式
CacheBatchEvict cacheBatchEvict = methodSignature.getMethod().getAnnotation(CacheBatchEvict.class);
String[] cacheNames = cacheBatchEvict.cacheNames();
// 清空所有缓存
if (cacheBatchEvict.isClearAll()) {
for (String cacheName : cacheNames) {
net.sf.ehcache.Cache cache = (Cache) Objects.requireNonNull(cacheManager.getCache(cacheName)).getNativeCache();
cache.removeAll();
}
}
String spEL = cacheBatchEvict.key();
if (!StringUtils.hasText(spEL)) {
log.warn("@CacheBatchEvict key is null");
return null;
}
// 2.获取目标方法参数
List<Object> keyList = generateKeyListBySpEL(spEL, joinPoint);
if (keyList == null) {
log.warn(String.format("unable to find method params by [%s]", spEL));
return null;
}
// 3.清除缓存
for (String cacheName : cacheNames) {
net.sf.ehcache.Cache cache = (Cache) Objects.requireNonNull(cacheManager.getCache(cacheName)).getNativeCache();
for (Object key : keyList) {
if (cache.isKeyInCache(key)) {
cache.remove(key);
}
}
}
// 4.执行目标方法
return joinPoint.proceed();
}
public List<Object> generateKeyListBySpEL(String spEL, ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 创建解析器
SpelExpressionParser parser = new SpelExpressionParser();
// 获取表达式
Expression expression = parser.parseExpression(spEL);
// 设置解析上下文
EvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
// 获取运行时参数名称
DefaultParameterNameDiscoverer discover = new DefaultParameterNameDiscoverer();
String[] parameterNames = discover.getParameterNames(method);
assert parameterNames != null;
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
// 解析
List<Object> valueList = (List<Object>) expression.getValue(context);
if (CollectionUtils.isEmpty(valueList)) {
return null;
}
return valueList;
}
}
1.2、在方法中使用注解
我们发现自定义的注解@CacheBatchEvict
不仅作为APO切入点的描述,还能嵌入一些补充信息如缓存组件名称cacheName
、缓存的key信息等供我们在切面类中灵活处理,这就是自定义注解的好处。
如何在方法中使用注解呢?在接口实现类的方法中(因为JDK的动态代理必须要有接口)使用@CacheBatchEvict
代替@CacheEvict
即可。例如:
@Override
@CacheBatchEvict(cacheNames = {"userEntityCache"}, key = "#idList") // spEL表达式描述方法参数名称
public void delete(List<Integer> idList) {
log.info("delete all by collection");
// 执行删除逻辑
}
2、测试
2.1、获取所有缓存的key
因为我们集成的缓存产品是Ehcache
,所以通过Spring
缓存抽象获取所有key信息时,需要把缓存组件转成net.sf.ehcache.Cache
,如:
/**
* @description: CacheController
* @author: laizhenghua
* @date: 2022/5/22 22:02
*/
@RestController
@RequestMapping(value = "cache")
public class CacheController {
@Autowired
private CacheManager cacheManager;
@RequestMapping(value = "/getKeyList", method = RequestMethod.GET)
public R getCacheKeyList(@RequestParam(value = "cacheName", required = false, defaultValue = "userEntityCache") String cacheName) {
net.sf.ehcache.Cache cache = (Cache) Objects.requireNonNull(cacheManager.getCache(cacheName)).getNativeCache();
List<String> keyList = cache.getKeys();
return R.ok().put("data", keyList);
}
}
2.2、测试方法编写
为了测试方便,我们把实体对象ID作为缓存的key。
1、新增保存方法,保存数据并把方法返回结果存入缓存中
controller
层
@RequestMapping(value = "/save/{id}/{name}", method = RequestMethod.GET)
public R save(@PathVariable("id") Integer id, @PathVariable("name") String name) {
UserEntity entity = userService.save(id, name);
return R.ok().put("data", entity);
}
service
层,注意接口已经省略,直接给出接口实现类里的方法
@Override
@Cacheable(value = "userEntityCache", key = "#root.args[0]") // 使用参数id作为换行key
public UserEntity save(Integer id, String name) {
UserEntity entity = new UserEntity(id, name);
log.info("save to ehcache -> " + entity.toString());
// 保存至数据库里
return entity;
}
2、新增获取实体方法,用于测试获取缓存中的数据
controller
层
@RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
public R getById(@PathVariable("id") Integer id) {
UserEntity entity = userService.getById(id);
return R.ok().put("data", entity);
}
service
层
@Override
@Cacheable(value = "userEntityCache", key = "#root.args[0]") // 使用参数id作为换行key
public UserEntity getById(Integer id) {
log.info("query entity by id = " + id);
// 如果返回null,说明缓存中数据已经没有了
return null;
}
3、编写批量删除方法
controller
层
@RequestMapping(value = "/delete", method = RequestMethod.POST)
public R delete(@RequestParam("idList") List<Integer> idList) {
userService.delete(idList);
return R.ok();
}
service
层
@Override
@CacheBatchEvict(cacheNames = {"userEntityCache"}, key = "#idList") // spEL表达式描述方法参数名称
public void delete(List<Integer> idList) {
log.info("delete all by collection");
// 执行删除逻辑
}
2.3、测试
1、调用保存接口,保存两条测试数据
如
2、保存到缓存中后,测试获取有没有走缓存
我们发现已经获取到数据了,但是后台并没有输出任何日志,说明已经走缓存了。
3、调用批量删除方法,看看能否也能批量删除缓存
OK方法已经执行成功了,在调用获取方法,观看日志输出情况!
日志输出情况
OK,成功删除缓存。
到这里内容也就结束了,简单做个小结,删除功能也很简单就是在删除数据之前通过AOP拿到方法参数信息,然后手动清理缓存。重要的不是功能,而是一种思想,要灵活运用AOP的思想,在目标方法之前或执行之后做一些与业务逻辑无关的额外功能。