玩转Spring Cache --- @Cacheable/@CachePut/@CacheEvict缓存注解相关基础类打点【享学Spring】(上)

简介: 玩转Spring Cache --- @Cacheable/@CachePut/@CacheEvict缓存注解相关基础类打点【享学Spring】(上)

前言


本文算是了解缓存注解原理的先行文章,因为它抽象出来的模块类比较多,所以做这篇文章进行关键类的打点。

若我们需要扩展缓存注解的能力,对这些抽象是非常有必要深入了解的~


Spring内置的三大注解缓存是:


  1. Cacheable:缓存
  2. CacheEvict:删除缓存
  3. CachePut:更新缓存


CacheOperation:缓存操作


它是缓存操作的基类。我们知道不同的缓存注解,都有不同的缓存操作并且注解内的属性也挺多,此类就是对缓存操作的抽象


// @since 3.1  三大缓存注解属性的基类~~~
public abstract class CacheOperation implements BasicOperation {
  private final String name;
  private final Set<String> cacheNames;
  private final String key;
  private final String keyGenerator;
  private final String cacheManager;
  private final String cacheResolver;
  private final String condition;
  // 把toString()作为一个成员变量记着了   因为调用的次数太多
  private final String toString;
  // 构造方法是protected 的~~~  入参一个Builder来对各个属性赋值
  // builder方式是@since 4.3提供的,显然Spring4.3对这部分进行了改造~
  protected CacheOperation(Builder b) {
    this.name = b.name;
    this.cacheNames = b.cacheNames;
    this.key = b.key;
    this.keyGenerator = b.keyGenerator;
    this.cacheManager = b.cacheManager;
    this.cacheResolver = b.cacheResolver;
    this.condition = b.condition;
    this.toString = b.getOperationDescription().toString();
  }
  ... // 省略所有的get/set(其实builder里都只有set方法~~~)
  // 它的public静态抽象内部类  Builder  简单的说,它是构建一个CacheOperation的构建器
  public abstract static class Builder {
    private String name = "";
    private Set<String> cacheNames = Collections.emptySet();
    private String key = "";
    private String keyGenerator = "";
    private String cacheManager = "";
    private String cacheResolver = "";
    private String condition = "";
    // 省略所有的get/set
    // 抽象方法  自行实现~
    public abstract CacheOperation build();
  }
}
// @since 4.1 
public interface BasicOperation {
  Set<String> getCacheNames();
}


它的继承图谱如下

image.png


对应着三个注解,Spring提供了三种不同的操作实现。基类CacheOperation里封装的是三哥们都共有的属性,所以实现类里处理各自的个性化属性~~~~


// @since 3.1
public class CacheableOperation extends CacheOperation {
  @Nullable
  private final String unless;
  private final boolean sync;
  public CacheableOperation(CacheableOperation.Builder b) {
    super(b);
    this.unless = b.unless;
    this.sync = b.sync;
  }
  ... // 省略get方法(无set方法哦)
  // @since 4.3
  public static class Builder extends CacheOperation.Builder {
    @Nullable
    private String unless;
    private boolean sync;
    ... // 生路set方法(没有get方法哦~)
    // Spring4.3抽象的这个技巧还是不错的,此处传this进去即可
    @Override
    public CacheableOperation build() {
      return new CacheableOperation(this);
    }
    @Override
    protected StringBuilder getOperationDescription() {
      StringBuilder sb = super.getOperationDescription();
      sb.append(" | unless='");
      sb.append(this.unless);
      sb.append("'");
      sb.append(" | sync='");
      sb.append(this.sync);
      sb.append("'");
      return sb;
    }
  }
}

因为三哥们的实现完全一样,所以此处只需要举CacheableOperation这一个例子即可


CacheOperationSource:缓存属性源



缓存属性源。该接口被CacheInterceptor它使用。它能够获取到Method上所有的缓存操作集合:


// @since 3.1
public interface CacheOperationSource {
  // 返回此Method方法上面所有的缓存操作~~~~CacheOperation 集合
  // 显然一个Method上可以对应有多个缓存操作~~~~
  @Nullable
  Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> targetClass);
}


它的继承树如下:


image.png


这里面有最为重要的AnnotationCacheOperationSource,以及还有NameMatchCacheOperationSource根据名称匹配的实现。


NameMatchCacheOperationSource


根据方法名称来匹配看看作用在此方法上的缓存操作有哪些~(不需要注解了)


// @since 3.1
public class NameMatchCacheOperationSource implements CacheOperationSource, Serializable {
  /** Keys are method names; values are TransactionAttributes. */
  private Map<String, Collection<CacheOperation>> nameMap = new LinkedHashMap<>();
  // 你配置的时候,可以调用此方法。这里使用的也是add方法
  // 先拦截这个方法,然后看看这个方法有木有匹配的缓存操作,有点想AOP的配置
  public void setNameMap(Map<String, Collection<CacheOperation>> nameMap) {
    nameMap.forEach(this::addCacheMethod);
  }
  public void addCacheMethod(String methodName, Collection<CacheOperation> ops) {
    // 输出debug日志
    if (logger.isDebugEnabled()) {
      logger.debug("Adding method [" + methodName + "] with cache operations [" + ops + "]");
    }
    this.nameMap.put(methodName, ops);
  }
  // 这部分逻辑挺简单,就是根据方法去Map类里匹配到一个最为合适的Collection<CacheOperation>
  @Override
  @Nullable
  public Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> targetClass) {
    // look for direct name match
    String methodName = method.getName();
    Collection<CacheOperation> ops = this.nameMap.get(methodName);
    // 若不是null就直接return了~  首次进来,都会进入到这个逻辑~~~~
    if (ops == null) {
      // Look for most specific name match. // 找打一个最适合的  最匹配的
      String bestNameMatch = null;
      // 遍历所有外部已经制定进来了的方法名们~~~~
      for (String mappedName : this.nameMap.keySet()) {
        // isMatch就是Ant风格匹配~~(第一步)  光Ant匹配上了还不算
        // 第二步:bestNameMatch=null或者
        if (isMatch(methodName, mappedName)
            && (bestNameMatch == null || bestNameMatch.length() <= mappedName.length())) {
          // mappedName为匹配上了的名称~~~~ 给bestNameMatch 赋值  
          // 继续循环,最终找到一个最佳的匹配~~~~
          ops = this.nameMap.get(mappedName);
          bestNameMatch = mappedName;
        }
      }
    }
    return ops;
  }
  protected boolean isMatch(String methodName, String mappedName) {
    return PatternMatchUtils.simpleMatch(mappedName, methodName);
  }
  ...
}


这个Collection<CacheOperation>的匹配规则很简单,就是使用methodName进行匹配的,支持Ant风格匹配模式。

AbstractFallbackCacheOperationSource


这个抽象方法主要目的是:让缓存注解(当然此抽象类并不要求一定是注解,别的方式也成)既能使用在类上,也能使用在方法上。方法上没找到,就Fallback到类上去找。


并且它还支持把注解写在接口上,哪怕你只是一个JDK动态代理的实现而已。比如我们的MyBatis Mapper接口上也是可以直接使用缓存注解的~


// @since 3.1
public abstract class AbstractFallbackCacheOperationSource implements CacheOperationSource {
  private static final Collection<CacheOperation> NULL_CACHING_ATTRIBUTE = Collections.emptyList();
  // 这个Map初始值可不小,因为它要缓存所有的Method
  private final Map<Object, Collection<CacheOperation>> attributeCache = new ConcurrentHashMap<>(1024);
  @Override
  @Nullable
  public Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> targetClass) {
    // 如果这个方法是Object的方法,那就不考虑缓存操作~~~
    if (method.getDeclaringClass() == Object.class) {
      return null;
    }
    // 以method和Class作为上面缓存Map的key
    Object cacheKey = getCacheKey(method, targetClass);
    Collection<CacheOperation> cached = this.attributeCache.get(cacheKey);
    if (cached != null) {
      return (cached != NULL_CACHING_ATTRIBUTE ? cached : null);
    }
    // 核心处理逻辑:包括AnnotationCacheOperationSource的主要逻辑也是沿用的这个模版
    else {
      // computeCacheOperations计算缓存属性,这个方法是本类的灵魂,见下面
      Collection<CacheOperation> cacheOps = computeCacheOperations(method, targetClass);
      if (cacheOps != null) {
        if (logger.isTraceEnabled()) {
          logger.trace("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps);
        }
        this.attributeCache.put(cacheKey, cacheOps);
      } else { // 若没有标注属性的方法,用NULL_CACHING_ATTRIBUTE占位~ 不用null值哦~~~~ Spring内部大都不直接使用null
        this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE);
      }
      return cacheOps;
    }
  }
  @Nullable
  private Collection<CacheOperation> computeCacheOperations(Method method, @Nullable Class<?> targetClass) {
    // Don't allow no-public methods as required.
    // allowPublicMethodsOnly()默认是false(子类复写后的默认值已经写为true了)
    // 也就是说:缓存注解只能标注在public方法上~~~~不接收别非public方法~~~
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
    }
    // The method may be on an interface, but we need attributes from the target class.
    // If the target class is null, the method will be unchanged.
    Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
    // First try is the method in the target class.
    // 第一步:先去该方法上找,看看有木有啥缓存属性  有就返回
    Collection<CacheOperation> opDef = findCacheOperations(specificMethod);
    if (opDef != null) {
      return opDef;
    }
    // Second try is the caching operation on the target class.
    // 第二步:方法上没有,就再去方法所在的类上去找。
    // isUserLevelMethod:我们自己书写的方法(非自动生成的) 才直接return,否则继续处理
    opDef = findCacheOperations(specificMethod.getDeclaringClass());
    if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
      return opDef;
    }
    // 他俩不相等,说明method这个方法它是标注在接口上的,这里也给与了支持
    // 此处透露的性息:我们的缓存注解也可以标注在接口方法上,比如MyBatis的接口上都是ok的~~~~
    if (specificMethod != method) {
      // Fallback is to look at the original method.
      opDef = findCacheOperations(method);
      if (opDef != null) {
        return opDef;
      }
      // Last fallback is the class of the original method.
      opDef = findCacheOperations(method.getDeclaringClass());
      if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
        return opDef;
      }
    }
    return null;
  }
  @Nullable
  protected abstract Collection<CacheOperation> findCacheOperations(Class<?> clazz);
  @Nullable
  protected abstract Collection<CacheOperation> findCacheOperations(Method method);
  protected boolean allowPublicMethodsOnly() {
    return false;
  }
}


该抽象类提供的能力是:你的缓存属性可以放在方法上,方法上没有的话会去类上找,它有大名鼎鼎的实现类:AnnotationCacheOperationSource


AnnotationCacheOperationSource

从名字就可以看出,它是和缓存注解有关的缓存属性源。它能够处理上述的三大缓存注解。


// @since 3.1
public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperationSource implements Serializable {
  // 是否只允许此注解标注在public方法上???下面有设置值为true
  // 该属性只能通过构造函数赋值
  private final boolean publicMethodsOnly;
  private final Set<CacheAnnotationParser> annotationParsers;
  // 默认就设置了publicMethodsOnly=true
  public AnnotationCacheOperationSource() {
    this(true);
  }
  public AnnotationCacheOperationSource(boolean publicMethodsOnly) {
    this.publicMethodsOnly = publicMethodsOnly;
    this.annotationParsers = Collections.singleton(new SpringCacheAnnotationParser());
  }
  ...
  // 也可以自己来实现注解的解析器。(比如我们要做自定义注解的话,可以这么搞~~~)
  public AnnotationCacheOperationSource(CacheAnnotationParser... annotationParsers) {
    this.publicMethodsOnly = true;
    this.annotationParsers = new LinkedHashSet<>(Arrays.asList(annotationParsers));
  }
  // 这两个方法:核心事件都交给了CacheAnnotationParser.parseCacheAnnotations方法
  @Override
  @Nullable
  protected Collection<CacheOperation> findCacheOperations(Class<?> clazz) {
    return determineCacheOperations(parser -> parser.parseCacheAnnotations(clazz));
  }
  @Override
  @Nullable
  protected Collection<CacheOperation> findCacheOperations(Method method) {
    return determineCacheOperations(parser -> parser.parseCacheAnnotations(method));
  }
  // CacheOperationProvider就是一个函数式接口而已~~~类似Function~
  // 这块determineCacheOperations() + CacheOperationProvider接口的设计还是很巧妙的  可以学习一下
  @Nullable
  protected Collection<CacheOperation> determineCacheOperations(CacheOperationProvider provider) {
    Collection<CacheOperation> ops = null;
    // 调用我们设置进来的所有的CacheAnnotationParser一个一个的处理~~~
    for (CacheAnnotationParser annotationParser : this.annotationParsers) {
      Collection<CacheOperation> annOps = provider.getCacheOperations(annotationParser);
      // 理解这一块:说明我们方法上、类上是可以标注N个注解的,都会同时生效~~~最后都会combined 
      if (annOps != null) {
        if (ops == null) {
          ops = annOps;
        } else {
          Collection<CacheOperation> combined = new ArrayList<>(ops.size() + annOps.size());
          combined.addAll(ops);
          combined.addAll(annOps);
          ops = combined;
        }
      }
    }
    return ops;
  }
}

它专用于处理三大缓存注解,就是获取标注在方法上的缓存注解们。

另外需要说明的是:若你想扩展你自己的缓存注解(比如加上超时时间TTL),你的处理器可以可以继承自AnnotationCacheOperationSource,然后进行自己的扩展吧~


可以看到解析缓存注解这块,依托的一个处理器是:CacheAnnotationParser,下面继续介绍。

相关文章
|
缓存 监控 Java
SpringBoot @Scheduled 注解详解
使用`@Scheduled`注解实现方法周期性执行,支持固定间隔、延迟或Cron表达式触发,基于Spring Task,适用于日志清理、数据同步等定时任务场景。需启用`@EnableScheduling`,注意线程阻塞与分布式重复问题,推荐结合`@Async`异步处理,提升任务调度效率。
1710 128
|
11月前
|
XML Java 应用服务中间件
【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
SpringBoot专栏开篇第一章,讲述认识SpringBoot、Bean容器功能的讲解、自动装配原理的入门,还有其他常用的Springboot注解!如果想要了解SpringBoot,那么就进来看看吧!
870 2
|
XML Java 数据格式
常用SpringBoot注解汇总与用法说明
这些注解的使用和组合是Spring Boot快速开发和微服务实现的基础,通过它们,可以有效地指导Spring容器进行类发现、自动装配、配置、代理和管理等核心功能。开发者应当根据项目实际需求,运用这些注解来优化代码结构和服务逻辑。
771 12
|
传感器 Java 数据库
探索Spring Boot的@Conditional注解的上下文配置
Spring Boot 的 `@Conditional` 注解可根据不同条件动态控制 Bean 的加载,提升应用的灵活性与可配置性。本文深入解析其用法与优势,并结合实例展示如何通过自定义条件类实现环境适配的智能配置。
703 0
探索Spring Boot的@Conditional注解的上下文配置
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
缓存 NoSQL Java
Redis+Caffeine构建高性能二级缓存
大家好,我是摘星。今天为大家带来的是Redis+Caffeine构建高性能二级缓存,废话不多说直接开始~
1888 0
|
11月前
|
缓存 负载均衡 监控
135_负载均衡:Redis缓存 - 提高缓存命中率的配置与最佳实践
在现代大型语言模型(LLM)部署架构中,缓存系统扮演着至关重要的角色。随着LLM应用规模的不断扩大和用户需求的持续增长,如何构建高效、可靠的缓存架构成为系统性能优化的核心挑战。Redis作为业界领先的内存数据库,因其高性能、丰富的数据结构和灵活的配置选项,已成为LLM部署中首选的缓存解决方案。
1013 25
|
存储 缓存 NoSQL
Redis专题-实战篇二-商户查询缓存
本文介绍了缓存的基本概念、应用场景及实现方式,涵盖Redis缓存设计、缓存更新策略、缓存穿透问题及其解决方案。重点讲解了缓存空对象与布隆过滤器的使用,并通过代码示例演示了商铺查询的缓存优化实践。
469 1
Redis专题-实战篇二-商户查询缓存
|
11月前
|
缓存 运维 监控
Redis 7.0 高性能缓存架构设计与优化
🌟蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕Redis 7.0高性能缓存架构,探索函数化编程、多层缓存、集群优化与分片消息系统,用代码在二进制星河中谱写极客诗篇。
1948 3
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。