依赖
<!--cache-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
配置
关于 SpringBoot
中配置 Redis
,本文不在赘述,请看:SpringBoot2.0.X配置Redis,本文配置也是在这个上面进行改造的。
1、启用缓存,在 Application
上添加 @EnableCaching
注解。
/*
* 启用缓存功能
* */
@EnableCaching
public class BuildingApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(BuildingApplication.class);
SpringApplication.run(BuildingApplication.class, args);
}
}
2、使 RedisConfig
继承 CachingConfigurerSupport
,重写 keyGenerator
方法,并缓存配置管理器
。
RedisConfig.java
完整代码如下,你只需要关注 keyGenerator()
和 cacheManager(RedisConnectionFactory redisConnectionFactory)
方法即可。
import com.blog.www.util.JacksonUtils;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置
* <p>
* 创建人:leigq <br>
* 创建时间:2018-11-08 10:11 <br>
* <p>
* 修改人: <br>
* 修改时间: <br>
* 修改备注: <br>
* </p>
*/
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
private final ObjectMapper objectMapper = getObjectMapper();
/**
* 自定义缓存 key 的生成策略。默认的生成策略是看不懂的(乱码内容) 通过 Spring 的依赖注入特性进行自定义的配置注入并且此类是一个配置类可以更多程度的自定义配置
* <br/>
* 配置参考:https://www.cnblogs.com/taiyonghai/p/9454764.html
* <br/>
* 使用参考:
* <ul>
* <li>
* <a href='http://blog.didispace.com/springbootcache2/'>Spring Boot中的缓存支持(二)使用Redis做集中式缓存</a>
* </li>
* <li>
* <a href='http://blog.didispace.com/springbootcache1/'>Spring Boot中的缓存支持(一)注解配置与EhCache使用</a>
* </li>
* </ul>
* <p>
*
* @return the key generator
* @author leiguoqing
* @date 2020 -07-23 21:43:33
*/
@Bean(name = "redisCacheKeyGenerator")
@Primary
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
};
}
/**
* 缓存配置管理器
*
* @param redisConnectionFactory the redis connection factory
* @return the cache manager
* @author leiguoqing
* @date 2020 -07-23 21:43:28
*/
@Bean(name = "redisCacheManager")
@Primary
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 以锁写入的方式创建RedisCacheWriter对象
RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory);
// 设置 CacheManager 的 Value 序列化方式为 Jackson2JsonRedisSerialize, RedisCacheConfiguration 默认就是使用 StringRedisSerializer序列化key,
// JdkSerializationRedisSerializer 序列化 value
RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer());
// 创建默认缓存配置对象
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);
return new RedisCacheManager(writer, config);
}
/**
* redisTemplate 序列化默认使用的 JdkSerializationRedisSerializer, 存储二进制字节码,这里改为使用 jackson2JsonRedisSerializer 自定义序列化
* 想了解 SpringBoot 是如何默认使用 JdkSerializationRedisSerializer 的,看这里:<a href='https://www.cnblogs.com/HuuuWnnn/p/11864380.html'>SpringBoot项目使用RedisTemplate设置序列化方式</a>
* <br/>
* StringRedisTemplate 使用的是 StringRedisSerializer,不受影响,不用重新配置
* <br/>
* 相关文章:<br/>
* <ul>
* <li>
* <a href='https://blog.csdn.net/m0_37893932/article/details/78259288'>Spring-boot通过redisTemplate使用redis(无须手动序列化)</a>
* </li>
* <li>
* <a href='https://www.cnblogs.com/wangzhuxing/p/10198347.html'>redisTemplate和stringRedisTemplate对比、redisTemplate几种序列化方式比较</a>
* </li>
* </ul>
*
* <br>创建人: leigq
* <br>创建时间: 2018-11-08 10:12
* <br>
*
* @param redisConnectionFactory redis连接工厂
* @return RedisTemplate
*/
@Bean
@Primary
public RedisTemplate<Object, Object> getRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用 Jackson2JsonRedisSerialize 替换默认序列化
// 以下代码为将 RedisTemplate 的 Value 序列化方式由 JdkSerializationRedisSerializer更换为 Jackson2JsonRedisSerializer
// 此种序列化方式结果清晰、容易阅读、存储字节少、速度快,所以推荐更换
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer());
// 设置 key 的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer());
redisTemplate.setStringSerializer(new StringRedisSerializer());
// 是否启用事务
// redisTemplate.setEnableTransactionSupport(true);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 初始化 ObjectMapper
*
* @return the object mapper
* @author leiguoqing
* @date 2020 -07-23 21:19:39
*/
private ObjectMapper getObjectMapper() {
ObjectMapper objMapper = new ObjectMapper();
objMapper = JacksonUtils.customObjectMapper(objMapper);
// 重写一些配置
objMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 序列化时允许非常量字段均输出类型 (此项必须配置,否则会报java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX)
objMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objMapper.disable(MapperFeature.USE_ANNOTATIONS);
objMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
objMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
objMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objMapper;
}
/**
* Jackson 2 json redis serializer jackson 2 json redis serializer.
*
* @return the jackson 2 json redis serializer
* @author leiguoqing
* @date 2020 -07-25 14:31:07
*/
private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
// 使用 Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
}
}
上面用到的 JacksonUtils.java
类:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;
/**
* Jackson 工具类
* </br>
* <ul>
* <li>兼容 Java8 的时间,</li>
* <li>不会出现科学计算法,已做处理</li>
* </ul>
*
* @author leigq
* @date 2020 -07-22 13:09:05
*/
public final class JacksonUtils {
/**
* 提供一个全局可用的序列化 Bean,该对象只在本类使用,不提供给其他类用。<br/>
*/
private static final ObjectMapper OBJECT_MAPPER = customObjectMapper(new ObjectMapper());
private static final String DATE_FORMATTER = "yyyy-MM-dd";
private static final String TIME_FORMATTER = "HH:mm:ss";
private static final String DATE_TIME_FORMATTER = DATE_FORMATTER + " " + TIME_FORMATTER;
private JacksonUtils() {
}
/**
* 将对象转为 JSON 字符串
*
* @param <T> the type parameter
* @param obj 可以是不带泛型的Java集合、Map、POJO对象、数组,也可以是复杂的对象
* @return json字符串 string
* @author leigq
* @date 2020 -07-22 11:58:18
*/
public static <T> String objToJson(T obj) {
try {
return OBJECT_MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new JacksonUtilsException(e);
}
}
/**
* 将 JSON 字符串转为不带泛型的对象, 只能转换成不带泛型的Java集合、Map、POJO这类简单对象
*
* @param <T> the type parameter
* @param json json字符串
* @param type 对象的类型
* @return 对象
* @author leigq
* @date 2020 -07-22 11:58:21
*/
public static <T> T jsonToObj(String json, Class<T> type) {
try {
return OBJECT_MAPPER.readValue(json, type);
} catch (IOException e) {
throw new JacksonUtilsException(e);
}
}
/**
* 将 JSON 字符串转为带泛型的对象
* <br>
*
* @param <T> the type parameter
* @param json the json
* @param collectionClass 集合类型
* @param elementClasses 元素类型
* @return 比如 {@code Set<Permission>} 这类带泛型的对象
* @author leigq
* @date 2020 -07-22 14:15:05
*/
public static <T> T jsonToObjOfParametric(String json, Class<?> collectionClass, Class<?>... elementClasses) {
try {
JavaType javaType = OBJECT_MAPPER.getTypeFactory().constructParametricType(collectionClass, elementClasses);
return OBJECT_MAPPER.readValue(json, javaType);
} catch (IOException e) {
throw new JacksonUtilsException(e);
}
}
/**
* 自定义配置以增强 ObjectMapper 默认配置,主要体现在 Java8 时间处理、科学记数法处理
* <br/>
* 如果你还需要额外的增强配置可以这样使用:<br/>
* <per>
* final ObjectMapper objectMapper = JacksonUtils.customObjectMapper(new ObjectMapper());
* objectMapper.setDateFormat(new SimpleDateFormat("YYYY-MM-dd"));
* </per>
*
* @param objectMapper the object mapper
* @return the object mapper
* @author leigq
* @date 2020 -07-24 11:14:28
*/
public static ObjectMapper customObjectMapper(ObjectMapper objectMapper) {
/* 一些配置 配置参考:http://www.imooc.com/wenda/detail/425280*/
objectMapper
// 设置时区
.setTimeZone(TimeZone.getTimeZone("GMT+8"))
// Date 对象的格式,非 java8 时间
.setDateFormat(new SimpleDateFormat(DATE_TIME_FORMATTER))
// 默认忽略值为 null 的属性,暂时不忽略,放开注释即不会序列化为 null 的属性
// .setSerializationInclusion(JsonInclude.Include.NON_NULL)
// 禁止打印时间为时间戳
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
// 禁止使用科学记数法
.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
// 自定义Java8的时间兼容模块
JavaTimeModule javaTimeModule = new JavaTimeModule();
// 序列化配置,针对java8 时间
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMATTER)));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMATTER)));
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_FORMATTER)));
// 反序列化配置,针对java8 时间
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMATTER)));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMATTER)));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(TIME_FORMATTER)));
/*注册模块*/
objectMapper
// Java8的时间兼容模块
.registerModule(javaTimeModule)
// Jdk8Module() -> 注册jdk8模块
.registerModule(new Jdk8Module())
// new ParameterNamesModule() ->
.registerModule(new ParameterNamesModule());
return objectMapper;
}
private static class JacksonUtilsException extends RuntimeException {
public JacksonUtilsException(Throwable cause) {
super(cause);
}
}
}
测试
写个简单的根据用户id查询用户信息,使用 @Cacheable
注解来缓存,cacheNames
为缓存名称(必填),cacheManager
、keyGenerator
用我们刚才在 RedisConfig.java
中配置的。
// 确实可以缓存, @Cache* 注解使用详解:http://blog.didispace.com/springbootcache1/
@Cacheable(cacheNames = {"users"}, cacheManager = "redisCacheManager", keyGenerator = "redisCacheKeyGenerator")
public User getUser(Long id) {
return userMapper.selectByPrimaryKey(id);
}
package com.blog.www;
import com.blog.www.base.BaseApplicationTests;
import com.blog.www.domain.entity.User;
import com.blog.www.service.UserService;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
/**
* Redis 缓存测试
* <p>
* 创建人:leigq <br>
* 创建时间:2018-12-08 14:48 <br>
* <p>
* 修改人: <br>
* 修改时间: <br>
* 修改备注: <br>
* </p>
*/
public class RedisCacheTest extends BaseApplicationTests {
@Autowired
private CacheManager cacheManager;
@Autowired
private UserService userService;
/**
* 用户缓存测试
*/
@Test
public void cacheUserTest() {
// 可根据cacheManager查看具体使用哪种缓存
log.warn(cacheManager.getCache("user").getName());
User user1 = userService.getUser(1L);
User user2 = userService.getUser(1L);
User user3 = userService.getUser(1L);
log.warn("user1 is [{}]", user1);
log.warn("user2 is [{}]", user2);
log.warn("user3 is [{}]", user3);
}
}
BaseApplicationTests.java
package com.blog.www.base;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* 测试基类,其他类继承此类
* <br/>
* @author :leigq
* @date :2019/8/13 17:17
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public abstract class BaseApplicationTests {
protected Logger log = LoggerFactory.getLogger(this.getClass());
private Long time;
@Before
public void setUp() {
this.time = System.currentTimeMillis();
log.info("==> 测试开始执行 <==");
}
@After
public void tearDown() {
log.info("==> 测试执行完成,耗时:{} ms <==", System.currentTimeMillis() - this.time);
}
}
测试结果如下:
我的项目是使用 MyBatis
并且打开了SQL执行日志打印,可以看到,第一次查询打印了SQL,第2、3次的时候没打印,说明第2、3次直接走的缓存。
常用注解
注解使用详解:http://blog.didispace.com/springbootcache1/