SpringCache(1)集成
声明式与编程式
说起SpringCache您可能不清楚。但您绝对清楚事务。一般使用事务分为编程式和声明式。
- 编程式:事务操作与业务代码耦合,一般我们不会使用这种方式;
- 声明式:AOP的运用,通过注解使得事务代码与业务代码解耦,目前项目中一般都是使用事务注解。
而我们平时使用缓存,正是编程式,即对缓存的操作与业务代码耦合。那么是否存在一种类似于事务的技术,完成声明式的缓存操作呢?
而SpringCahe便可以提供透明化的缓存操作,即用户可以使用注解的方式。灵活的操纵缓存。
1. 引入依赖
本篇是SpringCache+Redis的整合。SpringCache只是缓存抽象,即具体缓存的操作需要子类实现。
而spring-boot-starter-data-redis
中实现了SpringCache的抽象接口,即我们整合SpringCache+Redis无需自己实现具体缓存。
<!--SpringCache的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--整合Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
SpringBoot2.X整合Redis缓存可以看这篇文章,因为有个项目在生产环境中,使用lettuce客户端每隔一段时间连接断开(初步估计是Redis机房和应用服务器机房网络问题)。切换成了jedis客户端。
2. SpringCache配置
两种配置,一种可以在yml中配置,一种是在代码中配置,此处推荐在@Configuration中进行配置。
原因一是更加灵活,在配置CacheManager的Bean时,可以初始化Cache对象,在项目启动的时候注册到CacheManager中。
import com.galax.Config.serialize.RedisObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableCaching //开启缓存,可以放在启动类上。
public class RedisSpringCache {
/**
* 自定义KeyGenerator。
* @return
*/
@Bean
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
//获取代理对象的最终目标对象
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);
StringBuilder sb = new StringBuilder();
sb.append(targetClass.getSimpleName()).append(":");
sb.append(method.getName()).append(":");
//调用SimpleKey的逻辑
Object key = SimpleKeyGenerator.generateKey(params);
return sb.append(key);
};
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
//设置特有的Redis配置
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
//定制化的Cache为300s
cacheConfigurations.put("as",customRedisCacheConfiguration(Duration.ofSeconds(300)));
cacheConfigurations.put("books",customRedisCacheConfiguration(Duration.ofSeconds(300)));
cacheConfigurations.put("cs",customRedisCacheConfiguration(Duration.ofSeconds(300)));
//默认超时时间60s
return RedisCacheManager.builder(connectionFactory).
transactionAware(). //Cache的事务支持
cacheDefaults(customRedisCacheConfiguration(Duration.ofSeconds(60))).
withInitialCacheConfigurations(cacheConfigurations). //设置个性化的Cache配置
build();
}
/**
* 设置RedisConfiguration配置
*
* @param ttl
* @return
*/
public RedisCacheConfiguration customRedisCacheConfiguration(Duration ttl) {
//设置序列化格式
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer
= new Jackson2JsonRedisSerializer<>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(RedisObjectMapper.redisConfigurationObjectMapper());
return RedisCacheConfiguration.
defaultCacheConfig().serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).
computePrefixWith(cacheName -> cacheName + ":"). //设置Cache的前缀,默认::
disableCachingNullValues(). //若返回值为null,则不允许存储到Cache中
entryTtl(ttl); //设置缓存缺省超时时间
}
}
注意不要将ObjectMapper加入到Spring容器中。因为Spring容器中存在一个ObjectMapper,以用于@RequestBody
、ResponseBody
、RestTemplate
等地的序列化和反序列化。
为什么不采用Spring容器的ObjectMapper对象,而要自己设置是因为Redis配置了objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
属性,在序列化时记录类/属性的类型,以便在反序列化时得到POJO对象。
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class RedisObjectMapper {
public static ObjectMapper redisConfigurationObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
//JDK1.8新版时间格式化Model
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
objectMapper.registerModule(javaTimeModule);
//Date类型禁止转换为时间戳
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
//序列化时格式化时间戳
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
//字段名字开启驼峰命名法
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
//序列化无public的属性或方法时,不会抛出异常
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
//序列化时保存对象类型,以便反序列化时直接得到具体POJO
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//非空数据才进行格式化
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
//针对BigDecimal,序列化时,不采取科学计数法
objectMapper.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
//反序列化时,POJO中不含有JSON串的属性,不解析该字段,并且不会抛出异常
objectMapper.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
//反序列化{}时,不抛出异常,而是得到null值
objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
return objectMapper;
}
}
此处可以使用protostuff替换Jackson进行序列化和反序列化,详细内容请点击...
3. key的设置
需要注意的是,SpringCache作为应用层的声明式缓存。其数据结构为Key-Value
。那么设计一个安全优雅的Key,是一个重要的任务。
- 在SpringCache官网中,这样描述SpringCache默认的KeyGenerator的:
- 若没有参数值被得到,返回SimpleKey.EMPTY(空数组)。
- 若只有一个参数值被得到,返回该参数值的实例。
- 若多个参数值被得到,返回一个包含所有参数值SimpleKey对象。
- 默认的KeyGenerator如何获取参数值?
- 若注解上只是指定
cacheName
属性,SimpleKeyGenerator将获取所有的参数值。组成SimpleKey对象。 - 指定
cacheName
和key
属性,并且key
的属性支持SpEL
表达式:
1.基本形式
@Cacheable(value="cacheName", key="#id")
public ResultDTO method(int id);
2.组合形式
@Cacheable(value="cacheName", key="T(String).valueOf(#name).concat('-').concat(#password))
public ResultDTO method(int name, String password);
3.对象形式
@Cacheable(value="cacheName", key="#user.id)
public ResultDTO method(User user);
- 默认的SimpleKeyGenerator的缺陷
SimpleGenerator只会将参数值封装为SimpleKey对象。然后作为Key,可能会导致不同方法Key冲突。
我们虽然可以使用SpEL表达式获取类名、方法名,在进行拼接。但是需要为每一个注解指定,太过于繁杂。
- 自定义KeyGenerator
注解上keyGenerator
属性与key
属性是不共存的,即我们若通过keyGenerator
来自定义我们的Key生成器,那么就需要将所有的参数值均进行处理,而不能指定特定的参数值
处理。
@Bean
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
//获取代理对象的最终目标对象
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);
StringBuilder sb = new StringBuilder();
sb.append(targetClass.getSimpleName()).append(":");
sb.append(method.getName()).append(":");
//调用SimpleKey的逻辑
Object key = SimpleKeyGenerator.generateKey(params);
return sb.append(key);
};
}
使用:
@Cacheable(value = "book2",keyGenerator = "keyGenerator")
public Account getAccInfo(String customerId, String accType) {
//业务逻辑
}
4. 使用
springCache和事务类型,均采用AOP原理。故它们的注意事项也是相同。
1.若一个service中,注解方法被调用,则注解不会生效;
2.只有访问修饰符为public的方法,注解才会生效;
SpringCache(2)使用
在Spring3.1版本后,Spring框架提供了对缓存透明化应用的支持。缓存抽象允许使用各种缓存解决方案,而对代码的影响最小。
从Spring4.1开始,通过支持JSR-107注释和更多自定义选项,来改善缓存抽象。
1. 基于声明式注释的缓存
SpringCache是Service层的声明式缓存。即无需与业务代码耦合,通过注解完成缓存。
2.1 @Cacheable注解
@Cacheable的注解的处理流程如下图:
可以使用@Cacheable
用来划分可缓存的方法,即将结果存储在缓存中的方法,以便在后续调用(使用相同的参数)时,返回缓存中的值而无需实际执行该方法。
- 注释声明以最简单的形式:注解属性为CacheName。
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
- 支持多个CacheName。
@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
2.2 @Cacheable注解属性
调用者在调用方法时,会通过注解属性自动的去缓存中进行查询。那么我们需要指定cacheManager(CacheResolver)
、cacheName
、key(keyGenerator)
,来确定去那个缓存管理器(Redis,ConcurrentHashMap等)进行查询。而cacheName以及key会组装成对应的键。
2.2.1 CacheManager和CacheResolver
- @CacheManager:对于使用多个缓存管理器的应用程序,可以设置cacheMananger用于选择哪种缓存管理器(redis,EhCache...),非必需,当有多个才需要指定。
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager")
public Book findBook(ISBN isbn) {...}
- @CacheResolver:也可指定使用哪个缓存管理器。需要通过实现
org.springframework.cache.interceptor.CacheResolver
接口来解析
@Cacheable(cacheResolver="runtimeCacheResolver")
public Book findBook(ISBN isbn) {...}
cacheManager和cacheResolver参数是互斥的,同时指定这两个参数会导致异常。因为实现CacheManager会忽略自定义的CacheResolver。
2.2.2 cacheName
CacheName属性也是value属性,定义@Cacheable注解时,必须使用该属性。即指定缓存的名字。使用默认CacheManager属性,以及使用默认的key属性(SimpleKey对象包含所有的参数值)。
2.2.3 key和KeyGenerator
- keyGenrator属性
SpringCache默认使用SimpleKeyGenerator,默认情况下将参数值作为键,但是可能会导致key重复出现。
我们在整合SpringCache中自定义CacheGenerator,将类名:方法名
作为key的一部分。
而后@Cacheable注解中,指定自定义的KeyGenerator。
@Cacheable(value = "book2",keyGenerator = "keyGenerator")
public Account getAccInfo(String customerId, String accType) {
//业务逻辑
}
注意key和keyGenerator依旧是互斥的。
- key属性
当然若是使用key属性,也是可以指定类名和方法名等参数作为key。
SpringCache提供了与缓存相关的专用元数据,例如参数名称。下表描述了可用于上下文的项目,以便于key的生成和条件计算。
名称 | 位置 | 描述 | 例子 |
---|---|---|---|
methodMame | root | 被调用方法的名称 | #root.methodName |
method | root | 被调用的方法 | #root.method.name |
target | root | 被调用的目标对象 | #root.target |
targetClass | root | 被调用目标的类 | #root.targetClass |
args | root | 用于被调用目标的参数值(数组) | #root.args[0] |
caches | root | 执行当前方法缓存的集合 | #root.caches[0].name |
参数名称 | 调用的方法 | 方法的任何参数名称 | #iban或#a0 |
result | 调用的方法 | 仅用在unless,方法调用的结果(缓存值) | #result |
- cacheName无法使用SpEL表达式,#root.args是参数值。
@Cacheable(cacheNames = "#root.methodName",key = "#root.args")
public User getUser(int id) {
User user = new User().setUserName("tom").setId(id);
log.info("【调用getUser】方法");
return user;
}
图4- @Cacheable(cacheNames = "#root.methodName",key = "#root.args").png
- 两个SpEL表达式拼接,创建更具体的key值
@Cacheable(value = "book2",
key = "#root.targetClass.getSimpleName().concat(':').concat(#root.methodName).concat(':').concat(#customerId)")
public User getUser(int id) {
User user = new User().setUserName("tom").setId(id);
log.info("【调用getUser】方法");
return user;
}
图5-两个SpEL表达式拼接.png
2.3.4 同步缓存
在多线程环境下,某些操作可能会为一个参数并发调用。默认情况下,SpringCache不会锁定任何内容,并且可能多次计算相同的值,从而破坏了缓存的目的。
对于那些特殊情况,可以使用sync
属性来锁定。即只有一个线程正在忙于计算该值,而其他线程则被阻塞,直到缓存中更新该条目为止。
@Cacheable(cacheNames="foos", sync=true)
public Foo executeExpensiveOperation(String id) {...}
4. 条件缓存
- condition:方法可能不总适合缓存(例如:他可能取决于给你定的参数)。缓存注释通过condition支持这种功能,该参数采用SpEL表达式,该表达式的值等于true或false。如果为true,则缓存该方法。否则的话,每次调用该方法。例如:仅当参数name的长度小于32时才缓存以下方法:
@Cacheable(cacheNames="book", condition="#name.length() < 32")
public Book findBook(String name)
- unless(如果不):可以使用unless参数来决定是否将值添加到缓存中,该参数也采用SpEl表达式,该表达式输出结果boolean类型。与condition不同的是,unless表达式是在调用方法后求值的,并且当SpEL返回false时,加入到缓存中(unless:如果不小于1000,则存储。)。
@Cacheable(cacheNames="books", key="#isbn.rawNumber",unless ="#result.id < 1000" )
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) {
log.info("执行方法!");
Book book = Book.builder().id(1101).bookName("java").build();
return book;
}
SpringCache支持java.util.Optional
,仅在支持时才将其内容作为缓存。#result始终引用业务实体,而不引用受支持的包装器。因此可以重写为下面代码:
@Cacheable(cacheNames="books", key="#isbn.rawNumber",unless ="#result?.id >1000" )
public Optional<Book> findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) {
log.info("执行方法!");
Book book = Book.builder().id(111).bookName("java").build();
Optional<Book> optionalBook = Optional.of(book);
return optionalBook;
}
注意:result仍然指的是Book而不是Optional。
1. @CachePut注解
在不影响方法执行的情况下更新缓存,可以使用@CachePut注解。也就是说,该方法始终执行,将其结果放入缓存(根据@CachePut选项)。他支持与@Cacheable缓存相同的属性。但是它应用于缓存填充而不是方法优化。
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
图1-更新缓存.png
2.2 @CacheEvict注解
Spring Cache不仅允许缓存的填充,还允许删除缓存。此过程对于从缓存中删除陈旧或未使用的数据很有用,相对于@Cacheable,@CacheEvict是从缓存中删除数据的注解。@CacheEvict需要指定一个或多个受操作影响的缓存,允许自定义缓存和Key或者条件。
allEntries
,该参数指示是否需要在整个缓存范围内逐出而不仅仅是基于Key的逐条逐出。
代码1:逐出books缓存中的所有条目。
@CacheEvict(cacheNames="books", allEntries=true)
public void loadBooks(InputStream batch)
beforeInvocation
,该参数指定逐出缓存是在方法执行前还是方法执行后(默认方法执行后)。在默认情况下,如果方法未执行(可能已经被缓存)或者引发异常,缓存是不会被移除的。而beforeInvocation=true
逐出缓存则是在方法调用前发生。适用于移除操作和方法结果没有必要联系的情况。
代码2:方法执行前移除缓存
@CacheEvict(cacheNames = "books", key = "#isbn",beforeInvocation = true)
public void loadBooks(ISBN isbn) {
log.info("清除缓存!");
//出现异常,默认不会清除缓存
throw new RuntimeException("aa");
}
注:void方法可以与@CacheEvict一起使用,因为方法充当触发器,返回值将被忽略(因为他们不与缓存交互)。
2.3 Caching注解
指定多个相同类型的注解时(例如@CacheEvict或@CachePut)。因为Key或Key的表达式在不同的缓存间是不同的。@Caching允许嵌套多个@Cacheable,@CachePut和@CacheEvict注解来使用。
代码3:使用两个@CacheEvict注解
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)
2.4 CacheConfig注解
@CacheConfig是一个类级别的注解,他允许共享cacheNames,custom KeyGenerator,custom CacheManager和custom CacheResolver。将此注解注释在类上不会打开任何缓存操作。
注:方法级别的注解会覆盖类注解
- SpringBoot2 SpringCache SpringBootspringboot2 springcache springboot springboot2 缓存springcache springboot redis springboot2 springboot springboot2 springboot swagger3 swagger springboot2 springboot mybatis springboot2 springboot spring3 spring springboot2 springboot lettuce redis springboot2 springboot mybatis3 mybatis springboot2 springboot后台 管理系统