JAVA && Spring && SpringBoot2.x — 學習目錄
Springboot拋棄了繁雜的xml配置,采用了自動裝配的原理,所以我們看上去,只是配置了yml文件,就完成了繁雜bean的創(chuàng)建。
1. 為什么要自定義RedisTemplate
我們在代碼中,可以完成RedisTemplate的注入,而實際上,我們只是單純的配置了yml文件,在哪里創(chuàng)建了redisTemplate這個bean對象呢?
redisTemplate自動裝配的源碼:
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
通過源碼可以看出,SpringBoot自動幫我們在容器中生成了一個RedisTemplate和一個StringRedisTemplate。
- RedisTemplate的泛型是<Object,Object>,寫代碼不是很方便,一般我們的key是String類型,我們需要一個<String,Object>的泛型。
- RedisTemplate沒有設置數據存儲在Redis時,Key和Value的序列化方式。(采用默認的JDK序列化方式)
如何優(yōu)雅的解決上述兩個問題呢?
@ConditionalOnMissing注解:如果Spring容器中已經定義了id為redisTemplate的Bean,那么自動裝配的RedisTemplate不會實例化。因此我們可以寫一個配置類,配置Redisemplate。
若未自定義RedisTemplate,默認會對key進行jdk序列化。
2. 如何自定義RedisTemplate
2.1 Redis數據的序列化問題
針對StringRedisSerializer,Jackson2JsonRedisSerializer和JdkSerializationRedisSerializer進行測試
| 數據結構 | 序列化類 | 序列化前 | 序列化后 |
|---|---|---|---|
| Key/Value | StringRedisSerializer | test_value | test_value |
| Key/Value | Jackson2JsonRedisSerializer | test_value | "test_value" |
| Key/Value | JdkSerializationRedisSerializer | test_value | 亂碼 |
| Hash | StringRedisSerializer | 2016-08-18 | 2016-08-18 |
| Hash | Jackson2JsonRedisSerializer | 2016-08-18 | "2016-08-18" |
| Hash | JdkSerializationRedisSerializer | 2016-08-18 | \xAC\xED\x00\x05t |
由此可以得到結論:
- StringRedisSerializer進行序列化后的值,在Java和Redis中保存的內容時一致的。
- 用Jackson2JsonRedisSerializer序列化后,在Redis中保存的內容,比Java中多一對逗號。
- 用JdkSerializationRedisSerializer序列化后,對于Key-Value結構來說,在Redis中不可讀;對于Hash的Value來說,比Java的內容多了一些字符。
自定義的redisTemplate實例:
@Configuration
public class RedisConfig {
/**
* 由于原生的redis自動裝配,在存儲key和value時,沒有設置序列化方式,故自己創(chuàng)建redisTemplate實例
* @param factory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
對于Key來說,我們采用stringRedisSerializer。而對于Value來我們采用jackson2JsonRedisSerializer的序列化方式。
Jackson進行序列化
復習盤點-Java序列化方式(1)JSON序列化(溫故知新-泛型)(jdk8-LocalDate序列化)
ObjectMapper是Jackson操作的核心,Jackson所有的json操作都是在ObjectMapper中實現的。
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
設置所有訪問權限以及所有的實際類型都可序列化和反序列化om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
Jackson的ObjectMapper.DefaultTyping.NON_FINAL屬性的作用
2.2 Jackson序列化特性
1. 對于jdk1.8新時間API的序列化
在JDK1.8中的時間類,采用了一套了新的API。但是在反序列化中,會出現異常。
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of java.time.LocalDate (no Creators, like default construct, exist):
cannot deserialize from Object value (no delegate- or property-based Creator)
在SpringBoot中的解決方案:
- 在MAVEN中加入
ckson-datatype-jsr310依賴。
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
- 配置Configuration中的ObjectMapper。
@Bean
public ObjectMapper serializingObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
return objectMapper;
}
2. 常用注解
- @JsonIgnore 此注解用于屬性上,作用是Json操作時,忽略該屬性。
- @JsonFormat 此注解用于屬性上, 作用是把Date類型轉化成為想要的格式。
- @JsonProperty 此注解用于屬性上,作用是把改屬性的名稱序列化成另一個名稱。
- @JsonSerialize 此注解用于屬性上,作用是指定屬性序列化的類型。
- @JsonDeserialize 此注解用于屬性上,作用是指定屬性反序列化的類型。
- @JsonInclude 屬性值為null的不參與序列化。
- @JsonInclude(Include.NON_NULL)屬性為null,則不參與序列化。
JsonSerialize與 JsonDeserialize使用
3. 常用的redis序列化工具類
靈活的獲取Spring Bean對象
@Service
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.context=applicationContext;
}
public static <T> T getBean(String name, Class<T> requiredType){
return context.getBean(name, requiredType);
}
}
Redis常用API
@Slf4j
public class RedisUtil {
private static final RedisTemplate<String, Object> redisTemplate = SpringContextUtil.getBean("redisTemplate", RedisTemplate.class);
/**********************************************************************************
* redis-公共操作
**********************************************************************************/
/**
* 指定緩存失效時間
*
* @param key 鍵
* @param time 時間(秒)
* @return
*/
public static boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
log.error("【redis:指定緩存失效時間-異?!?, e);
return false;
}
}
/**
* 根據key 獲取過期時間
*
* @param key 鍵 不能為null
* @return 時間(秒) 返回0代表為永久有效;如果該key已經過期,將返回"-2";
*/
public static long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判斷key是否存在
*
* @param key 鍵
* @return true 存在 false不存在
*/
public static boolean exists(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
log.error("【redis:判斷{}是否存在-異?!?, key, e);
return false;
}
}
/**********************************************************************************
* redis-String類型的操作
**********************************************************************************/
/**
* 普通緩存放入
*
* @param key 鍵
* @param value 值
* @return true成功 false失敗
*/
public static boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
log.error("【redis:普通緩存放入-異常】", e);
return false;
}
}
/**
* 普通緩存放入并設置時間
*
* @param key 鍵
* @param value 值
* @param time 時間(秒) time要大于0 如果time小于等于0 將設置無限期
* @return true成功 false 失敗
*/
public static boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
log.error("【redis:普通緩存放入并設置時間-異?!?, e);
return false;
}
}
/**
* 遞增
*
* @param key 鍵
* @param delta 要增加幾(大于0)
* @return
*/
public static long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("遞增因子必須大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 遞減
*
* @param key 鍵
* @param delta 要減少幾(小于0)
* @return
*/
public static long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("遞減因子必須大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
/**
* 刪除緩存
*
* @param key 可以傳一個值 或多個
*/
@SuppressWarnings("unchecked")
public static void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
/**
* 獲取緩存
*
* @param key redis的key
* @param clazz value的class類型
* @param <T>
* @return value的實際對象
*/
public static <T> T get(String key, Class<T> clazz) {
Object obj = key == null ? null : redisTemplate.opsForValue().get(key);
if (!obj.getClass().isAssignableFrom(clazz)) {
throw new ClassCastException("類轉化異常");
}
return (T) obj;
}
/**
* 獲取泛型
*
* @param key 鍵
* @return 值
*/
public static Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
}
4. jedis客戶端與lettuce客戶端
SpringBoot集成Redis主要是使用RedisTemplate類進行操作,但是在SpringBoot2.0以后,底層訪問的不再是Jedis而是lettuce。
jedis客戶端和lettuce客戶端的區(qū)別
jedis采用的是直連redis server,在多線程之間公用一個jedis實例,是線程不安全的,想要避免線程不安全,可以使用連接池pool,這樣每個線程單獨使用一個jedis實例,但是線程過多時,帶來的是redis server的負載較大。有點類似BIO模式。
lettuce采用netty連接redis server,實例在多個線程間共享,不存在線程不安全的情況,這樣可以減少線程數量。當然在特殊情況下,lettuce也可以使用多個實例,有點類似NIO模式。
<!--redis默認使用的Lettuce客戶端-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--使用默認的Lettuce時,若配置spring.redis.lettuce.pool則必須配置該依賴-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
需要注意的是,若是在配置中配置了pool屬性,那么必須在pom.xml文件中加入commons-pool2的依賴。

SpringBoot2集成redis,使用lettuce客戶端
如果使用lettuce客戶端每隔一段時間連接斷開怎么辦
將lettuce客戶端換成jedis客戶端
<!-- 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>
<!--使用redis連接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>