Spring基于注解的缓存声明深入探究

2022-11-13 14:11:36 缓存 注解 探究

一、概述

从3.1版本起,spring框架就已经支持将缓存添加到现有的Spring应用中,和事务支持一样,缓存抽象允许在对代码影响最小的情况下一致性地使用各种缓存解决方案。

从Spring 4.1版本起,有了jsR-107注解和更多定制化的选项支持后,缓存抽象有了重大的改进。

二、声明式基于注解的缓存

对于缓存声明,该抽象提供了一套Java注解:

  • @Cacheable:触发缓存构建。
  • @CacheEvict:触发缓存销毁。
  • @CachePut:更新缓存。
  • @Caching:重组应用到方法上的多个缓存操作。
  • @CacheConfig:类级别共享缓存相关的通用设置。

1、@Cacheable注解

正如其名,@Cacheable注解用来区分方法执行结果是否应该被缓存,如果后续该方法再次被调用,方法的执行结果直接从缓存中获取,而不会调用实际的方法逻辑。示例如下:

@Cacheable("books")
public Book findBook(ISBN isbn) {...}

当然我们也可以指定多个缓存名称,如果至少一个缓存被命中,那么关联的缓存结果就会返回,示例如下:

@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}

(1) 默认缓存key的生成

因为缓存都是key-value存储,每次缓存方法的调用都会被转义为缓存key的访问。Spring缓存抽象对于key的生成会采用KeyGenerator来生成,算法如下:

  • 如果没有方法参数,返回SimpleKey.EMPTY
  • 如果该方法只有一个参数,返回参数实例。
  • 如果方法不止一个参数,返回包含所有参数的SimpleKey实例。

这种key的生成策略适用于大部分场景,只要方法参数合理实现了hashCode()equals()方法。

SimpleKeyGenerator源码如下:

public class SimpleKeyGenerator implements KeyGenerator {
	@Override
	public Object generate(Object target, Method method, Object... params) {
		return generateKey(params);
	}
	
	public static Object generateKey(Object... params) {
		if (params.length == 0) {
			return SimpleKey.EMPTY;
		}
		if (params.length == 1) {
			Object param = params[0];
			if (param != null && !param.getClass().isArray()) {
				return param;
			}
		}
		return new SimpleKey(params);
	}
}

备注:如果要实现自定义key生成策略,需要实现org.springframework.cache.interceptor.KeyGenerator接口。

(2) 声明式自定义key生成

目标方法可能会有多个参数,有些参数可能只应用于方法逻辑,而不适合用作key的生成,例如:

@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

对于这种情况,@Cacheable注解有一个key属性,通过该属性可以自定义Key生成。我们也可以使用SPEL(Spring表达式语言)去指定参数或者参数的嵌套属性,示例如下:

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

当然,我们也可以通过@Cacheable注解指定自定义的KeyGenerator实例,示例如下:

@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

备注:keykeyGenerator参数是互斥的,如果两者都指定会触发异常。

(3) 默认缓存解析

Spring缓存抽象通过CacheResolver去解析操作级别的缓存,而CacheResolver会用CacheManager去获取缓存,接口定义如下:

@FunctionalInterface
public interface CacheResolver {
	
	Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context);
}
public interface CacheManager {
	
	@Nullable
	Cache getCache(String name);
	
	Collection<String> getCacheNames();
}

(4) 自定义缓存解析

默认缓存解析对于单CacheManager应用适应很好,对于有多个缓存管理器的应用,我们可以对每个操作设置缓存管理器,如下:

@Cacheable(cacheNames="books", cacheManager="anotherCacheManager")
public Book findBook(ISBN isbn) {...}

(5) 条件式缓存

有时方法缓存结果可能要取决于指定的参数,缓存注解通过支持SPELcondition属性实现该功能,示例如下:

@Cacheable(cacheNames="book", condition="#name.length() < 32")
public Book findBook(String name)

备注:只有当参数name的长度小于32时,方法结果才会被缓存。

除了condition属性,unless属性可以用来决定方法返回值不缓存。与condition不同,unless表达式在方法被调用后才会执行,示例如下:

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback")
public Book findBook(String name)

备注:如果Book对象的hardback属性为true则不缓存,为false才缓存。

当然,缓存抽象同时也支持java.util.Optional,只有当Optional中的值存在时,方法返回值才会被缓存。#result代表方法的执行结果,上面的我们可以改写:

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)

备注;#result引用的始终是Book对象,而不是Optional对象,因为返回值可能为空,所以我们应该使用安全导航操作符 => ?.

关于其它可以用的缓存SpEL表达式上下文,可以参考:Available Caching SpEL Evaluation Context。

2、@CachePut注解

这个注解主要用于更新缓存,也就说带有该注解的方法总是会执行,并且方法的返回值会刷新缓存。该注解和@Cacheable的参数相同,示例如下:

@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)

备注:@CachePut@Cacheable的主要区别在于后者会通过缓存跳过方法的执行,而前者为了更新缓存会迫使方法执行。

3、@CacheEvict注解

这个注解主要用来清除缓存,与@Cacheable注解相反,方法的执行会触发从缓存中删除数据。@CacheEvit注解要求指定一个或多个缓存名。除此之外,该注解还有一个额外的属性allEntries,指定该属性值为true后会清除某个缓存名下的所有缓存key。示例如下:

@CacheEvict(cacheNames="books", allEntries=true)
public void loadBooks(InputStream batch)

备注:缓存名为books下的所有缓存key都会被清除。

4、@Caching注解

有些情况下,相同类型多个注解,如@CacheEvict或者@CachePut需要被指定。@Caching注解允许多个嵌套@Cacheable@CachePut@CacheEvict注解用在同一个方法上。示例如下:

@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)

5、@CacheConfig注解

目前我们已经了解到缓存操作提供了很多定制化的选项,然而有些定制化选项如果应用到类中的所有操作可能会有些冗余,示例如下:

@CacheConfig("books")
public class BookRepositoryImpl implements BookRepository {
	@Cacheable
	public Book findBook(ISBN isbn) {...}
}

@CacheConfig是一个类级别的注解,这个注解可以共享缓存名称,自定义的KeyGenerator,自定义的CacheManager和自定义的CacheResolver

方法操作级别的自定义选项总是会重写@CacheConfig中的自定义选项。下面是每个缓存操作自定义选项对应的3个级别,优先级从上至下越来越高。

  • 全局配置的CacheManagerKeyGenerator等。
  • 类级别,通过@CacheConfig指定。
  • 方法操作级别。

三、开启声明式缓存注解

直接在配置类上加上#EnableCaching即可,如下:

@Configuration
@EnableCaching
public class AppConfig {
}

四、使用自定义注解

Spring缓存抽象允许我们用自定义注解去标识什么方法可以触发缓存构建或者消除。@Cacheable, @CachePut, @CacheEvict and @CacheConfig这些注解都可以作为元注解,其实即使可以修饰其它注解,示例如下:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface SlowService {
}

上面我们自定义了SlowService注解,该注解被@Cacheable所修饰,现在我们可以用自定义注解代替如下代码:

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

替代代码如下:

@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

尽管@SlowService注解并不是Spring原生注解,但Spring容器会在运行时识别并且知道它是用来干嘛的。

备注:后面我们会利用自定义注解实现自定义过期时间的缓存方案。

到此这篇关于Spring基于注解的缓存声明深入探究的文章就介绍到这了,更多相关Spring注解的缓存内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

相关文章