JAVA && Spring && SpringBoot2.x — 學習目錄
SpringBoot2.x—SpringCache(1)集成
SpringBoot2.x—SpringCache(2)使用
SpringBoot2.x—SpringCache(3) CacheManager源碼
聲明式與編程式
說起SpringCache您可能不清楚。但您絕對清楚事務(wù)氓侧。
一般使用事務(wù)分為編程式和聲明式脊另。
編程式:事務(wù)操作與業(yè)務(wù)代碼耦合,一般我們不會使用這種方式约巷;
聲明式:AOP的運用偎痛,通過注解使得事務(wù)代碼與業(yè)務(wù)代碼解耦,目前項目中一般都是使用事務(wù)注解独郎。
而我們平時使用緩存踩麦,正是編程式,即對緩存的操作與業(yè)務(wù)代碼耦合氓癌。那么是否存在一種類似于事務(wù)的技術(shù)靖榕,完成聲明式的緩存操作呢?
而SpringCahe便可以提供透明化的緩存操作顽铸,即用戶可以使用注解的方式茁计。靈活的操縱緩存。
1. 引入依賴
本篇是SpringCache+Redis的整合谓松。SpringCache只是緩存抽象星压,即具體緩存的操作需要子類實現(xiàn)。
而spring-boot-starter-data-redis
中實現(xiàn)了SpringCache的抽象接口鬼譬,即我們整合SpringCache+Redis無需自己實現(xiàn)具體緩存娜膘。
<!--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緩存可以看這篇文章,因為有個項目在生產(chǎn)環(huán)境中优质,使用lettuce客戶端每隔一段時間連接斷開(初步估計是Redis機房和應(yīng)用服務(wù)器機房網(wǎng)絡(luò)問題)竣贪。切換成了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(":");
//調(diào)用SimpleKey的邏輯
Object key = SimpleKeyGenerator.generateKey(params);
return sb.append(key);
};
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
//設(shè)置特有的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的事務(wù)支持
cacheDefaults(customRedisCacheConfiguration(Duration.ofSeconds(60))).
withInitialCacheConfigurations(cacheConfigurations). //設(shè)置個性化的Cache配置
build();
}
/**
* 設(shè)置RedisConfiguration配置
*
* @param ttl
* @return
*/
public RedisCacheConfiguration customRedisCacheConfiguration(Duration ttl) {
//設(shè)置序列化格式
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer
= new Jackson2JsonRedisSerializer<>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(RedisObjectMapper.redisConfigurationObjectMapper());
return RedisCacheConfiguration.
defaultCacheConfig().serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).
computePrefixWith(cacheName -> cacheName + ":"). //設(shè)置Cache的前綴德谅,默認::
disableCachingNullValues(). //若返回值為null,則不允許存儲到Cache中
entryTtl(ttl); //設(shè)置緩存缺省超時時間
}
}
注意不要將ObjectMapper加入到Spring容器中萨螺。因為Spring容器中存在一個ObjectMapper窄做,以用于@RequestBody
、ResponseBody
屑迂、RestTemplate
等地的序列化和反序列化。
為什么不采用Spring容器的ObjectMapper對象冯键,而要自己設(shè)置是因為Redis配置了objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
屬性惹盼,在序列化時記錄類/屬性的類型,以便在反序列化時得到POJO對象惫确。此屬性詳見——Jackson的ObjectMapper.DefaultTyping.NON_FINAL屬性手报。
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類型禁止轉(zhuǎn)換為時間戳
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);
//非空數(shù)據(jù)才進行格式化
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
//針對BigDecimal改化,序列化時掩蛤,不采取科學計數(shù)法
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進行序列化和反序列化阳藻,詳細內(nèi)容請點擊...
3. key的設(shè)置
需要注意的是,SpringCache作為應(yīng)用層的聲明式緩存谈撒。其數(shù)據(jù)結(jié)構(gòu)為Key-Value
腥泥。那么設(shè)計一個安全優(yōu)雅的Key,是一個重要的任務(wù)啃匿。
- 在SpringCache官網(wǎng)中蛔外,這樣描述SpringCache默認的KeyGenerator的:
- 若沒有參數(shù)值被得到,返回SimpleKey.EMPTY(空數(shù)組)溯乒。
- 若只有一個參數(shù)值被得到夹厌,返回該參數(shù)值的實例。
- 若多個參數(shù)值被得到裆悄,返回一個包含所有參數(shù)值SimpleKey對象尊流。
- 默認的KeyGenerator如何獲取參數(shù)值?
- 若注解上只是指定
cacheName
屬性灯帮,SimpleKeyGenerator將獲取所有的參數(shù)值崖技。組成SimpleKey對象逻住。 - 指定
cacheName
和key
屬性,并且key
的屬性支持SpEL
表達式:
- 基本形式
@Cacheable(value="cacheName", key="#id")
public ResultDTO method(int id);
- 組合形式
@Cacheable(value="cacheName", key="T(String).valueOf(#name).concat('-').concat(#password))
public ResultDTO method(int name, String password);
- 對象形式
@Cacheable(value="cacheName", key="#user.id)
public ResultDTO method(User user);
- 默認的SimpleKeyGenerator的缺陷
SimpleGenerator只會將參數(shù)值封裝為SimpleKey對象迎献。然后作為Key瞎访,可能會導致不同方法Key沖突。
我們雖然可以使用SpEL
表達式獲取類名吁恍、方法名扒秸,在進行拼接。但是需要為每一個注解指定冀瓦,太過于繁雜伴奥。
- 自定義KeyGenerator
注解上keyGenerator
屬性與key
屬性是不共存的,即我們?nèi)敉ㄟ^keyGenerator
來自定義我們的Key生成器翼闽,那么就需要將所有的參數(shù)值均進行處理拾徙,而不能指定特定的參數(shù)值
處理。
@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(":");
//調(diào)用SimpleKey的邏輯
Object key = SimpleKeyGenerator.generateKey(params);
return sb.append(key);
};
}
使用:
@Cacheable(value = "book2",keyGenerator = "keyGenerator")
public Account getAccInfo(String customerId, String accType) {
//業(yè)務(wù)邏輯
}
4. 使用
springCache和事務(wù)類型感局,均采用AOP原理尼啡。故它們的注意事項也是相同。
- 若一個service中询微,注解方法被調(diào)用崖瞭,則注解不會生效;
- 只有訪問修飾符為public的方法撑毛,注解才會生效书聚;
SpringCache的使用請參考——SpringBoot2.x—SpringCache(2)使用