spring自定义缓存切面

需求背景

Spring cache是Spring提供的一个缓存框架,利用了AOP实现了基于注解的缓存功能。虽然使用方便,但有如下缺陷。

首先,针对不同业务,不支持手动设置不同的缓存过期时间,例如商品缓存想要30s过期,优惠券信息想要50s过期。

此外,缓存注解不能避免缓存雪崩的场景,需要借助额外的编码才能实现。

目前项目的部分业务涉及到不同维度的数据,每种维度数据需要不同的缓存过期时间,而且也会有缓存雪崩场景。为了满足这些场景,当前做法是在Spring cache之上做了大量编码工作,增加了研发和维护成本。
所以,我们希望自己实现一个可以满足上述需求的缓存切面框架,通过注解方式一键满足缓存的灵活需求。

目标

在Spring cache之上实现自定义缓存切面框架,一键设置缓存注解,减少研发成本。

代码开发步骤

1.自定义注解

EasyCache表示使用的缓存名称、缓存key、缓存过期时间等

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EasyCache {
    String cacheName() default "redisCache";
    String cacheKey();
    int expire() default -1;
    Lock lock() default @Lock;
}

Lock表示使用哪种锁(默认redis)、锁的key、失效时间

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Lock {
    String lockType() default "redis";
    String key() default "";
    int expire() default 60;
}

2.自定义实体类

EasyCacheBean

@Data
public class EasyCacheBean {
    private String cacheName;
    private String cacheKey;
    private int expire;
    private Method method;
    private LockBean lockBean;
}

LockBean

@Data
public class LockBean {
    private String lockType;
    private String key;
    private String value;
    private int expire;
}

3.自定义缓存操作接口

Cache接口

public interface Cache {
    String getName();

    <T> T get(String key);

    void put(String key, Object value);

    void put(String key, Object value,int expire);

    void evict(String key);
}

Cache接口的redis实现类

public class RedisCache implements Cache {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public String getName() {
        return "redisCache";
    }

    @Override
    public <T> T get(String key) {
        return (T)redisTemplate.opsForValue().get(key);
    }

    @Override
    public void put(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    @Override
    public void put(String key, Object value, int expire) {
        redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
    }

    @Override
    public void evict(String key) {

    }
}

4.自定义缓存管理器接口

Cache接口理论上可以有多个实现类,即有多种缓存方式。所以我们需要一个缓存管理器,维护所有Cache接口的实现类,当指定使用哪种缓存时,就从管理器中获取。

CacheManager接口

public interface CacheManager {
    Cache getCache(String cacheName);
}

CacheManager接口的默认实现类DefaultCacheManager

@Component
public class DefaultCacheManager implements CacheManager, InitializingBean, ApplicationContextAware {

    private ApplicationContext applicationContext;

    private ConcurrentHashMap<String, Cache> caches = new ConcurrentHashMap();

    @Override
    public Cache getCache(String cacheName) {
        return caches.get(cacheName);
    }

    @Override
    public void putCache(String cacheName, Cache cache) {

    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Map<String, Cache> cacheBeans = applicationContext.getBeansOfType(Cache.class);
        for (String s : cacheBeans.keySet()) {
            caches.put(cacheBeans.get(s).getName(),cacheBeans.get(s));
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

5.自定义锁操作接口

Lock接口,包含加锁和解锁

public interface Lock {
    String getName();

    boolean doLock(LockBean lockBean);

    boolean unlock(LockBean lockBean);
}

我们定义两种锁的实现类:redis锁和jdk内置锁

5.1RedisLock
@Slf4j
@Component
public class RedisLock implements Lock {

    private static final String SUCCESS = "OK";
    private static final GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public String getName() {
        return "redis";
    }

  public boolean lock(LockBean lockBean) {
        try {
            String ret = redisTemplate.execute((RedisCallback<String>) connection -> {
                Object nativeConnection = connection.getNativeConnection();
                if (nativeConnection instanceof RedisAsyncCommands) {
                    RedisAsyncCommands commands = (RedisAsyncCommands) nativeConnection;
                    //同步方法执行、setnx禁止异步
                    return commands.getStatefulConnection().sync().set(serializer.serialize(lockBean.getKey()), serializer.serialize(lockBean.getValue()), SetArgs.Builder.nx().ex(lockBean.getExpire()));
                }
                if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
                    RedisAdvancedClusterAsyncCommands clusterAsyncCommands = (RedisAdvancedClusterAsyncCommands) nativeConnection;
                    return clusterAsyncCommands.getStatefulConnection().sync().set(serializer.serialize(lockBean.getKey()), serializer.serialize(lockBean.getValue()), SetArgs.Builder.nx().ex(lockBean.getExpire()));
                }
                return null;
            });
            return SUCCESS.equals(ret);
        } catch (Exception e) {
            log.error("get lock error ,lock:{}", lockBean.getKey(), e);
            return false;
        }
    }

    @Override
    public boolean doLock(LockBean lockBean) {
        Random random = new Random();
        while (true) {
            if(lock(lockBean)) {
                return true;
            }
            log.info("等待线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) {
                log.error("获取分布式锁休眠被中断:", e);
                Thread.currentThread().interrupt();
            }
        }
    }

    @Override
    public boolean unlock(LockBean lockBean) {
        try {
            byte[] keyByte = serializer.serialize(lockBean.getKey());
            return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
                Long result = connection.del(keyByte);
                return result != null && result == 1L;
            });
        } catch (Exception e) {
            log.error("get lock error ,lock:{}", lockBean.getKey(), e);
            return false;
        }
    }
}
5.2JdkLock
@Component
public class JdkLock implements Lock {

    private ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<>();

    @Override
    public String getName() {
        return "jdk";
    }

    @Override
    public boolean doLock(LockBean lockBean) {
        ReentrantLock reentrantLock = new ReentrantLock();
        ReentrantLock oldLock = lockMap.putIfAbsent(lockBean.getKey(), reentrantLock);
        if(oldLock != null){
            oldLock.lock();
        }else {
            reentrantLock.lock();
        }
        return true;
    }

    @Override
    public boolean unlock(LockBean lockBean) {
        ReentrantLock reentrantLock = lockMap.get(lockBean.getKey());
        reentrantLock.unlock();
        return true;
    }
}

6.自定义锁管理接口

同Cache接口一样,Lock接口理论上可以有多个实现类,即有多种锁的方式。所以我们需要一个管理器,维护所有Lock接口的实现类。

LockManager

public interface LockManager {
    Lock getLock(String name);

}

LockManager 默认实现类DefaultLockManager

@Component
public class DefaultLockManager implements LockManager, InitializingBean, ApplicationContextAware {
    
    private ApplicationContext applicationContext;

    private ConcurrentHashMap<String,Lock> locks = new ConcurrentHashMap<>();
    
    @Override
    public Lock getLock(String name) {
        return locks.get(name);
    }

    @Override
    public void putLock(String name, Lock lock) {
        locks.put(name,lock);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Map<String, Lock> lockbeans = applicationContext.getBeansOfType(Lock.class);
        for (Map.Entry<String, Lock> entry : lockbeans.entrySet()) {
            locks.put(entry.getValue().getName(),entry.getValue());
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

7.自定义Advisor相关

下面就是自定义缓存切面的核心逻辑了,我们需要借助spring aop中的Advisor组件。
Advisor的介绍可参考我的另一篇博客聊聊spring aop中的advisor组件

7.1 EasyCacheMethodMatcher

用于判断类中的方法是否有@EasyCache注解,如果有则获取注解信息,并包装为
EasyCacheBean,加入自身缓存

@Component
public class EasyCacheMethodMatcher implements MethodMatcher {

    private static ConcurrentHashMap<MethodClassKey, EasyCacheBean> cache = new ConcurrentHashMap(1024);

    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        MethodClassKey key = new MethodClassKey(method, targetClass);
        EasyCacheBean existsBean = cache.get(key);
        if(existsBean != null){
            return true;
        }
        Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
        if(AnnotatedElementUtils.hasAnnotation(specificMethod, EasyCache.class)){
            EasyCacheBean easyCacheBean = new EasyCacheBean();
            EasyCache easyCacheAnno = AnnotationUtils.getAnnotation(specificMethod, EasyCache.class);
            easyCacheBean.setCacheName(easyCacheAnno.cacheName());
            easyCacheBean.setExpire(easyCacheAnno.expire());
            easyCacheBean.setCacheKey(easyCacheAnno.cacheKey());

            Lock lockAnno = easyCacheAnno.lock();
            LockBean lockBean = new LockBean();
            lockBean.setExpire(lockAnno.expire());
            lockBean.setKey(lockAnno.key());
            lockBean.setLockType(lockAnno.lockType());
            easyCacheBean.setLockBean(lockBean);

            easyCacheBean.setMethod(specificMethod);
            cache.putIfAbsent(key, easyCacheBean);
            return true;
        }
        return false;
    }

    @Override
    public boolean isRuntime() {
        return false;
    }

    @Override
    public boolean matches(Method method, Class<?> targetClass, Object... args) {
        return false;
    }

    public static EasyCacheBean getCache(Method method, Class<?> targetClass){
        return cache.get(new MethodClassKey(method, targetClass));
    }
}
7.2 EasyCachePointcut

即spring中的Pointcut组件,需要ClassFilter和刚才的MethodMatcher。此处ClassFilter.TRUE表示所有类都符合条件

public class EasyCachePointcut implements Pointcut {
    @Override
    public ClassFilter getClassFilter() {
        return ClassFilter.TRUE;
    }

    @Override
    public MethodMatcher getMethodMatcher() {
        return new EasyCacheMethodMatcher();
    }
}
7.3 EasyCacheAdvice

光有Pointcut还不行,我们还需要Advice

@Component
public class EasyCacheAdvice implements MethodInterceptor {

    @Autowired
    private CacheManager cacheManager;

    @Autowired
    private LockManager lockManager;

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Class<?> targetClass = invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null;
        EasyCacheBean easyCacheBean = EasyCacheMethodMatcher.getCache(invocation.getMethod(), targetClass);

        //缓存数据的key,根据方法和参数信息生成
        String cacheKey = getCacheKey(invocation, easyCacheBean);

        //获取缓存组件
        Cache cache = cacheManager.getCache(easyCacheBean.getCacheName());
        if(cache.get(cacheKey) != null){
            return cache.get(cacheKey);
        }

        LockBean lockBean = easyCacheBean.getLockBean();
        //重新设置锁的key
        lockBean.setKey(getLockKey(invocation, easyCacheBean));
        //设置锁的key的值
        lockBean.setValue(System.currentTimeMillis() + "");

        //获取锁
        Lock lock = lockManager.getLock(lockBean.getLockType());
        Object result = null;
        try {
            //加锁
            lock.doLock(lockBean);
            if(cache.get(cacheKey) != null){
                return cache.get(cacheKey);
            }
            //执行业务方法
            result = invocation.proceed();
            if(result != null){
                //是否设置缓存数据的过期时间
                if(easyCacheBean.getExpire() == -1){
                    cache.put(cacheKey, result);
                }else {
                    cache.put(cacheKey, result, easyCacheBean.getExpire());
                }
            }
        } finally {
            lock.unlock(lockBean);
        }
        return result;
    }

    public String getCacheKey(MethodInvocation invocation, EasyCacheBean easyCacheBean){
        //获取到参数名称
        String[] parameterNames = new DefaultParameterNameDiscoverer().getParameterNames(easyCacheBean.getMethod());
        return ElParser.getKey(easyCacheBean.getCacheKey(), parameterNames, invocation.getArguments());
    }

    public String getLockKey(MethodInvocation invocation, EasyCacheBean easyCacheBean){
        //获取到参数名称
        String[] parameterNames = new DefaultParameterNameDiscoverer().getParameterNames(easyCacheBean.getMethod());
        return ElParser.getKey(easyCacheBean.getLockBean().getKey(), parameterNames, invocation.getArguments());
    }
}

其中invoke方法我们需要获取缓存key和锁的key,利用的是spring的el表达式解析。工具类如下

public class ElParser {
    private static ExpressionParser parser = new SpelExpressionParser();

    public static String getKey(String key,String[] paramNames,Object[] args) {
        //#areaCode
        Expression expression = parser.parseExpression(key);
        StandardEvaluationContext context = new StandardEvaluationContext();

        if(args.length <= 0) {
            return null;
        }

        for (int i = 0; i < args.length; i++) {
            context.setVariable(paramNames[i],args[i]);
        }
        return expression.getValue(context,String.class);
    }
}
7.4 EasyCacheAdvisor

有了Pointcut和Advice,我们就可以组成Advisor了

@Component
public class EasyCacheAdvisor implements PointcutAdvisor {

    @Autowired
    private EasyCacheAdvice easyCacheAdvice;

    @Override
    public Pointcut getPointcut() {
        return new EasyCachePointcut();
    }
    @Override
    public Advice getAdvice() {
        return easyCacheAdvice;
    }

    @Override
    public boolean isPerInstance() {
        return false;
    }
}

至此,自定义缓存切面的组件就编写完成了。

项目结构

在这里插入图片描述

使用方法

在项目中某段测试方法上加注解

@EasyCache(cacheKey = "#name", expire = 60, lock = @Lock(key = "'test1'+#name"))
public String test1(String name){
    return "hello" + name;
}

包装为springboot-starter

如果我们仅仅只是在内部项目中使用,那么上述过程足够了。但如果我们希望将其变为jar包,共享给公司其他组使用,我们可以包装为springboot-starter。

1.编写配置类

EasyCacheConfig

@Configuration
public class EasyCacheConfig {

    @Bean
    public EasyCacheAdvice easyCacheAdvice(){
        return new EasyCacheAdvice();
    }

    @Bean
    public EasyCacheAdvisor easyCacheAdvisor(){
        return new EasyCacheAdvisor();
    }

    @Bean
    public CacheManager defaultCacheManager(){
        return new DefaultCacheManager();
    }

    @Bean
    public LockManager defaultLockManager(){
        return new DefaultLockManager();
    }

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(lettuceConnectionFactory);
        //key序列化
        template.setKeySerializer(new StringRedisSerializer());
        //value序列化
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public Lock redisLock(){
        return new RedisLock();
    }

    @Bean
    public Lock jdkLock(){
        return new JdkLock();
    }

    @Bean
    public Cache redisCache(){
        return new RedisCache();
    }
}

2.创建创建spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.sf.easycache.config.EasyCacheConfig

最终结构如下
在这里插入图片描述

3.maven install到本地仓库或者公司的私库

4.使用自定义的starter

在公司的项目中,引入打好的依赖

 <dependency>
        <groupId>com.sf</groupId>
        <artifactId>easy-cache</artifactId>
        <version>1.0.0</version>
    </dependency>
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值