SpringBoot @CacheEvict按通配符删除(redis)

3,860 阅读2分钟

背景

在使用SpringBoot提供的@CacheEvict注解时希望能够通过通配符批量删除一些keys;

//通过通配符删除keys
@CacheEvict(cacheNames = "cacheName", key = "keys:*")

上面的代码不能达到我们的目的,需要一点点改造;

@CacheEvict是如何使缓存失效的

SpringBoot提供的实现不支持按通配符进行删除;

这是SpringBoot的实现:

如何达到我们的目的

思路:

更改evict这个方法的实现,使其能够根据指定的pattern批量删除我们指定的key

怎么来实现

我们创建一个装饰器类来装饰这个默认的RedisCache类,做到比较优雅的实现我们的功能;

思路:代理CacheManager,在CacheManager中代理RedisCache

直接贴代码:

package cn.demo.demo; //你自己的包名

import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.support.AbstractCacheManager;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheWriter;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;


/**
 * 这是一个CacheManager装饰器,本质上包装了RedisCache
 * 使用装饰器模式扩展RedisCache的功能
 * 扩展的功能有:
 * *. evict方法支持删除以'*'结尾的通配符表示的所有的key(若key的字符串结尾为'*',则删除这个通配符匹配下的所有的key);
 */
public class RedisCacheManagerDecorator {
    private final CacheManager baseManager;
    private static final ConcurrentMap<Cache, Cache> cacheMap = new ConcurrentHashMap<>();
    private static final List<MethodHandler> methodHandlers = new ArrayList<>();

    static {
        loadMethodHandlers();
    }


    private RedisCacheManagerDecorator(CacheManager baseManager) {
        this.baseManager = baseManager;
    }

    /**
     * 装饰CacheManager
     * @param manager 要被装饰的CacheManager
     * @return 装饰后的代理对象
     */
    public static CacheManager decorate(CacheManager manager) {
        RedisCacheManagerDecorator decorator = new RedisCacheManagerDecorator(manager);
        return (CacheManager) Proxy.newProxyInstance(manager.getClass().getClassLoader(),
                AbstractCacheManager.class.getInterfaces(),
                cacheManagerInvocationHandler(decorator));
    }

    private static InvocationHandler cacheManagerInvocationHandler(RedisCacheManagerDecorator decorator) {
        return (proxy, method, args) -> {
            Object res = method.invoke(decorator.baseManager, args);
            if (method.getName().equals("getCache")) {
                res = decorateCache(res);
            }
            return res;
        };
    }

    private static Object decorateCache(Object cacheObj) {
        if (cacheObj instanceof Cache) {
            Cache cache = (Cache) cacheObj;
            Cache wrapperCache = cacheMap.get(cache);
            if (wrapperCache == null) {
                wrapperCache = buildRedisCacheProxy(cache);
                cacheMap.putIfAbsent(cache, wrapperCache);
            }
            return wrapperCache;
        }
        return cacheObj;
    }

    /**
     * 代理RedisCache
     */
    private static Cache buildRedisCacheProxy(Cache cache) {
        InvocationHandler handler = (proxy, method, args) -> {
            MethodHandler methodHandler = methodHandlers.stream().filter(h -> h.canHandle(cache, method, args)).findFirst().orElse(null);
            if (methodHandler != null) {
                return methodHandler.handle(cache, method, args);
            }
            return method.invoke(cache, args);
        };
        return (Cache) Proxy.newProxyInstance(cache.getClass().getClassLoader(),
                new Class<?>[]{Cache.class},
                handler);
    }

    private static void loadMethodHandlers() {
        methodHandlers.add(new EvictMethodHandler());
    }

    /**
     * 代理evict方法
     * 若key以'*'结尾,删除满足这个key的pattern的所有key
     */
    static class EvictMethodHandler implements MethodHandler {
        private static Method targetMethod;

        static {
            try {
                targetMethod = Cache.class.getMethod("evict", Object.class);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }

        String parsePattern(Object[] args) {
            if (args == null || args.length == 0) return null;
            String value = String.valueOf(args[0]);
            if (value.endsWith("*")) return value;
            return null;
        }

        @Override
        public boolean canHandle(Cache cache, Method method, Object[] args) {
            return cache instanceof RedisCache
                    && method.equals(targetMethod)
                    && parsePattern(args) != null;
        }

        @Override
        public Object handle(Cache cache, Method method, Object[] args) {
            RedisCache redisCache = (RedisCache) cache;
            String pattern = parsePattern(args);
            RedisCacheConfiguration configuration = redisCache.getCacheConfiguration();

            pattern = redisCache.getName() + "::" + pattern;
            RedisCacheWriter cacheWriter = redisCache.getNativeCache();
            byte[] convert = configuration.getConversionService().convert(pattern, byte[].class);
            if (convert != null) {
                cacheWriter.clean(redisCache.getName(), convert);
            }
            return null;
        }
    }

    interface MethodHandler {
        boolean canHandle(Cache cache, Method method, Object[] args);

        Object handle(Cache cache, Method method, Object[] args);
    }
}

如何使用:

在创建好RedisCacheManager后用上面的类创建一个装饰类:

上面的工作做完后就大功告成了;

原理: 当框架从@CacheEvict注解的信息拿到Cache实例时,框架拿到的是一个被包装的Cache实例(装饰器);这个装饰器拦截了Cache的evict方法,若装饰器发现evict方法的参数(即key)是以"*"结尾,拦截该方法并将与这个pattern匹配的所有的key都删除;