SpringBoot-WebFlux-Redis緩存注解

摘要

前言

最近在使用WebFlux時(shí)發(fā)現(xiàn),SpringBoot提供的@Cacheable,@CachePut熙含,@CacheEvict和@Caching注解不支持響應(yīng)式方法,SpringBoot官方也沒(méi)有提供響應(yīng)式方法的緩存注解艇纺,看到網(wǎng)上的一些解決方案都是直接在方法代碼中加入緩存數(shù)據(jù)的代碼邏輯怎静,這樣雖然可以解決問(wèn)題,但是代碼侵入并不優(yōu)雅黔衡,于是萌生自己寫(xiě)一個(gè)基于redis的響應(yīng)式方法緩存注解的想法蚓聘,本項(xiàng)目參考SpringBoot提供的@Cacheable,@CachePut盟劫,@CacheEvict和@Caching注解聲明夜牡,但是只是實(shí)現(xiàn)了一些基本功能,可以滿足絕大部分使用場(chǎng)景的要求侣签,因?yàn)镾pringBoot早晚會(huì)給出官方解決方案塘装,在此之前,不妨一試影所。

使用示例

  • 本項(xiàng)目已經(jīng)發(fā)布到maven中央倉(cāng)庫(kù)蹦肴,直接在項(xiàng)目中添加依賴即可。
  • 本項(xiàng)目雖然基于springboot:2.4.0構(gòu)建猴娩,但實(shí)際上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'
  • 此時(shí)項(xiàng)目中可能還要添加其它依賴,以gradle舉例
//webflux胀溺,非必須裂七,主要是面向響應(yīng)式編程的,所以使用springboot大概率會(huì)使用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連接池徙瓶,非必須毛雇,正式環(huán)境建議開(kāi)啟連接池
implementation 'org.apache.commons:commons-pool2'

//aop 面向方面編程 支持@Aspect,非必須
implementation 'org.springframework.boot:spring-boot-starter-aop'
  • 方法返回值必須是Mono或者Flux類型侦镇,使用方式與springboot提供的Cacheable等注解類似
    /**
    * 緩存 cacheName和key支持EL表達(dá)式灵疮,實(shí)際key的名稱是"cacheName_key"
    * 緩存結(jié)果
    * 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_"開(kāi)頭的緩存
    * allEntries 默認(rèn)false壳繁,此時(shí)需要指定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類似
    * 規(guī)則:
    * 1.cacheables不能與cacheEvicts或者cachePuts同時(shí)存在闹炉,因?yàn)楹笳咭欢〞?huì)執(zhí)行方法主體蒿赢,達(dá)不到調(diào)用緩存的目的,所以當(dāng)cacheables存在時(shí)渣触,后者即便指定也不執(zhí)行
    * 2.先執(zhí)行cacheEvicts羡棵,再執(zhí)行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

  • 如果使用時(shí)沒(méi)有創(chuàng)建RedisTemplate,本項(xiàng)目中提供了一個(gè)默認(rèn)的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养篓,我們?cè)谶@里修改了日期格式
        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)));

        //注冊(cè)模塊
        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;
    }

以下為源碼說(shuō)明

源碼依賴

<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緩存相關(guān)注解

  • 只支持方法返回類型為Mono或者Flux
  • 其它返回類型時(shí)請(qǐng)使用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表達(dá)式
    */
    String key();

    /**
     * 緩存key分組峻贮,會(huì)做為緩存key的前綴+"_"
     * 支持EL表達(dá)式
    */
    String cacheName();

    /**
     * 緩存過(guò)期時(shí)間,單位秒应闯,默認(rèn)24小時(shí)
    */
    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表達(dá)式
     */
    String key() default "";

    /**
     * 緩存key分組碉纺,會(huì)做為緩存key的前綴+"_"
     * 支持EL表達(dá)式
     */
    String cacheName();

    /**
     * 是否刪除cacheName下全部緩存數(shù)據(jù)船万,
     * true時(shí)cacheName不能為空,此時(shí)即便指定了key值骨田,也會(huì)刪除cacheName下全部緩存
     * false時(shí)key值不能為空
     */
    boolean allEntries() default false;

    /**
     * 調(diào)用清除緩存的時(shí)機(jī)耿导,true:執(zhí)行方法前,false:執(zhí)行方法后
     * 如果是false态贤,則方法執(zhí)行過(guò)程中發(fā)生異常舱呻,則不會(huì)清除緩存
    */
    boolean beforeInvocation() default false;
}

ReactiveRedisCachePut

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

import java.lang.annotation.*;

/**
 * <h1>執(zhí)行完方法更新緩存</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表達(dá)式
     */
    String key();

    /**
     * 緩存key分組,會(huì)做為緩存key的前綴+"_"
     * 支持EL表達(dá)式
     */
    String cacheName();

    /**
     * 緩存過(guò)期時(shí)間箱吕,單位秒芥驳,默認(rèn)24小時(shí)
     */
    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
//標(biāo)識(shí)是一個(gè)Aspect代理類
@Aspect
//如果有多個(gè)切面攔截相同的切點(diǎn),可以用@Order指定執(zhí)行順序
//@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() {
    }

    //環(huán)繞通知,一般不建議使用茬高,可以通過(guò)@Before和@AfterReturning實(shí)現(xiàn)
    //但是響應(yīng)式方法只能通過(guò)環(huán)繞通知實(shí)現(xiàn)aop兆旬,因?yàn)槠渌ㄖ獣?huì)導(dǎo)致不再同一個(gè)線程執(zhí)行
    @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();

        //轉(zhuǎn)換EL表達(dá)式
        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 {
            //實(shí)際執(zhí)行的方法
            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();

        //轉(zhuǎn)換EL表達(dá)式
        cacheName = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, cacheName);
        key = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, key);


        //執(zhí)行方法前清除緩存
        if (beforeInvocation) {

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

            //實(shí)際執(zhí)行的方法
            Object proceed = proceedingJoinPoint.proceed();
            return proceed;
        } else {//成功執(zhí)行方法后清除緩存

            //實(shí)際執(zhí)行的方法
            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();

        //轉(zhuǎn)換EL表達(dá)式
        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);
        }

        //實(shí)際執(zhí)行的方法
        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();

        //規(guī)則:
        //1.cacheables不能與cacheEvicts或者cachePuts同時(shí)存在,因?yàn)楹笳咭欢〞?huì)執(zhí)行方法主體怎栽,達(dá)不到調(diào)用緩存的目的丽猬,所以當(dāng)cacheables存在時(shí),后者即便指定也不執(zhí)行
        //2.先執(zhí)行cacheEvicts婚瓜,再執(zhí)行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();

                //轉(zhuǎn)換EL表達(dá)式
                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 {
                //實(shí)際執(zhí)行的方法
                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();

                    //轉(zhuǎn)換EL表達(dá)式
                    cacheName = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, cacheName);
                    key = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, key);

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

            //實(shí)際執(zhí)行的方法
            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();

                    //轉(zhuǎn)換EL表達(dá)式
                    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 -> {
                                //執(zhí)行方法后清除緩存
                                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 -> {
                                //執(zhí)行方法后清除緩存
                                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 -> {
                        //執(zhí)行方法后清除緩存
                        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 -> {
                        //執(zhí)行方法后清除緩存
                        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表達(dá)式的工具類

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;
        }

    }
}

本項(xiàng)目提供了自動(dòng)配置類巴刻,開(kāi)啟Aspect支持同時(shí)提供RedisTemplate

  • 支持LocalDate和LocalDateTime的序列化和反序列化
  • 存儲(chǔ)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 {

    /**
     * 默認(rèn)日期時(shí)間格式
     */
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    /**
     * 默認(rèn)日期格式
     */
    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    /**
     * 默認(rèn)時(shí)間格式
     */
    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胡陪,我們?cè)谶@里修改了日期格式
        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)));

        //注冊(cè)模塊
        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;
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市碍舍,隨后出現(xiàn)的幾起案子柠座,更是在濱河造成了極大的恐慌,老刑警劉巖片橡,帶你破解...
    沈念sama閱讀 221,406評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妈经,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡捧书,警方通過(guò)查閱死者的電腦和手機(jī)吹泡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)经瓷,“玉大人爆哑,你說(shuō)我怎么就攤上這事∮咚保” “怎么了揭朝?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,815評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)色冀。 經(jīng)常有香客問(wèn)我潭袱,道長(zhǎng),這世上最難降的妖魔是什么锋恬? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,537評(píng)論 1 296
  • 正文 為了忘掉前任屯换,我火速辦了婚禮,結(jié)果婚禮上伶氢,老公的妹妹穿的比我還像新娘趟径。我一直安慰自己瘪吏,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布蜗巧。 她就那樣靜靜地躺著掌眠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪幕屹。 梳的紋絲不亂的頭發(fā)上蓝丙,一...
    開(kāi)封第一講書(shū)人閱讀 52,184評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音望拖,去河邊找鬼渺尘。 笑死权她,一個(gè)胖子當(dāng)著我的面吹牛昌抠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播秋忙,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼盔沫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼医咨!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起架诞,我...
    開(kāi)封第一講書(shū)人閱讀 39,668評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拟淮,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后谴忧,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體很泊,經(jīng)...
    沈念sama閱讀 46,212評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評(píng)論 3 340
  • 正文 我和宋清朗相戀三年沾谓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了委造。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,438評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡搏屑,死狀恐怖争涌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辣恋,我是刑警寧澤亮垫,帶...
    沈念sama閱讀 36,128評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站伟骨,受9級(jí)特大地震影響饮潦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜携狭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評(píng)論 3 333
  • 文/蒙蒙 一继蜡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦稀并、人聲如沸仅颇。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,279評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)忘瓦。三九已至,卻和暖如春引颈,著一層夾襖步出監(jiān)牢的瞬間耕皮,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,395評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工蝙场, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凌停,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,827評(píng)論 3 376
  • 正文 我出身青樓售滤,卻偏偏與公主長(zhǎng)得像罚拟,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子完箩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容