自定义实现基于注解的缓存使用

一 写在前面

        相信很多人都是用过Spring与Redis的整合框架Spring-data-redis-starter提供的@Cacheabl,@CacheEviict和@CachePut注解,它以注解的形式简化了在Spring Boot程序中使用Redis缓存的成本,真心非常好用。

        但是出于一些特殊的原因,例如:公司自己有的缓存框架屏蔽了Redis连接地址,只允许通过namespace的方式执行访问的Redis集群,或者只是单纯地想要自己造个轮子,深入学习下这几个注解内部的底层实现原理,我们决定自己实现上述三个注解提供的能力支持。

注解名称功能含义
@BossCacheable如果有缓存则从缓存中获取数据,否则从数据库中查询到之后返回并且添加到缓存中
@BossCahceEvict使得指定缓存key对应的缓存失效
@BossCachePut更新缓存到执行的key中

二 开始行动

2.1 引入基础的依赖包:

<properties>
    <spring.version>5.3.18</spring.version>
</properties>


<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
     </dependencies>
</dependencyManagement>

代码工程结构: 

2.2 注解定义

定义注解BossCacheable

import org.springframework.cache.annotation.CacheConfig;
import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

/**
 * 效果等同于@Cacheable
 * 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface BossCacheable {
    /**
     * Alias for {@link #cacheNames}.
     */
    @AliasFor("cacheNames")
    String[] value() default {};

    /**
     * Names of the caches in which method invocation results are stored.
     * <p>Names may be used to determine the target cache (or caches), matching
     * the qualifier value or bean name of a specific bean definition.
     * @since 4.2
     * @see #value
     * @see CacheConfig#cacheNames
     */
    @AliasFor("value")
    String[] cacheNames() default {};

    /**
     * 缓存key,可以为空,
     * 如果指定要按照SpEL表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
     * <p>
     * 例如:@BossCacheable(key="#userName")
     *
     * @return
     */
    /**
     * Spring Expression Language (SpEL) expression for computing the key dynamically.
     * <p>Default is {@code ""}, meaning all method parameters are considered as a key,
     * unless a custom {@link #keyGenerator} has been configured.
     * <p>The SpEL expression evaluates against a dedicated context that provides the
     * following meta-data:
     * <ul>
     * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
     * references to the {@link java.lang.reflect.Method method}, target object, and
     * affected cache(s) respectively.</li>
     * <li>Shortcuts for the method name ({@code #root.methodName}) and target class
     * ({@code #root.targetClass}) are also available.
     * <li>Method arguments can be accessed by index. For instance the second argument
     * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
     * can also be accessed by name if that information is available.</li>
     * </ul>
     */
    String key() default "";

    /**
     * Spring Expression Language (SpEL) expression used for making the method
     * caching conditional.
     * <p>Default is {@code ""}, meaning the method result is always cached.
     * <p>The SpEL expression evaluates against a dedicated context that provides the
     * following meta-data:
     * <ul>
     * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
     * references to the {@link java.lang.reflect.Method method}, target object, and
     * affected cache(s) respectively.</li>
     * <li>Shortcuts for the method name ({@code #root.methodName}) and target class
     * ({@code #root.targetClass}) are also available.
     * <li>Method arguments can be accessed by index. For instance the second argument
     * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
     * can also be accessed by name if that information is available.</li>
     * </ul>
     */
    String condition() default "";

    /**
     * The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator}
     * to use.
     * <p>Mutually exclusive with the {@link #key} attribute.
     * @see CacheConfig#keyGenerator
     */
    String keyGenerator() default "";

    /**
     * 超时时间
     * 单位:seconds
     *
     * @return
     */
    int expire() default 10 * 60;
}

定义注解BossCacheEvict

import org.springframework.cache.annotation.CacheConfig;
import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BossCacheEvict {

    /**
     * Alias for {@link #cacheNames}.
     */
    @AliasFor("cacheNames")
    String[] value() default {};

    /**
     * Names of the caches to use for the cache eviction operation.
     * <p>Names may be used to determine the target cache (or caches), matching
     * the qualifier value or bean name of a specific bean definition.
     * @since 4.2
     * @see #value
     * @see CacheConfig#cacheNames
     */
    @AliasFor("value")
    String[] cacheNames() default {};

    /**
     * Spring Expression Language (SpEL) expression for computing the key dynamically.
     * <p>Default is {@code ""}, meaning all method parameters are considered as a key,
     * unless a custom {@link #keyGenerator} has been set.
     * <p>The SpEL expression evaluates against a dedicated context that provides the
     * following meta-data:
     * <ul>
     * <li>{@code #result} for a reference to the result of the method invocation, which
     * can only be used if {@link #beforeInvocation()} is {@code false}. For supported
     * wrappers such as {@code Optional}, {@code #result} refers to the actual object,
     * not the wrapper</li>
     * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
     * references to the {@link java.lang.reflect.Method method}, target object, and
     * affected cache(s) respectively.</li>
     * <li>Shortcuts for the method name ({@code #root.methodName}) and target class
     * ({@code #root.targetClass}) are also available.
     * <li>Method arguments can be accessed by index. For instance the second argument
     * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
     * can also be accessed by name if that information is available.</li>
     * </ul>
     */
    String key() default "";

    /**
     * The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator}
     * to use.
     * <p>Mutually exclusive with the {@link #key} attribute.
     * @see CacheConfig#keyGenerator
     */
    String keyGenerator() default "";

    /**
     * Spring Expression Language (SpEL) expression used for making the cache
     * eviction operation conditional.
     * <p>Default is {@code ""}, meaning the cache eviction is always performed.
     * <p>The SpEL expression evaluates against a dedicated context that provides the
     * following meta-data:
     * <ul>
     * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
     * references to the {@link java.lang.reflect.Method method}, target object, and
     * affected cache(s) respectively.</li>
     * <li>Shortcuts for the method name ({@code #root.methodName}) and target class
     * ({@code #root.targetClass}) are also available.
     * <li>Method arguments can be accessed by index. For instance the second argument
     * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
     * can also be accessed by name if that information is available.</li>
     * </ul>
     */
    String condition() default "";
}

定义注解BossCachePut

import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

/**
 * Annotation indicating that a method (or all methods on a class) triggers a
 * {@link org.springframework.cache.Cache#put(Object, Object) cache put} operation.
 *
 * <p>In contrast to the {@link Cacheable @Cacheable} annotation, this annotation
 * does not cause the advised method to be skipped. Rather, it always causes the
 * method to be invoked and its result to be stored in the associated cache. Note
 * that Java8's {@code Optional} return types are automatically handled and its
 * content is stored in the cache if present.
 *
 * <p>This annotation may be used as a <em>meta-annotation</em> to create custom
 * <em>composed annotations</em> with attribute overrides.
 *
 * @author Costin Leau
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @author Sam Brannen
 * @since 3.1
 * @see CacheConfig
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface BossCachePut {

    /**
     * Alias for {@link #cacheNames}.
     */
    @AliasFor("cacheNames")
    String[] value() default {};

    /**
     * Names of the caches to use for the cache put operation.
     * <p>Names may be used to determine the target cache (or caches), matching
     * the qualifier value or bean name of a specific bean definition.
     * @since 4.2
     * @see #value
     * @see CacheConfig#cacheNames
     */
    @AliasFor("value")
    String[] cacheNames() default {};

    /**
     * Spring Expression Language (SpEL) expression for computing the key dynamically.
     * <p>Default is {@code ""}, meaning all method parameters are considered as a key,
     * unless a custom {@link #keyGenerator} has been set.
     * <p>The SpEL expression evaluates against a dedicated context that provides the
     * following meta-data:
     * <ul>
     * <li>{@code #result} for a reference to the result of the method invocation. For
     * supported wrappers such as {@code Optional}, {@code #result} refers to the actual
     * object, not the wrapper</li>
     * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
     * references to the {@link java.lang.reflect.Method method}, target object, and
     * affected cache(s) respectively.</li>
     * <li>Shortcuts for the method name ({@code #root.methodName}) and target class
     * ({@code #root.targetClass}) are also available.
     * <li>Method arguments can be accessed by index. For instance the second argument
     * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
     * can also be accessed by name if that information is available.</li>
     * </ul>
     */
    String key() default "";

    /**
     * The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator}
     * to use.
     * <p>Mutually exclusive with the {@link #key} attribute.
     * @see CacheConfig#keyGenerator
     */
    String keyGenerator() default "";

    /**
     * Spring Expression Language (SpEL) expression used for making the cache
     * put operation conditional.
     * <p>Default is {@code ""}, meaning the method result is always cached.
     * <p>The SpEL expression evaluates against a dedicated context that provides the
     * following meta-data:
     * <ul>
     * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
     * references to the {@link java.lang.reflect.Method method}, target object, and
     * affected cache(s) respectively.</li>
     * <li>Shortcuts for the method name ({@code #root.methodName}) and target class
     * ({@code #root.targetClass}) are also available.
     * <li>Method arguments can be accessed by index. For instance the second argument
     * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
     * can also be accessed by name if that information is available.</li>
     * </ul>
     */
    String condition() default "";

    /**
     * 超时时间
     * 单位:seconds
     *
     * @return
     */
    int expire() default 10 * 60;
}

定义注解BossCacheEvict

import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BossCacheEvict {

    /**
     * Alias for {@link #cacheNames}.
     */
    @AliasFor("cacheNames")
    String[] value() default {};

    /**
     * Names of the caches to use for the cache eviction operation.
     * <p>Names may be used to determine the target cache (or caches), matching
     * the qualifier value or bean name of a specific bean definition.
     * @since 4.2
     * @see #value
     * @see CacheConfig#cacheNames
     */
    @AliasFor("value")
    String[] cacheNames() default {};

    /**
     * Spring Expression Language (SpEL) expression for computing the key dynamically.
     * <p>Default is {@code ""}, meaning all method parameters are considered as a key,
     * unless a custom {@link #keyGenerator} has been set.
     * <p>The SpEL expression evaluates against a dedicated context that provides the
     * following meta-data:
     * <ul>
     * <li>{@code #result} for a reference to the result of the method invocation, which
     * can only be used if {@link #beforeInvocation()} is {@code false}. For supported
     * wrappers such as {@code Optional}, {@code #result} refers to the actual object,
     * not the wrapper</li>
     * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
     * references to the {@link java.lang.reflect.Method method}, target object, and
     * affected cache(s) respectively.</li>
     * <li>Shortcuts for the method name ({@code #root.methodName}) and target class
     * ({@code #root.targetClass}) are also available.
     * <li>Method arguments can be accessed by index. For instance the second argument
     * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
     * can also be accessed by name if that information is available.</li>
     * </ul>
     */
    String key() default "";

    /**
     * The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator}
     * to use.
     * <p>Mutually exclusive with the {@link #key} attribute.
     * @see CacheConfig#keyGenerator
     */
    String keyGenerator() default "";

    /**
     * Spring Expression Language (SpEL) expression used for making the cache
     * eviction operation conditional.
     * <p>Default is {@code ""}, meaning the cache eviction is always performed.
     * <p>The SpEL expression evaluates against a dedicated context that provides the
     * following meta-data:
     * <ul>
     * <li>{@code #root.method}, {@code #root.target}, and {@code #root.caches} for
     * references to the {@link java.lang.reflect.Method method}, target object, and
     * affected cache(s) respectively.</li>
     * <li>Shortcuts for the method name ({@code #root.methodName}) and target class
     * ({@code #root.targetClass}) are also available.
     * <li>Method arguments can be accessed by index. For instance the second argument
     * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
     * can also be accessed by name if that information is available.</li>
     * </ul>
     */
    String condition() default "";
}

2.3 Redis配置注入

@Configuration
@ConfigurationProperties(prefix = "redis")
@Data
@Slf4j
@RefreshScope
public class CacheAutoConfig {


    @Bean
    public JedisPoolConfig jedisPoolConfig() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(maxTotal);
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMinIdle(minIdle);
        jedisPoolConfig.setBlockWhenExhausted(true);
        jedisPoolConfig.setNumTestsPerEvictionRun(10);
        jedisPoolConfig.setTimeBetweenEvictionRunsMillis(120000);
        jedisPoolConfig.setMinEvictableIdleTimeMillis(60000);
        jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(60000);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);

        return jedisPoolConfig;
    }

    @Bean
    public RedisPoolConfig redisPoolConfig(JedisPoolConfig jedisPoolConfig) {
        RedisPoolConfig redisPoolConfig = new RedisPoolConfig(jedisPoolConfig);
        redisPoolConfig.setEagerInit(true);
        redisPoolConfig.setLoadBalancer(ConsistentHashSorted);

        return redisPoolConfig;
    }

    @Bean
    public ICacheService cacheService(RedisClusterPoolClient redisClusterPoolClient) {
        CacheServiceRedisImpl cacheService = new CacheServiceRedisImpl();
        // 此处略去部分代码

        return cacheService;
    }

    @Bean
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            StringBuffer sb = new StringBuffer();
            sb.append(target.getClass().getName()).append("#");
            sb.append(method.getName()).append("(");
            sb.append(Arrays.asList(params).toString()).append(")");
            return sb.toString();
        };
    }

    @Bean
    public ExpressionParser expressionParser() {
        return new SpelExpressionParser();
    }

}

注解处理类BossCacheAnnotationAspect

@Slf4j
@Aspect
@Component
@Order(1000)
public class BossCacheAnnotationAspect implements BeanFactoryAware {

    @Resource
    private ICacheManagerService cacheManagerService;

    @Autowired
    private KeyGenerator keyGenerator;

    @Autowired
    private ExpressionParser expressionParser;

    private BeanFactory beanFactory;

    private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    private EvaluationContext createEvaluationContext(Object target, Method targetMethod, Object[] args) {
        return new MethodBasedEvaluationContext(target, targetMethod, args, parameterNameDiscoverer);
    }

    @Around(value = "@annotation(bossCacheable)")
    private Object around(ProceedingJoinPoint point, BossCacheable bossCacheable) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        String key = bossCacheable.key();
        String generator = bossCacheable.keyGenerator();

        // 生成缓存key
        String cacheKey = getCacheKey(point, method, key, generator);

        log.debug("========= cache key:【{}】========", cacheKey);
        long startTime = System.currentTimeMillis();
        if (StringUtils.isNotBlank(cacheKey)) {
            Object cacheValue = cacheManagerService.get(cacheKey, method.getReturnType());
            if (cacheValue != null) {
                log.debug("========= cache hit, cost time:{}ms, key:【{}】========", (System.currentTimeMillis() - startTime), cacheKey);
                return cacheValue;
            } else {
                Object resultValue = point.proceed();
                if (resultValue != null) {
                    addCacheKey(cacheKey, resultValue, bossCacheable.expire());
                }
                log.debug("========= cache miss, cost time:{}ms, key:【{}】========", (System.currentTimeMillis() - startTime), cacheKey);
                return resultValue;
            }
        } else {
            return point.proceed();
        }
    }

    @AfterReturning(value = "@annotation(bossCachePut)", returning = "result")
    private void put(JoinPoint point, Object result, BossCachePut bossCachePut) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        String key = bossCachePut.key();
        String generator = bossCachePut.keyGenerator();

        // 生成缓存key
        String cacheKey = getCacheKey(point, method, key, generator);

        log.debug("========= put cache key:【{}】========", cacheKey);

        if (StringUtils.isNotBlank(cacheKey) && result != null) {
            addCacheKey(cacheKey, result, bossCachePut.expire());
        }
    }

    @After(value = "@annotation(bossCacheEvict)")
    private void evict(JoinPoint point, BossCacheEvict bossCacheEvict) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        String key = bossCacheEvict.key();
        String generator = bossCacheEvict.keyGenerator();

        String cacheKey = getCacheKey(point, method, key, generator);

        log.debug("========= del cache key:【{}】========", cacheKey);

        cacheManagerService.del(cacheKey);
    }


    @Around(value = "@annotation(bossCacheLock)")
    private Object around(ProceedingJoinPoint point, BossCacheLock bossCacheLock) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        String key = bossCacheLock.key();
        String generator = bossCacheLock.keyGenerator();

        // 生成缓存key
        String cacheKey = getCacheKey(point, method, key, generator);
        Long expire = bossCacheLock.timeUnit().toMillis(bossCacheLock.lockTime());
        String uuid = UUID.randomUUID().toString();
        Boolean getLock = cacheManagerService.tryGetDistributedLock(cacheKey, uuid, expire);
        if (getLock) {
            return point.proceed();
        } else {
            return null;
        }
    }

    private String getCacheKey(JoinPoint point, Method method, String key, String generator) {
        // 生成缓存key
        String cacheKey;
        if (StringUtils.isNotBlank(key)) {
            EvaluationContext evaluationContext = createEvaluationContext(point.getTarget(), method, point.getArgs());
            cacheKey = getKey(key, evaluationContext).toString();
        } else {
            // 默认使用全局的keyGenerator
            KeyGenerator cacheKeyGenerator = keyGenerator;
            // 如果注解中的keyGenerator不为空,则使用自定义的keyGenerator
            if (StringUtils.isNotBlank(generator)) {
                cacheKeyGenerator = BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, KeyGenerator.class, generator);
            }
            cacheKey = cacheKeyGenerator.generate(point.getTarget(), method, point.getArgs()).toString();
        }

        return cacheKey;
    }

    private Object getKey(String key, EvaluationContext evaluationContext) {
        Expression expression = expressionParser.parseExpression(key);
        return expression.getValue(evaluationContext);
    }

    /**
     * 增加缓存,并将key存到list中,方便删除
     *
     * @param key
     * @param value
     * @param expire
     */
    private void addCacheKey(String key, Object value, Integer expire) {
        cacheManagerService.set(key, value, Object.class, expire);
    }
}

缓存服务


public interface ICacheManagerService {
    /**
     * 缓存字符串,可设置过期时间
     *
     * @param key     key
     * @param value   要换成的字符串
     * @param seconds 过期时间
     */
    void set(String key, String value, Integer seconds);

    /**
     * 缓存对象和过期时间
     *
     * @param key     key
     * @param object  所要缓存对象
     * @param clazz   对象的类别信息; 木有办法,泛型类别在运行时被擦除了,还得多传一下类信息用于序列,反序列化,麻烦一下吧
     * @param seconds 过期时间
     * @param <T>     对象泛型类别
     */
    <T> void set(String key, T object, Class<T> clazz, int seconds);

    String get(String key);

    Long del(String key);

    <T> T get(String key, Class<T> tClass);

    Boolean tryGetDistributedLock(String lockKey, String requestId, Long expireTime);
}

2.4 测试

import lombok.Data;

@Data
public class TestUser {
    private Integer id;

    private String name;
}
@Controller
public class CacheController {

    @RequestMapping("/cache/get")
    @ResponseBody
    @BossCacheable(key = "'user:'+#testUser.id", expire = 180)
    public String get(@RequestBody TestUser testUser) {
        return testUser.getName();
    }

    @RequestMapping("/cache/update")
    @ResponseBody
    @BossCachePut(key = "'user:'+#testUser.id", expire = 180)
    public String update(String key, @RequestBody TestUser testUser) {
        return key;
    }

    @RequestMapping("/cache/delete")
    @ResponseBody
    @BossCacheEvict(key = "'user:'+#testUser.id")
    public String delete(String key, @RequestBody TestUser testUser) {
        return key;
    }
}

三 参考资料

1. Spring缓存注解@Cacheable、@CacheEvict、@CachePut使用_灼烧的疯狂的博客-CSDN博客_cacheevict注解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

GeekerLou

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

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

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

打赏作者

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

抵扣说明:

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

余额充值