Spring Cache 注解

Last Modified: 2022/12/01

Spring Cache 特性

  • 开箱即用,提供缓存编程一致性抽象,方便切换各种底层缓存实现。例如:guava/redis/echcache。缓存逻辑的透明化,让你只需专注于业务逻辑(让你变得更无知);
  • 缓存可以感知 Spring 事务,这需要底层缓存的支持;
  • 通过 SPEL,支持复杂的缓存逻辑。

开启缓存

通过注解开启缓存

@Configuration
@EnableCaching
public class CachingConfig {
    @Bean
    public CacheManager cacheManager() {
        // other cache implements like guava or ehcache can be used if you wish
        return new ConcurrentMapCacheManager("users");
    }
}

通过 xml 配置文件开启缓存

<!-- xml 配置等价于上面基于注解的配置 -->
<cache:annotation-driven />
<bean id="cacheManager" class="org.springframework.cache.concurrent.ConcurrentMapCacheManager">
    <property name="cacheNames">
        <list>
            <value>users</value>
        </list>
    </property>
</bean>

注意:生产环境下应该使用ehcache/redis等功能更完备的缓存替代!

缓存相关的那些注解们

一旦你开启了 Spring Cache 之后,你就可以通过 Spring 提供的专门注解来驱动缓存,先拉出来晒一下:

  • @Cacheable
  • @CacheEvict
  • @CachePut
  • @CacheConfig
  • @Caching

@Cacheable

如果一个方法开销很大,而且方法调用结果不常改变,那么我们就可以考虑缓存这个方法的调用结果,下次调用时,直接从缓存中取出结果即可。而你要做的仅仅是给该方法添加一个 @Cacheable 注解。

// calcResult 是用于缓存计算结果的缓存名称
@Cacheable(cacheNames="calcResult")
public int costlyCalcMethod(int para) { // 耗时的计算  }

首次调用该方法,会将 (para, 方法调用结果) 键值对放入名为 calcResult 的缓存中。

@CacheEvict

通过 @Cacheable 可以缓存结果,但是程序中某些条件的变化,可能需要我们重新计算缓存,这个时候需要我们移除之前缓存的结果,而你要做的仅仅给方法添加一个 @CacheEvict 注解。

@CacheEvict(cacheNames="calcResult")
public void someMethod(int para) { 
  // 该方法调用会导致计算结果失效,因此需要移除缓存。
}

调用该方法,将会从名为 calcResult 的缓存中移除键值 para。

// allEntries 将会清空 calcResult 缓存
@CacheEvict(cacheNames="calcResult", allEntries=true)
// 默认情况下在方法调用之后清空缓存,但是可以通过 beforeInvocation 在方法调用之前就清空缓存。
@CacheEvict(cacheNames="calcResult", beforeInvocation=true)

@CachePut

@CachePut 是用来更新缓存,它不同于 @Cacheable,被该注解标注的方法每次都会调用,调用的结果将会放入到缓存中。

@CachePut(value="users")
public User getUser(int id) {...}

@Caching

如果调用一个方法后,需要清除多个缓存,怎么办呢?

@CacheEvict(cacheNames="cache1", key="#user.id")
@CacheEvict(cacheNames="cache2", key="#user.id")
public void methodCauseMultipleCacheKeyExpire(User user) {...}

遗憾的早期的 Java 不支持以上写法,此时就是 @Caching 出场了。

@Caching(evict = {
  @CacheEvict(cacheNames="cache1", key="#user.id"), 
  @CacheEvict(cacheNames="cache2", key="#user.id") })
public void methodCauseMultipleCacheKeyExpire(User user) {...}

PS: Java8 已经支持重复注解了,像这样:

@Repeatable(RequirePermission.class)
public @interface RequirePermission {
    String name();
}

如果重复注解早点出现的话,或许就不会有 @Caching 注解了。

@CacheConfig

如果你有多个方法,注解的属性都相似,那么可以通过该注解抽出公共的属性。

public class UserService {
    @Cacheable(cacheNames="users")
    public User getUser(int id) {...}
    @CacheEvict(cacheNames="users")
    public User updateUser(int id) {...}
}

// 通过 @CacheConfig 抽出公共的 cacheNames 属性
@CacheConfig(cacheNames="users")
public class UserService {
    @Cacheable
    public User getUser(int id) {...}
    @CacheEvict
    public User updateUser(int id) {...}
}

条件化缓存

真实业务场景下,是否缓存以及是否要移除缓存,往往要根据某些条件动态判断的,这个时候就需要使用条件化缓存,在 Spring 中是通过 SPEL 提供支持的。

//当参数 age 的值大于10时,缓存方法调用结果
@Cacheable(cacheNames="users", condition="#age>10")
public User getUser(int id, int age) {...}

除了 condition 属性之外,unless 属性也可以表达条件缓存。调用结果是否缓存是 condition/unless 条件共同决定的,condition 条件决定是否生成缓存请求,之后由 unless 决定缓存请求是否执行。

//当方法返回的用户年龄大于10时,缓存方法调用结果
@Cacheable(cacheNames="users", unless="#result.age<=10")
public User getUser(int id) {...}

其它一些可使用的 SPEL 表达式:

缓存的键值

上面在介绍使用时,都没提到缓存的键值问题,Spring 提供了 KeyGenerator 接口抽象了键值生成逻辑。

public interface KeyGenerator {
    Object generate(Object target, Method method, Object... params);
}

Spring3.x 默认的键值生成器是 DefaultKeyGenerator,由于它的实现易造成键值冲突,因此在4.x及以上版本已被标记为废弃,4.x及以上版本默认使用 SimpleKeyGenerator,该生成器默认使用方法的所有参数来生成键值。

如果你愿意折腾的话,也可以自定义键值生成器:

<!-- MyKeyGenerator implements KeyGenerator interface -->
<bean id="myKeyGenerator" class="com.jstar.MyKeyGenerator"/>
@Cacheable(cacheNames="users", keyGenerator="myKeyGenerator")
public getUser(int id) {...}
有问题吗?点此反馈!

温馨提示:反馈需要登录