Spring声明式基于注解的缓存(3-精进篇)

简介: 目录一、序言二、如何自定义过期时间三、解决方案1、CacheManger的作用2、CacheResolver的作用四、代码示例1、自定义缓存相关注解(1) @TTLCacheable注解(2) @TTLCachePut注解2、自定义CacheResolver3、自定义CacheManager4、开启声明式缓存配置类五、测试用例1、 测试服务类2、 带过期时间的缓存操作3、 带过期时间的更新操作六、结语

目录

一、序言

二、如何自定义过期时间

三、解决方案

1、CacheManger的作用

2、CacheResolver的作用

四、代码示例

1、自定义缓存相关注解

(1) @TTLCacheable注解

(2) @TTLCachePut注解

2、自定义CacheResolver

3、自定义CacheManager

4、开启声明式缓存配置类

五、测试用例

1、 测试服务类

2、 带过期时间的缓存操作

3、 带过期时间的更新操作

六、结语

一、序言


在上一节 Spring声明式基于注解的缓存(2-实践篇)中给出了一些声明式基于注解的缓存实际使用案例。在这一节中,我们会通过自定义CacheResolverRedisCacheManager还有Cache相关注解来实现带过期时间的缓存方案。

二、如何自定义过期时间


在实例化RedisCacheManager时,我们可以指定key过期的entryTtl属性,如下:

@EnableCaching
@Configuration
public class RedisCacheConfig {
  private static final String KEY_SEPERATOR = ":";
  /**
   * 自定义CacheManager,具体配置参考{@link org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration}
   * @param redisConnectionFactory 自动配置会注入
   * @return
   */
  @Bean(name = "redisCacheManager")
  public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
    RedisSerializer<String> keySerializer = new StringRedisSerializer();
    RedisSerializer<Object> valueSerializer = new GenericJackson2JsonRedisSerializer();
    RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
      .serializeKeysWith(SerializationPair.fromSerializer(keySerializer))
      .serializeValuesWith(SerializationPair.fromSerializer(valueSerializer))
      .computePrefixWith(key -> key.concat(KEY_SEPERATOR))
      .entryTtl(Duration.ofSeconds(1));
    return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(cacheConfig).build();
  }
}


备注:这种方式有一个很明显的缺点,所有key都会共享配置,比如这里会设置所有key的过期时间都为1秒。

三、解决方案



1、CacheManger的作用

在Spring声明式基于注解的缓存(1-理论篇)中我们了解到CacheManager主要有两个方法。一个是根据指定缓存名获取Cache实例,还有一个是获取所有缓存名称的。

public interface CacheManager {
  /**
   * Get the cache associated with the given name.
   * <p>Note that the cache may be lazily created at runtime if the
   * native provider supports it.
   * @param name the cache identifier (must not be {@code null})
   * @return the associated cache, or {@code null} if such a cache
   * does not exist or could be not created
   */
  @Nullable
  Cache getCache(String name);
  /**
   * Get a collection of the cache names known by this manager.
   * @return the names of all caches known by the cache manager
   */
  Collection<String> getCacheNames();
}

让我们看看RedisCacheManagerCacheManager的实现,实际上中间还继承了两个抽象类,如下:5faf3120103147afad9eb4bd5408b303.png其中getCache()方法的实现逻辑主要在AbstractCacheManager中,如下:

@Override
@Nullable
public Cache getCache(String name) {
  // Quick check for existing cache...
  Cache cache = this.cacheMap.get(name);
  if (cache != null) {
    return cache;
  }
  // The provider may support on-demand cache creation...
  Cache missingCache = getMissingCache(name);
  if (missingCache != null) {
    // Fully synchronize now for missing cache registration
    synchronized (this.cacheMap) {
      cache = this.cacheMap.get(name);
      if (cache == null) {
        cache = decorateCache(missingCache);
        this.cacheMap.put(name, cache);
        updateCacheNames(name);
      }
    }
  }
  return cache;
}


有经验的同学在看到decorateCache方法时绝对会眼前一亮,见名知意,这个方法就是用来装饰根据指定缓存名称获取到的缓存实例的,这个方法也正是交给子类来实现。(Ps:这里用到的是模板方法模式)


而decorateCache方法实际上是由AbstractTransactionSupportingCacheManager来实现的,该抽象类在装饰缓存时会附加事务的支持,比如在事务提交之后缓存,如下:

@Override
protected Cache decorateCache(Cache cache) {
  return (isTransactionAware() ? new TransactionAwareCacheDecorator(cache) : cache);
}


2、CacheResolver的作用


@FunctionalInterface
public interface CacheResolver {
  /**
   * Return the cache(s) to use for the specified invocation.
   * @param context the context of the particular invocation
   * @return the cache(s) to use (never {@code null})
   * @throws IllegalStateException if cache resolution failed
   */
  Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context);
}

该接口有个抽象类实现AbstractCacheResolver,对resolveCaches的实现如下:

@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
  Collection<String> cacheNames = getCacheNames(context);
  if (cacheNames == null) {
    return Collections.emptyList();
  }
  Collection<Cache> result = new ArrayList<>(cacheNames.size());
  for (String cacheName : cacheNames) {
    Cache cache = getCacheManager().getCache(cacheName);
    if (cache == null) {
      throw new IllegalArgumentException("Cannot find cache named '" +
          cacheName + "' for " + context.getOperation());
    }
    result.add(cache);
  }
  return result;
}


四、代码示例


1、自定义缓存相关注解

Spring中缓存相关注解同样可以作为元注解,这里我们自定义了@TTLCacheable@TTLCachePut两个注解,并且指定了名为ttlCacheResolver的缓存解析器实例。

(1) @TTLCacheable注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Cacheable(cacheResolver = "ttlCacheResolver")
public @interface TTLCacheable {
  @AliasFor(annotation = Cacheable.class)
  String[] value() default {};
  @AliasFor(annotation = Cacheable.class)
  String[] cacheNames() default {};
  @AliasFor(annotation = Cacheable.class)
  String key() default "";
  @AliasFor(annotation = Cacheable.class)
  String keyGenerator() default "";
  @AliasFor(annotation = Cacheable.class)
  String cacheManager() default "";
  @AliasFor(annotation = Cacheable.class)
  String condition() default "";
  @AliasFor(annotation = Cacheable.class)
  String unless() default "";
  @AliasFor(annotation = Cacheable.class)
  boolean sync() default false;
  /**
   * time to live
   */
  long ttl() default 0L;
  /**
   * 时间单位
   * @return
   */
  TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}


(2) @TTLCachePut注解

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@CachePut(cacheResolver = "ttlCacheResolver")
public @interface TTLCachePut {
  @AliasFor(annotation = CachePut.class)
  String[] value() default {};
  @AliasFor(annotation = CachePut.class)
  String[] cacheNames() default {};
  @AliasFor(annotation = CachePut.class)
  String key() default "";
  @AliasFor(annotation = CachePut.class)
  String keyGenerator() default "";
  @AliasFor(annotation = CachePut.class)
  String cacheManager() default "";
  @AliasFor(annotation = CachePut.class)
  String condition() default "";
  @AliasFor(annotation = CachePut.class)
  String unless() default "";
  /**
   * time to live
   */
  long ttl() default 0L;
  /**
   * 时间单位
   * @return
   */
  TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}

2、自定义CacheResolver

这里我们直接继承SimpleCacheResolver,在解析缓存时根据注解中的过期时间配置动态给CacheManager传值,然后再调用AbstractCacheResolverresolveCaches方法进行实际的缓存解析操作。

public class TTLCacheResolver extends SimpleCacheResolver {
  public TTLCacheResolver() {
  }
  public TTLCacheResolver(CacheManager cacheManager) {
    super(cacheManager);
  }
  @Override
  public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
    TTLCacheable ttlCacheable = context.getMethod().getAnnotation(TTLCacheable.class);
    TTLCachePut ttlCachePut = context.getMethod().getAnnotation(TTLCachePut.class);
    CacheManager cacheManager = super.getCacheManager();
    if (cacheManager instanceof TTLRedisCacheManager) {
      TTLRedisCacheManager ttlRedisCacheManager = (TTLRedisCacheManager) cacheManager;
      Optional.ofNullable(ttlCacheable).ifPresent(cacheable -> {
        ttlRedisCacheManager.setTtl(cacheable.ttl());
        ttlRedisCacheManager.setTimeUnit(cacheable.timeUnit());
      });
      Optional.ofNullable(ttlCachePut).ifPresent(cachePut -> {
        ttlRedisCacheManager.setTtl(cachePut.ttl());
        ttlRedisCacheManager.setTimeUnit(cachePut.timeUnit());
      });
    }
    return super.resolveCaches(context);
  }
}

3、自定义CacheManager

这里我们直接重写了RedisCacheManager

public class TTLRedisCacheManager extends RedisCacheManager {
  /**
   * 过期时间,具体见{@link com.netease.cache.distrubuted.redis.integration.custom.annotation.TTLCacheable}
   * 中的ttl说明
   */
  private long ttl;
  /**
   * 时间单位
   */
  private TimeUnit timeUnit;
  public TTLRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
    super(cacheWriter, defaultCacheConfiguration);
  }
  public TTLRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
    String... initialCacheNames) {
    super(cacheWriter, defaultCacheConfiguration, initialCacheNames);
  }
  public TTLRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
    boolean allowInFlightCacheCreation, String... initialCacheNames) {
    super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheNames);
  }
  public TTLRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
    Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
    super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
  }
  public TTLRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
    Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
    super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);
  }
  public void setTtl(long ttl) {
    this.ttl = ttl;
  }
  public void setTimeUnit(TimeUnit timeUnit) {
    this.timeUnit = timeUnit;
  }
  /**
   * CacheResolver调用CacheManager的getCache方法后会调用该方法进行装饰,这里我们可以给Cache加上过期时间
   * @param cache
   * @return
   */
  @Override
  protected Cache decorateCache(Cache cache) {
    RedisCache redisCache = (RedisCache) cache;
    RedisCacheConfiguration config = redisCache.getCacheConfiguration().entryTtl(resolveExpiryTime(ttl, timeUnit));
    return super.decorateCache(super.createRedisCache(redisCache.getName(), config));
  }
  private Duration resolveExpiryTime(long timeToLive, TimeUnit timeUnit) {
    return Duration.ofMillis(timeUnit.toMillis(timeToLive));
  }
}


4、开启声明式缓存配置类

@EnableCaching
@Configuration
public class TTLRedisCacheConfig {
  private static final String KEY_SEPERATOR = ":";
  @Bean(name = "ttlRedisCacheManager")
  public TTLRedisCacheManager ttlRedisCacheManager(RedisConnectionFactory redisConnectionFactory) {
    RedisSerializer<String> keySerializer = new StringRedisSerializer();
    RedisSerializer<Object> valueSerializer = new GenericJackson2JsonRedisSerializer();
    RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig();
    cacheConfig = cacheConfig.serializeKeysWith(SerializationPair.fromSerializer(keySerializer));
    cacheConfig = cacheConfig.serializeValuesWith(SerializationPair.fromSerializer(valueSerializer));
    cacheConfig = cacheConfig.computePrefixWith(key -> "ttl" + KEY_SEPERATOR + key + KEY_SEPERATOR);
    RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
    return new TTLRedisCacheManager(redisCacheWriter, cacheConfig);
  }
  @Bean(name = "ttlCacheResolver")
  public TTLCacheResolver ttlCacheResolver(TTLRedisCacheManager ttlRedisCacheManager) {
    return new TTLCacheResolver(ttlRedisCacheManager);
  }
}

备注:这里我们用自定义的TTLCacheManagerTTLCacheResolver初始化配置即可,缓存key的名称指定了前缀ttl:

五、测试用例


1、 测试服务类

@Service
public class TTLSpringCacheService {
  @TTLCacheable(cacheNames = "student-cache", key = "#stuNo", ttl = 200, timeUnit = TimeUnit.SECONDS)
  public StudentDO getStudentWithTTL(int stuNo, String stuName) {
    StudentDO student = new StudentDO(stuNo, stuName);
    System.out.println("模拟从数据库中读取...");
    return student;
  }
  @TTLCachePut(cacheNames = "student-cache", key = "#student.stuNo", ttl = 1, timeUnit = TimeUnit.MINUTES)
  public StudentDO updateStudent(StudentDO student) {
    System.out.println("数据库进行了更新,检查缓存是否一致");
    return student;
  }
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class TTLSpringCacheIntegrationTest {
  @Autowired
  private TTLSpringCacheService ttlSpringCacheService;
  @Test
  public void getStudentWithTTLTest() {
    StudentDO studentDO = ttlSpringCacheService.getStudentWithTTL(1, "Nick");
    System.out.println(studentDO);
  }
  @Test
  public void updateStudentWithTTLTest() {
    StudentDO studentDO = ttlSpringCacheService.updateStudent(new StudentDO(1, "Licky"));
    System.out.println(studentDO);
  }
}


2、 带过期时间的缓存操作

调用getStudentWithTTLTest方法,这里我们指定了缓存的过期时间为200秒,查看Redis中key对应的值,如下:bd07cc85db66451285072eb177c3180b.png

3、 带过期时间的更新操作

调用updateStudentWithTTLTest方法,更新时我们指定了缓存的过期时间为1分钟,查看Redis中key对应的值,如下:4eac4ee422e244dcb99bd87815779a70.png

六、结语


Spring基于注解的缓存抽象就到这里啦,Spring源码还是比较清晰易懂的,见名知意。除了自定义方案,阿里爸爸也有一个缓存抽象解决方案,叫做 jetcache。


它是Spring缓存抽象的增强版,提供的特性有带过期时间的缓存、二级缓存、自动缓存更新、代码操作缓存实例等,有兴趣的同学可以去体验一下。


相关文章
|
5月前
|
缓存 监控 Java
SpringBoot @Scheduled 注解详解
使用`@Scheduled`注解实现方法周期性执行,支持固定间隔、延迟或Cron表达式触发,基于Spring Task,适用于日志清理、数据同步等定时任务场景。需启用`@EnableScheduling`,注意线程阻塞与分布式重复问题,推荐结合`@Async`异步处理,提升任务调度效率。
858 128
|
4月前
|
XML Java 应用服务中间件
【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
SpringBoot专栏开篇第一章,讲述认识SpringBoot、Bean容器功能的讲解、自动装配原理的入门,还有其他常用的Springboot注解!如果想要了解SpringBoot,那么就进来看看吧!
544 2
|
5月前
|
XML Java 数据格式
常用SpringBoot注解汇总与用法说明
这些注解的使用和组合是Spring Boot快速开发和微服务实现的基础,通过它们,可以有效地指导Spring容器进行类发现、自动装配、配置、代理和管理等核心功能。开发者应当根据项目实际需求,运用这些注解来优化代码结构和服务逻辑。
420 12
|
5月前
|
缓存 Java 应用服务中间件
Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
本文详解Spring Boot十大核心配置优化技巧,涵盖Tomcat连接池、数据库连接池、Jackson时区、日志管理、缓存策略、异步线程池等关键配置,结合代码示例与通俗解释,助你轻松掌握高并发场景下的性能调优方法,适用于实际项目落地。
859 5
|
5月前
|
传感器 Java 数据库
探索Spring Boot的@Conditional注解的上下文配置
Spring Boot 的 `@Conditional` 注解可根据不同条件动态控制 Bean 的加载,提升应用的灵活性与可配置性。本文深入解析其用法与优势,并结合实例展示如何通过自定义条件类实现环境适配的智能配置。
291 0
探索Spring Boot的@Conditional注解的上下文配置
|
5月前
|
智能设计 Java 测试技术
Spring中最大化@Lazy注解,实现资源高效利用
本文深入探讨了 Spring 框架中的 `@Lazy` 注解,介绍了其在资源管理和性能优化中的作用。通过延迟初始化 Bean,`@Lazy` 可显著提升应用启动速度,合理利用系统资源,并增强对 Bean 生命周期的控制。文章还分析了 `@Lazy` 的工作机制、使用场景、最佳实践以及常见陷阱与解决方案,帮助开发者更高效地构建可扩展、高性能的 Spring 应用程序。
224 0
Spring中最大化@Lazy注解,实现资源高效利用
|
7月前
|
Java Spring 容器
SpringBoot自动配置的原理是什么?
Spring Boot自动配置核心在于@EnableAutoConfiguration注解,它通过@Import导入配置选择器,加载META-INF/spring.factories中定义的自动配置类。这些类根据@Conditional系列注解判断是否生效。但Spring Boot 3.0后已弃用spring.factories,改用新格式的.imports文件进行配置。
1155 0
|
8月前
|
人工智能 Java 测试技术
Spring Boot 集成 JUnit 单元测试
本文介绍了在Spring Boot中使用JUnit 5进行单元测试的常用方法与技巧,包括添加依赖、编写测试类、使用@SpringBootTest参数、自动装配测试模块(如JSON、MVC、WebFlux、JDBC等),以及@MockBean和@SpyBean的应用。内容实用,适合Java开发者参考学习。
929 0
|
4月前
|
JavaScript Java Maven
【SpringBoot(二)】带你认识Yaml配置文件类型、SpringMVC的资源访问路径 和 静态资源配置的原理!
SpringBoot专栏第二章,从本章开始正式进入SpringBoot的WEB阶段开发,本章先带你认识yaml配置文件和资源的路径配置原理,以方便在后面的文章中打下基础
461 3
|
4月前
|
Java 测试技术 数据库连接
【SpringBoot(四)】还不懂文件上传?JUnit使用?本文带你了解SpringBoot的文件上传、异常处理、组件注入等知识!并且带你领悟JUnit单元测试的使用!
Spring专栏第四章,本文带你上手 SpringBoot 的文件上传、异常处理、组件注入等功能 并且为你演示Junit5的基础上手体验
969 2