@CacheEvict增强版@CacheBatchEvict(自定义注解+AOP实现缓存批量删除)

@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的思想,在目标方法之前或执行之后做一些与业务逻辑无关的额外功能。

END

  • 7
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
要通过自定义注解AOP实现Spring Security配置指定接口不需要Token才能访问,可以按照以下步骤进行操作: 1. 创建一个自定义注解,例如`@NoTokenRequired`,用于标识不需要Token的接口。 ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NoTokenRequired { } ``` 2. 创建一个切面类,用于拦截带有`@NoTokenRequired`注解的方法,并跳过Spring Security的Token验证。 ```java @Aspect @Component public class TokenValidationAspect { @Before("@annotation(com.example.NoTokenRequired)") public void skipTokenValidation(JoinPoint joinPoint) { // 跳过Spring Security的Token验证逻辑 SecurityContextHolder.getContext().setAuthentication(null); } } ``` 3. 配置Spring Security,将AOP切面类添加到Spring Security的配置中。 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private TokenValidationAspect tokenValidationAspect; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() // 配置需要Token验证的接口 .anyRequest().authenticated() .and() .csrf().disable(); // 将AOP切面类添加到Spring Security的配置中 http.addFilterBefore(tokenValidationAspect, UsernamePasswordAuthenticationFilter.class); } } ``` 4. 在需要不需要Token验证的接口上,添加`@NoTokenRequired`注解。 ```java @RestController public class ExampleController { @NoTokenRequired @GetMapping("/example") public String example() { return "This API does not require Token"; } } ``` 这样配置之后,带有`@NoTokenRequired`注解的接口将不会进行Spring Security的Token验证,即可在没有Token的情况下访问该接口。其他接口仍然需要进行Token验证。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lambda.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值