SpringBoot基于AOP注解方式实现Redis缓存

news2024/11/17 11:53:53

一、前言

        Spring中的AOP(Aspect Oriented Programming)是基于代理的AOP实现,通过使用基于代理的技术,可以在不修改原有代码的情况下,对原有代码进行增强和改进。Spring AOP实现了面向切面编程的功能,将横切关注点(Cross-cutting concern)从业务逻辑中抽离出来,通过将切面应用到目标对象的方法上实现功能增强。Spring AOP支持多种通知类型:前置通知(@Before)、后置通知(@AfterReturning)、抛出通知(@AfterThrowing)、最终通知(@After)以及环绕通知(@Around),可以根据不同的需求进行选择。

        本文实现方式主要利用的技术就是Spring的AOP,通过自定义注解,AOP切入到注解上面,然后在切入点里面获取注解的参数等信息,调用Redis进行缓存。如果缓存未命中,切入点进行放行,让方法正常执行,拿到返回结果,然后进行Redis的缓存,最后返回结果。如果命中,则直接返回Redis的缓存,从而实现提升性能的目的。

二、依赖引入

        项目是基于Gradle构建,依赖如下:

// gradle 自身需求资源库 放头部
buildscript {
    repositories {
        maven { url 'https://maven.aliyun.com/repository/public' }// 加载其他Maven仓库
        mavenCentral()
    }
    dependencies {
        classpath('org.springframework.boot:spring-boot-gradle-plugin:2.1.1.RELEASE')// 加载插件,用到里面的函数方法
    }
}


apply plugin: 'java'
apply plugin: 'idea'
// 使用spring boot 框架
apply plugin: 'org.springframework.boot'
// 使用spring boot的自动依赖管理
apply plugin: 'io.spring.dependency-management'

// 版本信息
group 'com.littledyf'
version '1.0-SNAPSHOT'

// 执行项目中所使用的的资源仓库
repositories {
    maven { url 'https://maven.aliyun.com/repository/public' }
    mavenCentral()
}

// 项目中需要的依赖
dependencies {
    // 添加 jupiter 测试的依赖
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
    // 添加 jupiter 测试的依赖
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'

    // 添加 spring-boot-starter-web 的依赖 必须 排除了security 根据自身需求
    implementation('org.springframework.boot:spring-boot-starter-web') {
        exclude group: 'org.springframework.security', module: 'spring-security-config'
    }

    // 添加 spring-boot-starter-test 该依赖对于编译测试是必须的,默认包含编译产品依赖和编译时依赖
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    // 添加 junit 测试的依赖
    testImplementation group: 'junit', name: 'junit', version: '4.11'
    // 添加 lombok
    annotationProcessor 'org.projectlombok:lombok:1.18.22' // annotationProcessor代表main下代码的注解执行器
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.22'// testAnnotationProcessor代表test下代码的注解执行器
    compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.22' // compile代表编译时使用的lombok

    // redis
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '2.7.14'


    // aspectjrt
    implementation group: 'org.aspectj', name: 'aspectjrt', version: '1.9.19'

    // https://mvnrepository.com/artifact/cn.hutool/hutool-all
    implementation group: 'cn.hutool', name: 'hutool-all', version: '5.8.21'
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: '2.6.15'



}


test {
    useJUnitPlatform()
}

        如果使用maven构建,主要依赖就是spring-boot-starter、spring-boot-starter-data-redis、aspectjrt、hutool-all、spring-boot-starter-aop,因为是要缓存到Redis中,Redis依赖是必不可少的;hutool-all是其中使用到的工具类,主要用来生成一些MD5加密数据,用来当做存入Redis的key;要使用Spring的AOP,除了要引入aspectjrt以外,AOP也是必不可少的,否则是无法切入的。

三、代码

        SpringBoot启动类:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyTestApplication {

	public static void main(String[] args) {
		SpringApplication.run(MyTestApplication.class, args);
	}
}

        配置文件:

server:
  port: 8080
spring:
  application:
    name: my-test-service
  redis:
    host: 127.0.0.1
    port: 6379

        Redis配置类:

import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
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.StringRedisSerializer;

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {

    /**
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return createRedisTemplate(redisConnectionFactory);
    }

    private RedisTemplate<String, Object> createRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        Jackson2JsonRedisSerializer<Object> fastJsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);

        // value序列化方式采用fastJson
        template.setValueSerializer(fastJsonRedisSerializer);
        // hash的value序列化方式采用fastJson
        template.setHashValueSerializer(fastJsonRedisSerializer);

        template.afterPropertiesSet();

        return template;
    }

}

        注解类:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCacheable {
    // 过期时间,多久之后过期
    String expire() default "";
    // 过期时间,几点过期
    String expireAt() default "";
    // 名称,一般用服务名
    String invoker();
}

        Aspect类,这里是从注解那里切入进去,只要有调用注解的地方,就会切入进去:

import cn.hutool.crypto.digest.DigestUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;

import java.lang.reflect.Method;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.concurrent.TimeUnit;

import static org.springframework.http.HttpStatus.LOCKED;

@Aspect
@Slf4j
@Component
public class MyCacheableAspect {

    private final RedisTemplate redisTemplate;

    private final ObjectMapper objectMapper;

    public MyCacheableAspect(RedisTemplate redisTemplate, ObjectMapper objectMapper) {
        this.redisTemplate = redisTemplate;
        this.objectMapper = objectMapper;
    }

    @Around("@annotation(com.littledyf.cache.redis.MyCacheable)")
    public Object getCache(ProceedingJoinPoint joinPoint) {
        log.info("进入Cacheable切面");
        // 获取方法参数
        Object[] arguments = joinPoint.getArgs();
        // 获取方法签名
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        // 获取方法名
        String methodName = method.getName();
        // 获取注解
        MyCacheable annotation = method.getAnnotation(MyCacheable.class);
        // 获取注解的值
        String invoker = annotation.invoker();
        // 获取digest,即redis的key
        log.info("Digest准备生成:methodName:" + methodName + ",invoker=" + invoker);
        String digest = generateDigest(methodName, invoker, arguments);
        log.info("Digest生成:digest=" + digest);
        Object redisValue = redisTemplate.opsForValue().get(digest);

        if (redisValue == null) {
            log.info("缓存未命中:" + digest);
            log.info("缓存刷新开始:" + digest);
            String expire = annotation.expire();
            String expireAt = annotation.expireAt();

            redisValue = executeSynOperate(result -> {
                        if (!result) {
                            log.error("分布式锁异常");
                            return null;
                        }
                        Object checkGet = redisTemplate.opsForValue().get(digest);
                        if (checkGet != null) {
                            return checkGet;
                        }
                        // 刷新缓存
                        refreshCache(joinPoint, digest, expire, expireAt);

                        if (method.getAnnotation(PostMapping.class) != null) {
                            redisTemplate.opsForSet().add(methodName, arguments);
                        }
                        return redisTemplate.opsForValue().get(digest);
                    }, digest + "1", 50000
            );
        }

        log.info("Cache返回:digest=" + digest);

        return redisValue;
    }

    private void refreshCache(ProceedingJoinPoint joinPoint, String key, String expire, String expireAt) {
        Object methodResult = null;

        try {
            // 放行,让切面捕获的任务继续执行并获取返回结果
            methodResult = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        if (methodResult == null) {
            methodResult = new Object();
        }

        // 如果注解传入的expire参数不为空,则直接设置过期时间,否则看expireAt是否为空,否则设置默认过期时间
        if (!expire.equals("")) {
            long expireLong = Long.parseLong(expire);
            redisTemplate.opsForValue().set(key, methodResult, expireLong, TimeUnit.SECONDS);
        } else if (!expireAt.equals("")) {
            LocalTime expireAtTime = LocalTime.parse(expireAt);
            LocalDateTime now = LocalDateTime.now();
            LocalDateTime expireDateTime = LocalDateTime.of(now.toLocalDate(), expireAtTime);

            if (expireDateTime.compareTo(now) <= 0) {
                expireDateTime = expireDateTime.plusDays(1);
            }
            redisTemplate.opsForValue().set(key, methodResult, Duration.between(now, expireDateTime));
        } else {
            redisTemplate.opsForValue().set(key, methodResult, 3600 * 12, TimeUnit.SECONDS);
        }
    }

    // 生成digest,用来当做redis中的key
    private String generateDigest(String methodName, String invoker, Object[] arguments) {
        String argumentsDigest = "";

        if (arguments != null && arguments.length > 0) {
            StringBuilder stringBuilder = new StringBuilder();

            for (Object argument : arguments) {
                try {
                    String valueAsString = objectMapper.writeValueAsString(argument);

                    stringBuilder.append(valueAsString);
                } catch (JsonProcessingException e) {
                    log.error("参数" + argument + "字符串处理失败", e);
                }
            }

            byte[] bytes = DigestUtil.md5(stringBuilder.toString());
            argumentsDigest = new String(bytes);
        }

        return methodName + (invoker == null ? "" : invoker) + argumentsDigest;
    }

    // 等待时间重复获取
    private <T> T executeSynOperate(MainOperator<T> operator, String lockCacheKey, long milliTimeout) {
        try {
            if (operator != null && lockCacheKey != null && milliTimeout >= 0L) {
                boolean locked = false;
                long startNano = System.nanoTime();
                boolean waitFlag = milliTimeout > 0L;
                long nanoTimeOut = (waitFlag ? milliTimeout : 50L) * 1000000L;
                T resultObj = null;

                try {
                    while (System.nanoTime() - startNano < nanoTimeOut) {
                        if (redisTemplate.opsForValue().setIfAbsent(lockCacheKey, LOCKED, 120L, TimeUnit.SECONDS)) {
                            locked = true;
                            break;
                        }

                        if (!waitFlag) {
                            break;
                        }

                        Thread.sleep(1000);
                    }

                    resultObj = operator.executeInvokeLogic(locked);
                } catch (Exception ex) {
                    log.error("处理逻辑", ex);
                    return null;
                } finally {
                    if (locked) {
                        releaseRedisLock(lockCacheKey);
                    }
                }

                return resultObj;
            } else {
                throw new Exception("参数不合法");
            }
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 释放锁
     *
     * @param cacheKey
     */
    public boolean releaseRedisLock(final String cacheKey) {
        Boolean deleteLock = redisTemplate.delete(cacheKey);

        if (Boolean.TRUE.equals(deleteLock)) {
            return true;
        }

        return false;
    }

    // 函数式接口
    public interface MainOperator<T> {
        boolean HOLD_LOCK_TAG = false;

        T executeInvokeLogic(boolean result) throws Exception;
    }


}

        controller层和service层,service层中的方法会加上自定义注解:

import com.littledyf.cache.redis.service.RedisCacheServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/my-test/redis-cache")
public class RedisCacheController {

    private final RedisCacheServiceImpl redisCacheService;

    public RedisCacheController(RedisCacheServiceImpl redisCacheService) {
        this.redisCacheService = redisCacheService;
    }


    @GetMapping(value = "/test/{value}")
    public String testRedisCache(@PathVariable("value")  String value)  {
        return redisCacheService.testRedisCache(value);
    }
}
import com.littledyf.cache.redis.MyCacheable;
import org.springframework.stereotype.Service;

@Service
public class RedisCacheServiceImpl {

    @MyCacheable(expire = "60", invoker = "my-test")
    public String testRedisCache(String value) {
        System.err.println("testRedisCache");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 模拟业务逻辑处理
        return value;
    }
}

        这里使用休眠5秒来查验效果,注解中用的expire,即redis的缓存在60秒后过期,也可以使用expireAt指定在具体什么时间过期。

四、测试结果

        首先可以查看Redis中的key,可以看到目前是没有缓存数据的:

         启动项目,第一次调用结果:

 

        第一次调用显示时间5s以上,并且Redis中也有了缓存数据:

 

         在60s时间内再进行第二次调用:

        可以发现时间直接来到15ms,证明缓存是有效的,直接跳过了代码中休眠的5s。等60s后,刚才缓存的数据自动会过期,其余测试就不展示了,有兴趣也可以试试 expireAt在指定时间过期的方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/921606.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

电工-捡测电动机绕组首尾端的电路接线图

电工捡测电动机绕组首尾端的电路接线图 若三相电动机绕组首尾端接错&#xff0c;电动机起动时&#xff0c;会引起振动、噪声、三相电流严重不平衡、电动机过热、转速降低&#xff0c;甚至电动机不转等故障。 可用交流电源和灯泡来检查电动机三相绕组的首尾端用以检测电动机首尾…

Vue3项目实战

目录 一、项目准备 二、基础语法应用 2.1、mixin应用 2.2、网络请求 2.3、显示与隐藏 2.4、编程式路由跳转 2.5、下载资料 2.6、调用方法 2.7、监听路由变化 2.8、pinia应用 (1)存储token(user.js) (2)全选全不选案例(car.js) 一、项目准备 下载&#xff1a; cnp…

Google Play上线问题及解决方案

将应用上线到Google Play商店也可能会面临一些问题&#xff0c;在上线应用到Google Play商店之前&#xff0c;确保你充分测试应用&#xff0c;遵循Google Play的开发者政策和要求&#xff0c;以及关注用户的反馈&#xff0c;这些都能帮助你尽可能地解决问题并提供优秀的用户体验…

opencv 案例实战02-停车场车牌识别SVM模型训练及验证

1. 整个识别的流程图&#xff1a; 2. 车牌定位中分割流程图&#xff1a; 三、车牌识别中字符分割流程图&#xff1a; 1.准备数据集 下载车牌相关字符样本用于训练和测试&#xff0c;本文使用14个汉字样本和34个数字跟字母样本&#xff0c;每个字符样本数为40&#xff0c;样本尺…

无涯教程-PHP - preg_replace()函数

preg_replace() - 语法 mixed preg_replace (mixed pattern, mixed replacement, mixed string [, int limit [, int &$count]] ); preg_replace()函数的操作与POSIX函数ereg_replace()相同&#xff0c;不同之处在于可以在模式和替换输入参数中使用正则表达式。 可选的输…

PowerJob的启动及使用

首先&#xff0c;本文中提到的server就是指powerjob-server模块&#xff08;也就是powerJob的重点之一的调度服务&#xff09; 一、初始化项目 1. PowerJob的下载 官方文档 2. 导入到IDEA中&#xff0c;下载依赖后&#xff0c;打开powerjob-server模块的a…

开发中常用的小脚本、工具

文章目录 1. mysql数据库相关1.1 查看数据库各表占用内存大小1.2 数据库字段脱敏脚本 1. mysql数据库相关 1.1 查看数据库各表占用内存大小 SELECT table_name, ROUND(((data_length index_length) / 1024 / 1024), 2) AS "Size (MB)" FROM information_schema.t…

Instagram合规运营的10条策略

Instagram每月活跃用户15亿&#xff0c;是跨境外贸开发客户与广告引流的常用工具。本文总结10条Instagram运营基本策略与原则&#xff0c;帮助各位跨境人更好的了解平台规则&#xff0c;规避风险&#xff0c;提高投放效率&#xff01; 1、使用商业账号 企业在instagram 上进行…

高精度参考电压源是什么意思

高精度参考电压源是一种能够提供稳定、准确且可靠的参考电压的电路或器件。在电子系统中&#xff0c;参考电压起着至关重要的作用&#xff0c;它被用作比较、校准、测量等各种应用中的基准电压。高精度参考电压源能够提供高精度的参考电压&#xff0c;具有很低的温漂、噪声和漂…

软考A计划-系统集成项目管理工程师-项目变更管理

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 &#x1f449;关于作者 专注于Android/Unity和各种游…

2023年天府杯A 题:震源属性识别模型构建与震级预测

基于数据分析的震源属性识别模型构建与震级预测问题的研究 问题一&#xff1a; 解题思路: 第一部: 对数据进行一个处理&#xff0c;将数据进行分类&#xff0c;求出数据中的最大值&#xff0c;最小值&#xff0c;极差&#xff0c;方差等等一系列特征数据。&#xff0c;将天然…

Git企业开发控制理论和实操-从入门到深入(三)|分支管理

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总 然后就是博主最近最花时间的一个专栏…

PhantomJS+java 后端生成echart图表的图片

PhantomJSjava 后端生成echart图表的图片 前言源码效果实现echarts-convertPhantomJS实现echarts截图得到图片java延时读取base64数据 参考 前言 该项目仅用作个人学习使用 源码 地址 docker镜像&#xff1a; registry.cn-chengdu.aliyuncs.com/qinjie/java-phantomjs:1.0 …

【Git】代码误推送还原(真实项目环境,非纸上谈兵)

背景 RT&#xff0c; 我今天眼睛花了&#xff0c;不小心把工作分支【合并】到了一个不相干的功能分支上&#xff0c;并且代码已经推送到远程仓库了。于是&#xff0c;只能尝试还原到上一次提交中。 【合并】分支有一个点我们是不可避免的&#xff0c;文字很难描述&#xff0c;…

一文总结:如何在csdn中使用markdown写出精美文章

这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个…

导出功能exportExcel (现成直接用)

1. 实体类字段上加 Excel(name "xxx"), 表示要导出的字段 Excel(name "订单号")private String orderNo; 2. controller (get请求) /*** 导出订单列表*/ApiOperation("导出订单列表")GetMapping("/export")public void export(HttpS…

Ompl初探

在/ompl-1.x.0/build/Release/bin下有很多生成的demo可执行文件 在终端执行 ./demo_Point2DPlanning 测试程序 #include <ompl/base/SpaceInformation.h> #include <ompl/base/spaces/SE3StateSpace.h> #include <ompl/base/StateSpace.h> #include <o…

Python“牵手”蘑菇街商品详情API接口运用场景及功能介绍,蘑菇街接口申请指南

蘑菇街是专注于时尚女性消费者的电子商务网站&#xff0c;是时尚和生活方式目的地。 蘑菇街通过形式多样的时尚内容等时尚商品&#xff0c;让人们在分享和发现流行趋势的同时&#xff0c;享受购物体验。蘑菇街不是一个购物平台&#xff0c;它是一个购物指南网站&#xff0c;帮…

python3对接godaddy API,实现自动更改域名解析(DDNS)

python3对接godaddy API&#xff0c;实现自动更改域名解析&#xff08;DDNS&#xff09; 文章开始前&#xff0c;先解释下如下问题&#xff1a; ①什么是域名解析&#xff1f; 域名解析一般是指通过一个域名指向IP地址&#xff08;A解析&#xff09;&#xff0c;然后我们访问…

缓存之争:Redis和JVM面对面,你会选谁?

大家好&#xff0c;我是你们的小米&#xff01;今天要和大家聊聊一个在技术面试中经常被问到的问题&#xff1a;Redis缓存和JVM缓存有什么区别呢&#xff1f;相信这个问题在不少小伙伴的面试路上都遇到过&#xff0c;今天就让我们来深入剖析一下吧&#xff01; 缓存的作用和意…