@EnableCaching
spring boot提供了比较简单的缓存方案。只要使用 @EnableCaching
即可完成简单的缓存功能。
blog.csdn.net/micro_hz/article/details/76599632
添加依赖
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring2.0集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.3.14.RELEASE</version>
</dependency>
yml配置
spring:
data:
redis:
repositories:
enabled: false
redis:
# Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0
database: 0
host: localhost
port: 6379
# 连接密码(默认为空)
password:
# 连接超时时间(毫秒)
timeout: 10000ms
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-wait: -1
# 连接池中的最大空闲连接 默认 8
max-idle: 8
# 连接池中的最小空闲连接 默认 0
min-idle: 0
代码
自定义RedisTemplate
使用fastjson进行序列化
package njgis.opengms.portal.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> template(RedisConnectionFactory factory) {
// 创建RedisTemplate<String, Object>对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(factory);
// redis key 序列化方式使用stringSerial
template.setKeySerializer(new StringRedisSerializer());
// redis value 序列化方式自定义
// template.setValueSerializer(new GenericFastJsonRedisSerializer());
template.setValueSerializer(valueSerializer());
// redis hash key 序列化方式使用stringSerial
template.setHashKeySerializer(new StringRedisSerializer());
// redis hash value 序列化方式自定义
// template.setHashValueSerializer(new GenericFastJsonRedisSerializer());
template.setHashValueSerializer(valueSerializer());
return template;
}
private RedisSerializer<Object> valueSerializer() {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 此项必须配置,否则如果序列化的对象里边还有对象,会报如下错误:
// java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
// 旧版写法:
// objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
}
}
redis缓存注解:插入
import njgis.opengms.portal.enums.ItemTypeEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AopCacheEnable {
//redis缓存key
String key();
//redis缓存存活时间默认值(可自定义)
long expireTime() default 3600;
//redis缓存的分组
ItemTypeEnum group() default ItemTypeEnum.PortalItem;
}
redis缓存注解:删除
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AopCacheEvict {
//redis中的key值
String key();
//redis缓存的分组
String group();
}
自定义缓存切面具体实现类
CacheEnableAspect.java
import lombok.extern.slf4j.Slf4j;
import njgis.opengms.portal.enums.ItemTypeEnum;
import njgis.opengms.portal.service.RedisService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Slf4j
@Aspect
@Component
public class CacheEnableAspect {
@Autowired
RedisService redisService;
/**
* 用于SpEL表达式解析.
*/
private SpelExpressionParser parser = new SpelExpressionParser();
/**
* 用于获取方法参数定义名字.
*/
private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* Mapper层切点 使用到了我们定义的 AopCacheEnable 作为切点表达式。
*/
@Pointcut("@annotation(njgis.opengms.portal.component.AopCacheEnable)")
public void queryCache() {
}
/**
* Mapper层切点 使用到了我们定义的 AopCacheEvict 作为切点表达式。
*/
@Pointcut("@annotation(njgis.opengms.portal.component.AopCacheEvict)")
public void ClearCache() {
}
@Around("queryCache()")
public Object Interceptor(ProceedingJoinPoint pjp) throws Throwable {
// StringBuilder redisKeySb = new StringBuilder("AOP").append("::");
StringBuilder redisKeySb = new StringBuilder("AOP");
// 类
// String className = pjp.getTarget().toString().split("@")[0];
// redisKeySb.append(className).append("::");
//获取当前被切注解的方法名
Method method = getMethod(pjp);
//获取当前被切方法的注解
AopCacheEnable aopCacheEnable = method.getAnnotation(AopCacheEnable.class);
if (aopCacheEnable == null) {
return pjp.proceed();
}
//获取被切注解方法返回类型
// Type returnType = method.getAnnotatedReturnType().getType();
// String[] split = returnType.getTypeName().split("\\.");
// String type = split[split.length - 1];
// redisKeySb.append(":").append(type);
//从注解中获取key
//通过注解key使用的SpEL表达式获取到SpEL执行结果
String key = aopCacheEnable.key();
// redisKeySb.append(args);
String resV = generateKeyBySpEL(key, pjp).toString();
redisKeySb.append(":").append(aopCacheEnable.group()).append(":").append(resV);
//获取方法参数值
// Object[] arguments = pjp.getArgs();
// redisKeySb.append(":").append(arguments[0]);
String redisKey = redisKeySb.toString();
Object result = redisService.get(redisKey);
if (result != null) {
log.info("从Redis中获取数据:{}", result);
return result;
} else {
try {
result = pjp.proceed();
log.info("从数据库中获取数据:{}", result);
} catch (Throwable e) {
throw new RuntimeException(e.getMessage(), e);
}
// 获取失效时间
long expire = aopCacheEnable.expireTime();
redisService.set(redisKey, result, expire);
}
return result;
}
/*** 定义清除缓存逻辑,先操作数据库,后清除缓存*/
@Around(value = "ClearCache()")
public Object evict(ProceedingJoinPoint pjp) throws Throwable {
StringBuilder redisKeySb = new StringBuilder("AOP");
Method method = getMethod(pjp);
// 获取方法的注解
AopCacheEvict cacheEvict = method.getAnnotation(AopCacheEvict.class);
if (cacheEvict == null) {
return pjp.proceed();
}
//从注解中获取key
//通过注解key使用的SpEL表达式获取到SpEL执行结果
String key = cacheEvict.key();
// redisKeySb.append(args);
key = generateKeyBySpEL(key, pjp).toString();
//清楚缓存的group从参数拿
String group = cacheEvict.group();
ItemTypeEnum type = (ItemTypeEnum)generateKeyBySpEL(group, pjp);
redisKeySb.append(":").append(type).append(":").append(key);
//先操作db
Object result = pjp.proceed();
//根据key从缓存中删除
String redisKey = redisKeySb.toString();
redisService.delete(redisKey);
return result;
}
/**
* 获取被拦截方法对象
*/
public Method getMethod(ProceedingJoinPoint pjp) {
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
return targetMethod;
}
public Object generateKeyBySpEL(String spELString, ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] paramNames = nameDiscoverer.getParameterNames(methodSignature.getMethod());
Expression expression = parser.parseExpression(spELString);
EvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
return expression.getValue(context);
}
}
注意这里的queryCache和ClearCache,里面切点表达式
分别对应上面自定义的两个AopCacheEnable和AopCacheEvict。
然后在环绕通知的queryCache方法执行前后时
获取被切方法的参数,参数中的key,然后根据key去redis中去查询
Service层
redis服务接口
public interface RedisService {
void set(String key, Object value);
void set(String key, Object value, long expire);
Object get(String key);
void expire(String key, long expire);
void delete(String key);
// 由于dao接口同时继承了MongoRepository和GenericItemDao,
// 所以这边写接口调用他们防止继承冲突
PortalItem insertItem(PortalItem item, ItemTypeEnum type);
PortalItem saveItem(PortalItem item, ItemTypeEnum type);
void deleteItem(PortalItem item, ItemTypeEnum type);
}
AopCacheEnable测试
public interface ModelItemDao extends MongoRepository<ModelItem,String>, GenericItemDao<ModelItem> {
@AopCacheEnable(key = "#id", group = ItemTypeEnum.ModelItem)
ModelItem findFirstById(String id);
}
AopCacheEvict测试
@Service("redisService")
public class RedisServiceImpl implements RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
GenericService genericService;
@Override
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
@Override
public void set(String key, Object value, long expire) {
redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
}
@Override
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
@Override
public void expire(String key, long expire) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
@Override
public void delete(String key) {
redisTemplate.delete(key);
}
@Override
public PortalItem insertItem(PortalItem item, ItemTypeEnum type) {
GenericItemDao itemDao = (GenericItemDao)genericService.daoFactory(type).get("itemDao");
PortalItem result = (PortalItem)itemDao.insert(item);
return result;
}
@Override
@AopCacheEvict(key = "#item.id", group = "#type")
public PortalItem saveItem(PortalItem item, ItemTypeEnum type) {
GenericItemDao itemDao = (GenericItemDao)genericService.daoFactory(type).get("itemDao");
PortalItem result = (PortalItem)itemDao.save(item);
return result;
}
@Override
public void deleteItem(PortalItem item, ItemTypeEnum type) {
GenericItemDao itemDao = (GenericItemDao)genericService.daoFactory(type).get("itemDao");
itemDao.delete(item);
}
}
测试
@Test
public void aopFindTest(){
for (int i = 0; i < 5; i++) {
ModelItem modelItem = modelItemDao.findFirstById("3f6857ba-c2d2-4e27-b220-6e5367803a12");
System.out.println(modelItem);
}
}
@Test
public void aopSaveTest(){
ModelItem modelItem = modelItemDao.findFirstById("3f6857ba-c2d2-4e27-b220-6e5367803a12");
modelItem.setThumbsUpCount(50);
redisService.saveItem(modelItem,ItemTypeEnum.ModelItem);
}
遇到的问题
问题:
Could not safely identify store assignment for repository candidate interface
条件:
使用了Spring data jpa 作为持久层框架并同时使用starter引入了Elasticsearch或Redis依赖包。
原因:
RedisRepositoriesAutoConfiguration或ElasticsearchRepositoriesAutoConfiguration 里面的注解@ConditionalOnProperty会判断 spring.data.redis/elasticsearch.repositories.enabled 这个配置项是否存在。若存在会自动扫描继承org.springframework.data.repository.Repository的实体Repository接口。
解决办法:
spring:
data:
redis:
repositories:
enabled: false
elasticsearch:
repositories:
enabled: false
问题:
Redis获取缓存异常
Resolved [java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.alibaba.fastjson.JSONObject]
出现场景:
SpringBoot项目中使用Redis来进行缓存。把数据放到缓存中时没有问题,但从缓存中取出来反序列化为对象时报错:“java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to xxx”。(xxx为反序列化的目标对象对应的类。)
只有这个类里有其他对象字段才会报这个问题,如果这个类里都是初始的类型(比如:Integer,String)则不会报这个错误。
只要用到Redis序列化反序列化的地方都会遇到这个问题,比如:RedisTemplate,Redisson,@Cacheable注解等。
原因:
SpringBoot 的缓存使用 jackson 来做数据的序列化与反序列化,如果默认使用 Object 作为序列化与反序列化的类型,则其只能识别 java 基本类型,遇到复杂类型时,jackson 就会先序列化成 LinkedHashMap ,然后再尝试强转为所需类别,这样大部分情况下会强转失败。
解决方法:
出现这种异常,需要自定义ObjectMapper,设置一些参数,而不是直接使用Jackson2JsonRedisSerializer类中黙认的ObjectMapper,看源代码可以知道,Jackson2JsonRedisSerializer中的ObjectMapper是直接使用new ObjectMapper()创建的,这样ObjectMapper会将Redis中的字符串反序列化为java.util.LinkedHashMap类型,导致后续Spring对其进行转换成报错。其实我们只要它返回Object类型就可以了。
修改RedisTemplate这个bean的valueSerializer,设置默认类型。
参考博客:
blog.51cto.com/knifeedge/5010643
修改配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> template(RedisConnectionFactory factory) {
// 创建RedisTemplate<String, Object>对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(factory);
// redis key 序列化方式使用stringSerial
template.setKeySerializer(new StringRedisSerializer());
// redis value 序列化方式自定义,使用jackson会出现转换类型的错误
// template.setValueSerializer(new GenericFastJsonRedisSerializer());
template.setValueSerializer(valueSerializer());
// redis hash key 序列化方式使用stringSerial
template.setHashKeySerializer(new StringRedisSerializer());
// redis hash value 序列化方式自定义,使用jackson会出现转换类型的错误
// template.setHashValueSerializer(new GenericFastJsonRedisSerializer());
template.setHashValueSerializer(valueSerializer());
return template;
}
private RedisSerializer<Object> valueSerializer() {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 此项必须配置,否则如果序列化的对象里边还有对象,会报如下错误:
// java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
// 旧版写法:
// objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
}
}
问题:
注解不生效
注解生效代码
@UserCacheEnable(key = "#email")
public JSONObject getItemUserInfoByEmail(String email) {
User user = null;
JSONObject userJson = new JSONObject();
if (email.contains("@")){
user = userDao.findFirstByEmail(email);
} else {
user = userDao.findFirstByAccessId(email);
if (user != null){
email = user.getEmail();
} else {
userJson.put("name", email);
userJson.put("email", null);
userJson.put("accessId", null);
userJson.put("image", null);
return userJson;
}
}
JSONObject userInfo = getInfoFromUserServer(email);
userJson.put("name", userInfo.getString("name"));
// userJson.put("id", user.getId());
userJson.put("email", user.getEmail());
userJson.put("accessId", user.getAccessId());
// userJson.put("image", user.getAvatar().equals("") ? "" : htmlLoadPath + user.getAvatar());
userJson.put("image", userInfo.getString("avatar"));
return userJson;
}
注解失效代码
@UserCacheEnable(key = "#email")
public JSONObject getInfoFromUserServer(String email){
// return getInfoFromUserServerPart(email);
JSONObject jsonObject = new JSONObject();
try {
RestTemplate restTemplate = new RestTemplate();
String userInfoUrl = "远程接口";
HttpHeaders headers = new HttpHeaders();
MediaType mediaType = MediaType.parseMediaType("application/json;charset=UTF-8");
headers.setContentType(mediaType);
headers.set("user-agent", "portal_backend");
HttpEntity httpEntity = new HttpEntity(headers);
ResponseEntity<JSONObject> response = restTemplate.exchange(userInfoUrl, HttpMethod.GET, httpEntity, JSONObject.class);
JSONObject userInfo = response.getBody().getJSONObject("data");
String avatar = userInfo.getString("avatar");
if(avatar!=null){
// avatar = "/userServer" + avatar;
//修正avatar前面加了/userServer
// avatar = avatar.replaceAll("/userServer","");
genericService.formatUserAvatar(avatar);
}
userInfo.put("avatar",avatar);
userInfo.put("msg","suc");
return userInfo;
}catch(Exception e){
log.error(e.getMessage());
// System.out.println(e.fillInStackTrace());
jsonObject.put("msg","no user");
}
return jsonObject;
}
原因:
如下面几种场景
1、Controller直接调用Service B方法:Controller > Service A
在Service A 上加@Transactional的时候可以正常实现AOP功能。
2、Controller调用Service A方法,A再调用B方法:Controller > Service A > Service B
在Service B上加@Transactional的时候不能实现AOP功能,因为在Service A方法中调用Service B方法想当于使用this.B(),this代表的是Service类本身,并不是真实的代理Service对象,所以这种不能实现代理功能。
所以,如果不是直接调用的方式,是不能实现代理功能的,非常需要注意。