在 Java 中,一般对调用方法进行缓存控制,比如调用 findUserById(id)
,那么应该在调用此方法之前先从缓存中查找,如果没有再掉该方法如从数据库中加载,接着添加到缓存中,下次调用时将会从缓存中获取到数据。
自 Spring 3.1 起,提供了类似于 @Transactional
注解事务的注解 Cache 支持,且提供了 Cache 抽象;使用Spring Cache 的好处:
对于 Spring Cache 抽象,主要从以下几个方面学习:
本文所使用的框架版本:
<!-- 需要缓存的对象 --> <bean id="bookService" class="com.ariclee.cache.xml.XmlBookService"/> <!-- 声明缓存 --> <cache:advice id="cacheAdvice" cache-manager="cacheManager" > <cache:caching cache="books"> <!-- 读取缓存: 应用到读取数据的方法上,即可缓存的方法,如查找方法, 先从缓存中读取,如果没有再调用方法获取数据, 然后把数据添加到缓存中 --> <cache:cacheable method="findBook" key="#id"/> <!-- 更新缓存: 应用到写数据的方法上,如新增/修改方法 调用方法时会自动把相应的数据放入缓存 --> <cache:cache-put method="saveBook" key="#book.id"/> <!-- 删除单个缓存: 应用到移除数据的方法上,如删除方法 调用方法时会从缓存中移除相应的数据 --> <cache:cache-evict method="delete" key="id"/> <!-- 清空缓存:同上,注意属性 all-entries 默认为 false --> <cache:cache-evict method="deleteAll" all-entries="true"/> </cache:caching> </cache:advice> <!-- 缓存切面 --> <aop:config> <aop:advisor advice-ref="cacheAdvice" pointcut="execution(* com.ariclee.cache.xml.XmlBookService.*(..))"/> </aop:config> <!-- 方式一:注册缓存管理(JVM 缓存) --> <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager"> <property name="caches"> <set> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/> </set> </property> </bean> <!-- 方式二:注册缓存管理(JVM 缓存) --> <bean id="cacheManager2" class="org.springframework.cache.concurrent.ConcurrentMapCacheManager"> <constructor-arg value="books"/> </bean> 复制代码
声明需要缓存的类,声明缓存并配置,并使用名为 books 的缓存,声明方法 findBook
需要缓存,并使用 id 为键。注册 cacheManager 类,并使用 JVM 内存作为实际缓存。
使用 xml 开启包扫描
<mvc:annotation-driven/> <context:component-scan base-package="com.ariclee.cache.annotation" > <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" /> </context:component-scan> <context:component-scan base-package="com.ariclee.cache.annotation" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" /> </context:component-scan> 复制代码
使用 @Cacheable
注解,作用在 findBook 方法上, value
属性值赋为 books 意为缓存的名字。
@Component public class AnnotationBookService { private static Map<String, Book> respository = new HashMap<>(); static { respository.put("1", new Book("Thinking In Java")); respository.put("2", new Book("EfficetiveJava")); } // 同 xml @Cacheable(value = "books") public Book findBook(String id) { return respository.get(id); } // 同 xml @CachePut(value = "books", key = "#book.id") public void saveBook(Book book) { respository.put(book.getId(), book); } // 同 xml @CacheEvict(value = "books", key = "id", allEntries = false) public void delete(String id) { respository.remove(id); } // 同 xml @CacheEvict(value = "books", allEntries = true) public void deleteAll() { respository.clear(); } } 复制代码
使用注解和类配置缓存管理类, ConcurrentMapCacheManager
为 spring 提供的在内存中管理缓存的简单类。
@EnableCaching @Configuration public class AnnotationConfigure implements CachingConfigurer { @Bean @Override public CacheManager cacheManager() { // 使用 JVM 内存 return new ConcurrentMapCacheManager("books"); } @Override public KeyGenerator keyGenerator() { return new SimpleKeyGenerator(); } // 省略... } 复制代码
@SpringBootApplication @EnableCaching public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 复制代码
使用 @EnableCaching
注解开启缓存, yml 配置文件如下:
spring: cache: type: simple cache-names: books 复制代码
其中 type 值为 org.springframework.boot.autoconfigure.cache.CacheType
类属性。
<!-- 注册缓存管理(Redis) --> <bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="hostName" value="127.0.0.1"/> <property name="port" value="6379"/> <property name="password" value="2940184"/> <property name="timeout" value="2000"/> </bean> <!-- 声明 RedisTemplate 类--> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="connectionFactory" /> </bean> <!-- 声明缓存管理类 --> <bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager"> <constructor-arg ref="redisTemplate" /> <constructor-arg value="books" /> </bean> 复制代码
声明 Redis 连接工厂和 RedisTemplate。声明缓存管理类,注入 redisTemplate,并设置缓存名为 books。
创建 Redis 配置类
@Configuration public class RedisConfigure { // 声明 redis 链接工厂类 @Bean public JedisConnectionFactory jedisConnectionFactory() { JedisConnectionFactory factory = new JedisConnectionFactory(); factory.setHostName("127.0.0.1"); factory.setPassword("123456"); factory.setPort(6379); factory.setTimeout(2000); factory.setUsePool(false); factory.setPoolConfig(new JedisPoolConfig()); factory.setDatabase(1); factory.setConvertPipelineAndTxResults(false); return factory; } // 声明 redis 操作模板类 @Bean public RedisTemplate redisTemplate() { RedisTemplate temp = new RedisTemplate(); temp.setConnectionFactory(this.jedisConnectionFactory()); return temp; } } 复制代码
创建 Cache 配置类
@EnableCaching @Configuration public class AnnotationCacheConfigure implements CachingConfigurer { @Autowired RedisTemplate redisTemplate; @Bean @Override public CacheManager cacheManager() { // 使用 Redis return new RedisCacheManager(redisTemplate, Collections.singletonList("books")); } @Override public KeyGenerator keyGenerator() { return new SimpleKeyGenerator(); } // 省略... } 复制代码
spring: cache: cache-names: books type: redis redis: use-key-prefix: true redis: host: 127.0.0.1 port: 6379 password: 2940184 timeout: 2000s database: 1 复制代码
xml 方式:
<!-- 只缓存参数名中包含 Java --> <cache:cacheable method="findBookByName" condition="#name.contains('Java')" key="#name"/> <!-- 不缓存结果集中带有 default --> <cache:cacheable method="findBookByName" unless="#result.name.contains('default')" key="#name"/> 复制代码
注解方式:
// 同 xml @Cacheable(value = "books", condition = "#name.contains('Java')") public Book findBookByName(String name) {} // 同 xml @Cacheable(value = "books", unless = "#result.name.contains('default')") public Book findBookByName(String name) {} 复制代码
condition
属性搭配在 Cacheable
注解时,表示 ”当 condition 为 true 读缓存“ ,作用于入参, unless
属性表示 "当 unless 为 true 不写缓存" ,作用于返回值。注意加以区别!属性 condition
和 unless
还可以作用于 CachePut
注解:
// 当 id 为 10 时,写入缓存 @CachePut(value = "books", key = "#book.id", condition = "#book.id == 10") public void saveBook(Book book) {} // 当 id 为 10 时,不写入缓存 @CachePut(value = "books", key = "#book.id", unless = "#book.id == 10") public void saveBook(Book book) {} 复制代码
比如新增书籍成功后,我们要添加 id:book;name:book;的缓存键值对。此时就需要组合多个注解标签:
xml 方式:
<cache:caching method="saveAndAddAllCacheMapping" cache="books"> <cache:cache-put key="#book.id"/> <cache:cache-put key="#book.name"/> </cache:caching> 复制代码
注解方式:
@Caching( put = { @CachePut(value = "books", key = "#book.id"), @CachePut(value = "books", key = "#book.name"), } ) public Book saveBook(Book book) {} 复制代码
再如根据书籍名字查找时,在第一次放入缓存的时候,将 id:book 的缓存映射也加进去,则可以通过如此组合注解实现:
@Caching( cacheable = { @Cacheable(value = "books", key = "#name") }, put = { @CachePut(value = "books", key = "#result.id", @CachePut(value = "books", key = "#result.name") } ) public Book findBookByName(String name) {} 复制代码
SpEL 官方文档
名字 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root对象 | 当前被调用的方法名 | #root.methodName |
method | root对象 | 当前被调用的方法 | #root.method.name |
target | root对象 | 当前被调用的目标对象 | #root.target |
targetClass | root对象 | 当前被调用的目标对象类 | #root.targetClass |
args | root对象 | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root对象 | 当前方法调用使用的缓存列表(如 @Cacheable(value={"cache1", "cache2"})),则有两个 cache | #root.caches[0].name |
argument name | 执行上下文 | 当前被调用的方法的参数,如 findById(Long id) ,我们可以通过 #id 拿到参数 | #user.id |
result | 执行上下文 | 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,'cache evict'的beforeInvocation=false) | #result |
Directly through your cache provider. The cache abstraction is… well, an abstraction not a cache implementation.
Spring Cache 是抽象框架没有提供过期控制的功能,过期实现交给缓存功能提供者。如 org.springframework.data.redis.cache.RedisCacheManager
类提供 Map<String, Long> expires
属性可以设置指定名字的缓存过期时间。
需要实现 org.springframework.cache.interceptor.KeyGenerator
接口,复写 generate
方法。如:
public class CustomKeyGenerator implements KeyGenerator { @Override public Object generate(Object target, Method method, Object... params) { String key = this.generateKey(params); System.out.println("生成缓存键:" + key); return key; } private String generateKey(Object[] params) { if (params == null || params.length < 1) { return ""; } StringJoiner stringJoiner = new StringJoiner(":"); for (Object obj : params) { if (obj instanceof Book) { Book temp = (Book) obj; stringJoiner.add(temp.getId() + ":" + temp.getName()); } else { stringJoiner.add(obj.toString()); } } return stringJoiner.toString(); } } 复制代码
使用组合注解
@Caching( put = { @CachePut(value = "books", key = "#book.id"), @CachePut(value = "books", key = "#book.name"), } ) public Book saveBook(Book book) {} 复制代码
声明一个组合注解
@Caching( put = { @CachePut(value = "books", key = "#book.id"), @CachePut(value = "books", key = "#book.name"), } ) @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface BookCaching { } 复制代码
优化后使用
@BookCaching public Book saveBook(Book book) {} 复制代码
参见 org.springframework.cache.concurrent.ConcurrentMapCacheManager
。