searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

Spring AOP 实现 Redis 缓存切面

2024-08-29 02:13:46
60
0

@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对象​,所以这种不能实现代理功能。

所以,如果不是直接调用的方式,是不能实现代理功能的,非常需要注意。

0条评论
0 / 1000
陈****彬
6文章数
1粉丝数
陈****彬
6 文章 | 1 粉丝
陈****彬
6文章数
1粉丝数
陈****彬
6 文章 | 1 粉丝
原创

Spring AOP 实现 Redis 缓存切面

2024-08-29 02:13:46
60
0

@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对象​,所以这种不能实现代理功能。

所以,如果不是直接调用的方式,是不能实现代理功能的,非常需要注意。

文章来自个人专栏
扣顶
6 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
4
3