项目修改需求描述
项目需要使用分布式缓存机制,但是使用@Cacheable原始仅配置了内存版的,故此次需要改成redis用以支持多应用模式的。
项目中如果直接改成redis的,存在一个问题。如果内存对象同一类,比如都是String的list对象,存的key值又都是"code",会把缓存给冲掉,所以需要对redis的做后缀处理。又因为只有一个redis服务器,所以需要对缓存做项目的前缀处理。
还希望能支持历史项目,做到内存与redis共存,所以顺便就做了CacheManager处理。
代码
使用到的依赖包
<!-- 缓存、权限 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>5.1.0.M1</version>
</dependency>
代码处理
首先如果希望程序支持缓存需要注解
@EnableCaching
然后在application.yml里增加redis的配置
spring:
application:
name:app
redis:
database: 0
host: XX.XX.XX.XX
port: 6379
password:
pool:
max-total: 100
block-when-exhausted: false
timeout: 5000
增加一个redis处理类RedisConfig,此处做了redis前缀的处理
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
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.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
@Configuration
public class RedisConfig {
@Autowired
private PrefixRedisSerializer stringRedisSerializer;
@Autowired
private Environment env;
// @Bean
// //这块如果不使用redisTemplate.opsForHash(),可以不用使用
// public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// // 为了开发方便,一般使用<String, Object>
// RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// redisTemplate.setConnectionFactory(redisConnectionFactory);
//
// // Json序列化配置
// Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// ObjectMapper objectMapper = new ObjectMapper();
// objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//
// //StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//
// // 设置key的序列化方式
// redisTemplate.setKeySerializer(stringRedisSerializer);
//
// // 设置hashkey的序列化方式
// redisTemplate.setHashKeySerializer(stringRedisSerializer);
//
// // 设置value的序列化方式
template.setValueSerializer(stringRedisSerializer);
// redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
//
// // 设置hashvalue的序列化方式
template.setHashValueSerializer(stringRedisSerializer);
// redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
//
template.setBeanClassLoader(this.getClass().getClassLoader());
//
redisTemplate.afterPropertiesSet();
//
// return redisTemplate;
// }
@Bean("redisCacheManager")
//@Primary
public RedisCacheManager myRedisCacheManager(RedisConnectionFactory redisConnectionFactory){
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
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);
//String prefixStr = env.getProperty("spring.application.name") + ":";
//if(!'prod'.equals(env.getActiveProfiles()[0])) {
// prefixStr += env.getActiveProfiles()[0] + ":";
//}
// 配置序列化(解决乱码的问题)
// RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(this.getClass().getClassLoader())
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.computePrefixWith(cacheName -> {
String prefixStr = env.getProperty("spring.application.name") + ":";
if (!"prod".equals(env.getActiveProfiles()[0])) {
prefixStr += env.getActiveProfiles()[0] + ":";
}
prefixStr += cacheName + ":";
return prefixStr;
})
//.prefixKeysWith(prefixStr)
//这块如果不使用computePrefixWith,也可以把redisSerializer改成stringRedisSerializer
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build();
return cacheManager;
}
@Bean("defaultCacheManager")
@Primary
public ConcurrentMapCacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
return cacheManager;
}
}
注:RedisCacheManager 和 RedisTemplate 里的json处理要注意一致性。
这边配置了2个cacheManager:(ConcurrentMapCacheManager 默认缓存、RedisCacheManager redis缓存)
必须要默认一个cacheManager,哪个是默认的就在哪个bean上加注解@Primary
如果不写会报下列错误:
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
19:42:21.462 [restartedMain] ERROR o.s.b.SpringApplication:858 - Application run failed
java.lang.IllegalStateException: No CacheResolver specified, and no unique bean of type CacheManager found. Mark one as primary or declare a specific CacheManager to use.
at org.springframework.cache.interceptor.CacheAspectSupport.afterSingletonsInstantiated(CacheAspectSupport.java:223)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:866)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)
下面这个类是针对加redis-key前缀用的,不用可以不考虑
import java.nio.charset.StandardCharsets;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.google.common.base.Strings;
@Configuration
public class PrefixRedisSerializer extends StringRedisSerializer {
@Autowired
private Environment env;
// public PrefixRedisSerializer() {
// super(StandardCharsets.UTF_8);
// }
private String cachePrefix;
private String getPrefix() {
if(!Strings.isNullOrEmpty(cachePrefix)) {
return cachePrefix;
}
//这块是去application配置文件获取项目名称和环境配置,可以按照自己需要加
String prefixStr = env.getProperty("spring.application.name") + ":";
if (!"prod".equals(env.getActiveProfiles()[0])) {
prefixStr += env.getActiveProfiles()[0] + ":";
}
cachePrefix = prefixStr;
return cachePrefix;
}
@Override
public byte[] serialize(String string) {
return (string == null ? null : (getPrefix() + string).getBytes(StandardCharsets.UTF_8));
}
@Override
public String deserialize(byte[] bytes) {
return (bytes == null ? null : new String(bytes, StandardCharsets.UTF_8).replaceFirst(getPrefix(), ""));
}
}
增加MyKeyGenerator,做redis后缀处理
CachingConfigurerSupport 这个接口默认就有@Primary功能,无需额外配置
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
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 java.lang.reflect.Method;
@Configuration
public class MyKeyGeneratorConfig extends CachingConfigurerSupport {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Bean
// @Primary
// @Override
public KeyGenerator myKeyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
// String re= String.format("%s::%s(%s)",target.getClass().getName(),method.getName(),
// CollectionUtils.arrayToList(params));//Arrays.asList(params)
StringBuilder sb = new StringBuilder();
String valueStr = null;
if (method.isAnnotationPresent(Cacheable.class)) {
Cacheable ca = method.getAnnotation(Cacheable.class);
if (ca.value() != null) {
valueStr = ca.value()[0];
}
} else if(method.isAnnotationPresent(CacheEvict.class)) {
CacheEvict ce = method.getAnnotation(CacheEvict.class);
if (ce.value() != null) {
valueStr = ce.value()[0];
}
}
if (valueStr != null) {
sb.append(valueStr + ":");
} else {
sb.append(target.getClass().getName() + ":");
sb.append(method.getName() + ":");
}
for (Object obj : params) {
sb.append(obj.toString() + ":");
}
// return sb.toString();
String re = sb.toString();
logger.debug("缓存生成的key:{}。",re);
return re;
}
};
}
}
缓存类处理:
@Cacheable(value = "XXX", keyGenerator = "myKeyGenerator", cacheManager = "redisCacheManager")
public List<String> getXXXX(String userCode) {
XXXXXXXXXX
}
对应的@CacheEvict 记得也把该属性加上,如果不是默认的话: keyGenerator = “myKeyGenerator”
权限类处理:
针对权限类controller增加注解判断,没权限直接报错
@PreAuthorize(“hasAuthority(‘privileges’)”)
权限缓存加载类:
@Cacheable("privileges")
private List<GrantedAuthority> makeAuth(String userCode) {
List<MyGrantedAuthority> privileges = XXX;
return privileges;
}
权限类:
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;
public class MyGrantedAuthority implements GrantedAuthority {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private String authority;
public MyGrantedAuthority() {
super();
}
public MyGrantedAuthority(String authority) {
Assert.hasText(authority, "A granted authority textual representation is required");
this.authority = authority;
}
@Override
public String getAuthority() {
return authority;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof MyGrantedAuthority) {
return authority.equals(((MyGrantedAuthority) obj).authority);
}
return false;
}
@Override
public int hashCode() {
return this.authority.hashCode();
}
@Override
public String toString() {
return this.authority;
}
}
异常问题解决
如果项目中存在LocalDateTime会报错:
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (byte[])
[truncated 981 bytes]; line: 1, column: 360] (through reference chain: ["createdTime"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1452)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1028)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1297)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
at com.fasterxml.jackson.databind.deser.impl
- 文章目录
- 繁