SpringBoot-WebFlux-Redis缓存注解

摘要

前言

最近在使用WebFlux时发现,SpringBoot提供的@Cacheable,@CachePut,@CacheEvict和@Caching注解不支持响应式方法,SpringBoot官方也没有提供响应式方法的缓存注解,看到网上的一些解决方案都是直接在方法代码中加入缓存数据的代码逻辑,这样虽然可以解决问题,但是代码侵入并不优雅,于是萌生自己写一个基于redis的响应式方法缓存注解的想法,本项目参考SpringBoot提供的@Cacheable,@CachePut,@CacheEvict和@Caching注解声明,但是只是实现了一些基本功能,可以满足绝大部分使用场景的要求,因为SpringBoot早晚会给出官方解决方案,在此之前,不妨一试。

使用示例

  • 本项目已经发布到maven中央仓库,直接在项目中添加依赖即可。
  • 本项目虽然基于springboot:2.4.0构建,但实际上springboot2.0+都可以使用。
  • maven依赖
<dependency>
  <groupId>com.hanqunfeng</groupId>
  <artifactId>reactive-redis-cache-annotation-spring-boot-starter</artifactId>
  <version>1.1.0</version>
</dependency>
  • gradle依赖
implementation 'com.hanqunfeng:reactive-redis-cache-annotation-spring-boot-starter:1.1.0'
  • 此时项目中可能还要添加其它依赖,以gradle举例
//webflux,非必须,主要是面向响应式编程的,所以使用springboot大概率会使用webflux
implementation 'org.springframework.boot:spring-boot-starter-webflux'

//Spring Boot Redis 依赖,或者spring-boot-starter-data-redis-reactive,任选其一即可,注意要在配置文件中加入redis的配置
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

//redis lettuce连接池依赖,也可以使用jedis连接池,非必须,正式环境建议开启连接池
implementation 'org.apache.commons:commons-pool2'

//aop 面向方面编程 支持@Aspect,非必须
implementation 'org.springframework.boot:spring-boot-starter-aop'
  • 方法返回值必须是Mono或者Flux类型,使用方式与springboot提供的Cacheable等注解类似
    /**
    * 缓存 cacheName和key支持EL表达式,实际key的名称是"cacheName_key"
    * 缓存结果
    * key:sysuser_find_lisi
    * value:
    * [
    * "com.example.model.SysUser"
    * {
    *    id:"5c74a4e4-c4f2-4570-8735-761d7a570d36"
    *    username:"lisi"
    *    password:"$2a$10$PXoGXLwg05.5YO.QtZa46ONypBmiK59yfefvO1OGO8kYFwzOB.Os6"
    *    enable:true
    * }
    * ]
    */
    @ReactiveRedisCacheable(cacheName = "sysuser", key = "'find_' + #username")
    public Mono<SysUser> findUserByUsername(String username) {
        return sysUserRepository.findByUsername(username);
    }

    @ReactiveRedisCacheable(cacheName = "sysuser", key = "all")
    public Flux<SysUser> findAll() {
        return sysUserRepository.findAll();
    }

    /**
    * 删除缓存,allEntries = true 表示删除全部以"cacheName_"开头的缓存
    * allEntries 默认false,此时需要指定key的值,表示删除指定的"cacheName_key"
    */
    @ReactiveRedisCacheEvict(cacheName = "sysuser", allEntries = true)
    public Mono<SysUser> add(SysUser sysUser) {
        return sysUserRepository.addSysUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), sysUser.getEnable()).flatMap(data -> sysUserRepository.findById(sysUser.getId()));
    }

    /**
    * 组合注解,用法与@Caching类似
    * 规则:
    * 1.cacheables不能与cacheEvicts或者cachePuts同时存在,因为后者一定会执行方法主体,达不到调用缓存的目的,所以当cacheables存在时,后者即便指定也不执行
    * 2.先执行cacheEvicts,再执行cachePuts
    */
    @ReactiveRedisCaching(
            evict = {@ReactiveRedisCacheEvict(cacheName = "sysuser", key = "all")},
            put = {@ReactiveRedisCachePut(cacheName = "sysuser", key = "'find_' + #sysUser.username")}
    )
    public Mono<SysUser> update(SysUser sysUser) {
        Mono<SysUser> save = sysUserRepository.save(sysUser);
        return save;
    }

    /**
    * 删除指定的"cacheName_key"
    */
    @ReactiveRedisCacheEvict(cacheName = "sysuser", key="'find_' + #username")
    public Mono<Boolean> deleteByUserName(String username) {
        return sysUserRepository.deleteByUsername(username);
    }

RedisTemplate

  • 如果使用时没有创建RedisTemplate,本项目中提供了一个默认的RedisTemplate,基于jackson序列化,支持jdk8的LocalDate和LocalDateTime
    @Bean
    @ConditionalOnMissingBean(value = RedisTemplate.class)
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

        log.debug("ReactiveRedisConfig RedisTemplate");
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);

        //LocalDateTime系列序列化和反序列化模块,继承自jsr310,我们在这里修改了日期格式
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        //序列化
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(
                DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addSerializer(LocalDate.class,
                new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        javaTimeModule.addSerializer(LocalTime.class,
                new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
        //反序列化
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(
                DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addDeserializer(LocalDate.class,
                new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        javaTimeModule.addDeserializer(LocalTime.class,
                new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册模块
        objectMapper.registerModule(javaTimeModule);

        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        serializer.setObjectMapper(objectMapper);

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(serializer);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

以下为源码说明

源码依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

自定义Redis缓存相关注解

  • 只支持方法返回类型为Mono或者Flux
  • 其它返回类型时请使用springboot提供的Cacheable,CachePut,CacheEvict和Caching
  • 使用方式与springboot提供的Cacheable,CachePut,CacheEvict和Caching类似,具体看本文上面的示例

ReactiveRedisCacheable

package com.hanqunfeng.reactive.redis.cache.aop;

import java.lang.annotation.*;

/**
 * <h1>redis方法缓存注解</h1>
 * Created by hanqf on 2020/11/21 18:28.
 */

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReactiveRedisCacheable {
    /**
     * 缓存key,key为cacheName+"_"+key
     * 支持EL表达式
    */
    String key();

    /**
     * 缓存key分组,会做为缓存key的前缀+"_"
     * 支持EL表达式
    */
    String cacheName();

    /**
     * 缓存过期时间,单位秒,默认24小时
    */
    long timeout() default 24 * 3600L;
}

ReactiveRedisCacheEvict

package com.hanqunfeng.reactive.redis.cache.aop;

import java.lang.annotation.*;

/**
 * <h1>redis清除缓存注解</h1>
 * Created by hanqf on 2020/11/21 22:26.
 */

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReactiveRedisCacheEvict {
    /**
     * 缓存key,如果cacheName不为空,则key为cacheName+"_"+key
     * 支持EL表达式
     */
    String key() default "";

    /**
     * 缓存key分组,会做为缓存key的前缀+"_"
     * 支持EL表达式
     */
    String cacheName();

    /**
     * 是否删除cacheName下全部缓存数据,
     * true时cacheName不能为空,此时即便指定了key值,也会删除cacheName下全部缓存
     * false时key值不能为空
     */
    boolean allEntries() default false;

    /**
     * 调用清除缓存的时机,true:执行方法前,false:执行方法后
     * 如果是false,则方法执行过程中发生异常,则不会清除缓存
    */
    boolean beforeInvocation() default false;
}

ReactiveRedisCachePut

package com.hanqunfeng.reactive.redis.cache.aop;

import java.lang.annotation.*;

/**
 * <h1>执行完方法更新缓存</h1>
 * Created by hanqf on 2020/11/21 23:15.
 */

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReactiveRedisCachePut {

    /**
     * 缓存key,key为cacheName+"_"+key
     * 支持EL表达式
     */
    String key();

    /**
     * 缓存key分组,会做为缓存key的前缀+"_"
     * 支持EL表达式
     */
    String cacheName();

    /**
     * 缓存过期时间,单位秒,默认24小时
     */
    long timeout() default 24 * 3600L;
}

ReactiveRedisCaching

package com.hanqunfeng.reactive.redis.cache.aop;

import java.lang.annotation.*;

/**
 * <h1>组合</h1>
 * Created by hanqf on 2020/11/21 23:24.
 */

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReactiveRedisCaching {

    ReactiveRedisCacheable[] cacheable() default {};

    ReactiveRedisCachePut[] put() default {};

    ReactiveRedisCacheEvict[] evict() default {};
}

AOP--ReactiveRedisCacheAspect

  • 支持方法返回类型为Mono或者Flux
package com.hanqunfeng.reactive.redis.cache.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
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.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * <h1>redis缓存aop</h1>
 * Created by hanqf on 2020/11/21 16:16.
 */

@Component
//标识是一个Aspect代理类
@Aspect
//如果有多个切面拦截相同的切点,可以用@Order指定执行顺序
//@Order(1)
@Slf4j
public class ReactiveRedisCacheAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    @Pointcut("@annotation(com.hanqunfeng.reactive.redis.cache.aop.ReactiveRedisCacheable)")
    public void cacheablePointCut() {
    }

    @Pointcut("@annotation(com.hanqunfeng.reactive.redis.cache.aop.ReactiveRedisCacheEvict)")
    public void cacheEvictPointCut() {
    }

    @Pointcut("@annotation(com.hanqunfeng.reactive.redis.cache.aop.ReactiveRedisCachePut)")
    public void cachePutPointCut() {
    }

    @Pointcut("@annotation(com.hanqunfeng.reactive.redis.cache.aop.ReactiveRedisCaching)")
    public void cachingPointCut() {
    }

    //环绕通知,一般不建议使用,可以通过@Before和@AfterReturning实现
    //但是响应式方法只能通过环绕通知实现aop,因为其它通知会导致不再同一个线程执行
    @Around("cacheablePointCut()")
    public Object cacheableAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.debug("ReactiveRedisCacheAspect cacheableAround....");

        MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String returnTypeName = method.getReturnType().getSimpleName();


        ReactiveRedisCacheable annotation = method.getAnnotation(ReactiveRedisCacheable.class);
        String cacheName = annotation.cacheName();
        String key = annotation.key();
        long timeout = annotation.timeout();

        //转换EL表达式
        cacheName = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, cacheName);
        key = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, key);

        String redis_key = cacheName + "_" + key;

        boolean hasKey = redisTemplate.hasKey(redis_key);
        if (hasKey) {
            Object o = redisTemplate.opsForValue().get(redis_key);
            if (returnTypeName.equals("Flux")) {
                return Flux.fromIterable((List) o);
            } else if (returnTypeName.equals("Mono")) {
                return Mono.just(o);
            } else {
                return o;
            }
        } else {
            //实际执行的方法
            Object proceed = proceedingJoinPoint.proceed();
            if (returnTypeName.equals("Flux")) {
                return ((Flux) proceed).collectList().doOnNext(list -> redisTemplate.opsForValue().set(redis_key, list, timeout, TimeUnit.SECONDS)).flatMapMany(list -> Flux.fromIterable((List) list));
            } else if (returnTypeName.equals("Mono")) {
                return ((Mono) proceed).doOnNext(obj -> redisTemplate.opsForValue().set(redis_key, obj, timeout, TimeUnit.SECONDS));
            } else {
                return proceed;
            }
        }

    }


    @Around("cacheEvictPointCut()")
    public Object cacheEvictAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.debug("ReactiveRedisCacheAspect cacheEvictAround....");

        MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String returnTypeName = method.getReturnType().getSimpleName();

        ReactiveRedisCacheEvict annotation = method.getAnnotation(ReactiveRedisCacheEvict.class);
        String cacheName = annotation.cacheName();
        String key = annotation.key();
        boolean allEntries = annotation.allEntries();
        boolean beforeInvocation = annotation.beforeInvocation();

        //转换EL表达式
        cacheName = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, cacheName);
        key = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, key);


        //执行方法前清除缓存
        if (beforeInvocation) {

            //清除全部缓存
            deleteRedisCache(cacheName, key, allEntries);

            //实际执行的方法
            Object proceed = proceedingJoinPoint.proceed();
            return proceed;
        } else {//成功执行方法后清除缓存

            //实际执行的方法
            Object proceed = proceedingJoinPoint.proceed();

            final String cacheNameTemp = cacheName;
            final String keyTemp = key;

            if (returnTypeName.equals("Flux")) {
                return ((Flux) proceed).collectList().doOnNext(list -> {
                    //清除全部缓存
                    deleteRedisCache(cacheNameTemp, keyTemp, allEntries);
                }).flatMapMany(list -> Flux.fromIterable((List) list));
            } else if (returnTypeName.equals("Mono")) {
                return ((Mono) proceed).doOnNext(obj -> {
                    //清除全部缓存
                    deleteRedisCache(cacheNameTemp, keyTemp, allEntries);
                });
            } else {
                return proceed;
            }

        }
    }


    @Around("cachePutPointCut()")
    public Object cachePutAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.debug("ReactiveRedisCacheAspect cachePutAround....");

        MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String returnTypeName = method.getReturnType().getSimpleName();

        ReactiveRedisCachePut annotation = method.getAnnotation(ReactiveRedisCachePut.class);
        String cacheName = annotation.cacheName();
        String key = annotation.key();
        long timeout = annotation.timeout();

        //转换EL表达式
        cacheName = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, cacheName);
        key = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, key);

        String redis_key = cacheName + "_" + key;

        boolean hasKey = redisTemplate.hasKey(redis_key);
        if (hasKey) {
            redisTemplate.delete(redis_key);
        }

        //实际执行的方法
        Object proceed = proceedingJoinPoint.proceed();
        if (returnTypeName.equals("Flux")) {
            return ((Flux) proceed).collectList().doOnNext(list -> redisTemplate.opsForValue().set(redis_key, list, timeout, TimeUnit.SECONDS)).flatMapMany(list -> Flux.fromIterable((List) list));
        } else if (returnTypeName.equals("Mono")) {
            return ((Mono) proceed).doOnNext(obj -> redisTemplate.opsForValue().set(redis_key, obj, timeout, TimeUnit.SECONDS));
        } else {
            return proceed;
        }
    }


    @Around("cachingPointCut()")
    public Object cachingAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.debug("ReactiveRedisCacheAspect cachingAround....");

        MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String returnTypeName = method.getReturnType().getSimpleName();

        ReactiveRedisCaching annotation = method.getAnnotation(ReactiveRedisCaching.class);

        ReactiveRedisCacheEvict[] cacheEvicts = annotation.evict();
        ReactiveRedisCachePut[] cachePuts = annotation.put();
        ReactiveRedisCacheable[] cacheables = annotation.cacheable();

        //规则:
        //1.cacheables不能与cacheEvicts或者cachePuts同时存在,因为后者一定会执行方法主体,达不到调用缓存的目的,所以当cacheables存在时,后者即便指定也不执行
        //2.先执行cacheEvicts,再执行cachePuts

        if (cacheables.length > 0) {
            Map<String, Long> key_map = new HashMap<>();
            List<String> key_list = new ArrayList<>();
            Arrays.stream(cacheables).forEach(cacheable -> {
                String cacheName = cacheable.cacheName();
                String key = cacheable.key();
                long timeout = cacheable.timeout();

                //转换EL表达式
                cacheName = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, cacheName);
                key = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, key);

                String redis_key = cacheName + "_" + key;

                key_map.put(redis_key, timeout);
                key_list.add(redis_key);
            });

            AtomicBoolean isAllKeyHas = new AtomicBoolean(true);
            key_list.forEach(key -> {
                if (!redisTemplate.hasKey(key)) {
                    isAllKeyHas.set(false);
                }
            });

            //全部key都有值,则直接返回缓存
            if (isAllKeyHas.get()) {
                Object o = redisTemplate.opsForValue().get(key_list.get(0));
                if (returnTypeName.equals("Flux")) {
                    return Flux.fromIterable((List) o);
                } else if (returnTypeName.equals("Mono")) {
                    return Mono.just(o);
                } else {
                    return o;
                }
            } else {
                //实际执行的方法
                Object proceed = proceedingJoinPoint.proceed();

                if (returnTypeName.equals("Flux")) {
                    return ((Flux) proceed).collectList()
                            .doOnNext(list -> key_map.forEach((key, val) -> redisTemplate.opsForValue().set(key, list, val, TimeUnit.SECONDS)))
                            .flatMapMany(list -> Flux.fromIterable((List) list));
                } else if (returnTypeName.equals("Mono")) {
                    return ((Mono) proceed)
                            .doOnNext(obj -> key_map.forEach((key, val) -> redisTemplate.opsForValue().set(key, obj, val, TimeUnit.SECONDS)));
                } else {
                    return proceed;
                }
            }

        } else {


            Map<String, Boolean> map = new HashMap<>();
            if (cacheEvicts.length > 0) {
                Arrays.stream(cacheEvicts).forEach(cacheEvict -> {
                    String cacheName = cacheEvict.cacheName();
                    String key = cacheEvict.key();
                    boolean allEntries = cacheEvict.allEntries();
                    boolean beforeInvocation = cacheEvict.beforeInvocation();

                    //转换EL表达式
                    cacheName = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, cacheName);
                    key = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, key);

                    if (beforeInvocation) { //执行方法前清除缓存
                        //清除全部缓存
                        deleteRedisCache(cacheName, key, allEntries);
                    } else { //成功执行方法后清除缓存,先保存到map中
                        //清除全部缓存
                        if (allEntries) {
                            map.put(cacheName, true);
                        } else {
                            map.put(cacheName + "_" + key, false);
                        }
                    }
                });
            }

            //实际执行的方法
            Object proceed = proceedingJoinPoint.proceed();


            if (cachePuts.length > 0) {
                Map<String, Long> key_map = new HashMap<>();
                Arrays.stream(cachePuts).forEach(cachePut -> {
                    String cacheName = cachePut.cacheName();
                    String key = cachePut.key();
                    long timeout = cachePut.timeout();

                    //转换EL表达式
                    cacheName = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, cacheName);
                    key = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, key);

                    String redis_key = cacheName + "_" + key;

                    key_map.put(redis_key, timeout);

                    boolean hasKey = redisTemplate.hasKey(redis_key);
                    if (hasKey) {
                        redisTemplate.delete(redis_key);
                    }

                });

                if (returnTypeName.equals("Flux")) {
                    return ((Flux) proceed).collectList()
                            .doOnNext(list -> {
                                //执行方法后清除缓存
                                if (map.size() > 0) {
                                    map.forEach((key, val) -> {
                                        deleteRedisCache(key, val);
                                    });
                                }
                                key_map.forEach((key, val) -> redisTemplate.opsForValue().set(key, list, val, TimeUnit.SECONDS));
                            })
                            .flatMapMany(list -> Flux.fromIterable((List) list));
                } else if (returnTypeName.equals("Mono")) {
                    return ((Mono) proceed)
                            .doOnNext(obj -> {
                                //执行方法后清除缓存
                                if (map.size() > 0) {
                                    map.forEach((key, val) -> {
                                        deleteRedisCache(key, val);
                                    });
                                }
                                key_map.forEach((key, val) -> redisTemplate.opsForValue().set(key, obj, val, TimeUnit.SECONDS));
                            });
                } else {
                    return proceed;
                }
            } else {

                if (returnTypeName.equals("Flux")) {
                    return ((Flux) proceed).collectList().doOnNext(list -> {
                        //执行方法后清除缓存
                        if (map.size() > 0) {
                            map.forEach((key, val) -> {
                                deleteRedisCache(key, val);
                            });
                        }
                    }).flatMapMany(list -> Flux.fromIterable((List) list));
                } else if (returnTypeName.equals("Mono")) {
                    return ((Mono) proceed).doOnNext(obj -> {
                        //执行方法后清除缓存
                        if (map.size() > 0) {
                            map.forEach((key, val) -> {
                                deleteRedisCache(key, val);
                            });
                        }
                    });
                } else {
                    return proceed;
                }
            }
        }


    }

    private void deleteRedisCache(String key, boolean clearAll) {
        if (clearAll) {
            Set keys = redisTemplate.keys(key + "_*");
            if (!keys.isEmpty()) {
                redisTemplate.delete(keys);
            }
        } else {
            if (redisTemplate.hasKey(key)) {
                redisTemplate.delete(key);
            }
        }
    }

    private void deleteRedisCache(String cacheName, String key, boolean clearAll) {

        String redis_key = "";
        if (clearAll) {
            redis_key = cacheName + "_*";
        } else {
            redis_key = cacheName + "_" + key;
        }

        deleteRedisCache(redis_key, clearAll);
    }

}

注解属性支持EL表达式的工具类

AspectSupportUtils

package com.hanqunfeng.reactive.redis.cache.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.cache.interceptor.SimpleKeyGenerator;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.EvaluationContext;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;


public class AspectSupportUtils {

    private static ExpressionEvaluator evaluator = new ExpressionEvaluator();

    public static Object getKeyValue(JoinPoint joinPoint, String keyExpression) {
        if(keyExpression.contains("#") || keyExpression.contains("'")) {
            return getKeyValue(joinPoint.getTarget(), joinPoint.getArgs(), joinPoint.getTarget().getClass(),
                    ((MethodSignature) joinPoint.getSignature()).getMethod(), keyExpression);
        }
        return keyExpression;
    }

    private static Object getKeyValue(Object object, Object[] args, Class<?> clazz, Method method,
                                      String keyExpression) {
        if (StringUtils.hasText(keyExpression)) {
            EvaluationContext evaluationContext = evaluator.createEvaluationContext(object, clazz, method, args);
            AnnotatedElementKey methodKey = new AnnotatedElementKey(method, clazz);
            return evaluator.key(keyExpression, methodKey, evaluationContext);
        }
        return SimpleKeyGenerator.generateKey(args);
    }

}

ExpressionEvaluator

package com.hanqunfeng.reactive.redis.cache.aop;

import org.springframework.aop.support.AopUtils;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.context.expression.CachedExpressionEvaluator;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


public class ExpressionEvaluator extends CachedExpressionEvaluator {
    private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();

    private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<>(64);

    private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);

    public EvaluationContext createEvaluationContext(Object object, Class<?> targetClass, Method method,
                                                     Object[] args) {
        Method targetMethod = getTargetMethod(targetClass, method);
        ExpressionRootObject root = new ExpressionRootObject(object, args);
        return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer);
    }

    public Object key(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext) {
        return getExpression(this.conditionCache, elementKey, conditionExpression).getValue(evalContext);
    }

    private Method getTargetMethod(Class<?> targetClass, Method method) {
        AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
        Method targetMethod = this.targetMethodCache.get(methodKey);
        if (targetMethod == null) {
            targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
            if (targetMethod == null) {
                targetMethod = method;
            }
            this.targetMethodCache.put(methodKey, targetMethod);
        }
        return targetMethod;
    }


    private class ExpressionRootObject {

        private final Object object;

        private final Object[] args;

        public ExpressionRootObject(Object object, Object[] args) {
            this.object = object;
            this.args = args;
        }

        public Object getobject() {
            return object;
        }

        public Object[] getArgs() {
            return args;
        }

    }
}

本项目提供了自动配置类,开启Aspect支持同时提供RedisTemplate

  • 支持LocalDate和LocalDateTime的序列化和反序列化
  • 存储key为字符串,值为json
package com.hanqunfeng.reactive.redis.cache.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 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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
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.StringRedisSerializer;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

/**
 * @author hanqf
 * Created by hanqf on 2020/11/22 15:38
 */
@Configuration
@ComponentScan(basePackages = "org.hanqf.reactive.redis.cache")
@EnableAspectJAutoProxy
@Slf4j
public class ReactiveRedisConfig {

    /**
     * 默认日期时间格式
     */
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    /**
     * 默认日期格式
     */
    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    /**
     * 默认时间格式
     */
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";


    @Bean
    @ConditionalOnMissingBean(value = RedisTemplate.class)
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

        log.debug("ReactiveRedisConfig RedisTemplate");
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);

        //LocalDateTime系列序列化和反序列化模块,继承自jsr310,我们在这里修改了日期格式
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        //序列化
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(
                DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addSerializer(LocalDate.class,
                new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        javaTimeModule.addSerializer(LocalTime.class,
                new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
        //反序列化
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(
                DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addDeserializer(LocalDate.class,
                new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        javaTimeModule.addDeserializer(LocalTime.class,
                new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册模块
        objectMapper.registerModule(javaTimeModule);

        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        serializer.setObjectMapper(objectMapper);

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(serializer);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容