Spring Boot中使用AOP和反射机制设计一个的幂等注解(两种持久化模式),简单易懂教程

该帖子介绍如何设计利用AOP设计幂等注解,且可设置两种持久化模式

1、普通模式:基于redis的幂等注解,持久化程度较低

2、增强模式:基于数据库(MySQL)的幂等注解,持久化程度高

如果只需要具有redis持久化幂等的功能就可以,参考Spring Boot中使用AOP设计一个基于redis的幂等注解,简单易懂教程-CSDN博客

由于对于一些非查询操作,有时候需要保证该操作是幂等的,该帖子设计幂等注解的原理是使用AOP和反射机制获取方法的类、方法和参数,然后拼接形成一个幂等键,当下一次有重复操作过来的时候,判断该幂等键是否存放,如果存在则为”重复操作“,不继续执行;如果不存在,则为”第一次操作“,可以执行。

javaer可以在自己的项目中,加入这个点,增加项目的亮点。

1、配置依赖、配置redis、创建MySQL表

1.1、在pom文件中加入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

1.2、配置redis地址

如何安装redis、获取redis的ip地址,以及redis可视化工具RDM的使用,参考以下博客的1、2点Spring Boot项目中加入布隆过滤器————实战-CSDN博客

如果不想使用docker容器安装redis,可以自己下载安装redis。

spring:
  redis:
    host: 192.168.57.111 #替换为自己redis所在服务器的ip
    port: 6378 #替换为自己redis的端口
    password: # 如果无密码则留空

1.3、使用RDM连接redis 

如何连接,参考以下博客的1、2点Spring Boot项目中加入布隆过滤器————实战-CSDN博客

1.4、创建mysql持久化键的表

在springboot连接的mysql数据库上,执行以下语句

-- 创建用于存储幂等键的表
CREATE TABLE idempotent_keys (
    id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 主键,自增,唯一标识每条记录
    idempotent_key VARCHAR(255) NOT NULL UNIQUE, -- 幂等键,唯一约束,用于防止重复操作
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 键的创建时间,默认为当前时间
) COMMENT='存储幂等键的表,用于实现幂等性操作';

创建成功

下一步是写逻辑代码

2、 主要逻辑代码

2.1、创建目录和文件

创建类似的目录结构,util与service同一级即可,并如下创建四个文件

2.2、Idempotent.java

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

/**
 * 幂等性注解
 * 支持Redis持久和数据库持久模式
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    Mode mode() default Mode.REDIS; // 持久模式:默认Redis

    enum Mode {
        REDIS, DATABASE
    }
}

默认为redis持久模式,可设置为数据库持久模式

2.3、IdempotentAspect.java

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.springframework.stereotype.Component;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
public class IdempotentAspect {

    private final RedisUtil redisUtil;
    private final IdempotentDatabaseUtil databaseUtil;

    public IdempotentAspect(RedisUtil redisUtil, IdempotentDatabaseUtil databaseUtil) {
        this.redisUtil = redisUtil;
        this.databaseUtil = databaseUtil;
    }

    /**
     * 定义Pointcut,用于拦截service包中的所有方法
     */
    //@Pointcut("execution(* com.xxx.service..*(..)) && @annotation(idempotent)")
    //可以对下面这一行注释掉,然后使用上面这一行代码,但包的路径需要换
    @Pointcut("@annotation(idempotent)")
    public void idempotentMethods(Idempotent idempotent) {
    }

    /**
     * 定义环绕通知,处理幂等性逻辑
     */
    @Around("idempotentMethods(idempotent)")
    public Object handleIdempotent(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
        // 生成幂等键
        String key = generateKey(joinPoint);
        if (key == null || key.isEmpty()) {
            throw new IllegalArgumentException("无法生成幂等键");
        }

        boolean success;
        if (idempotent.mode() == Idempotent.Mode.REDIS) {
            success = redisUtil.setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
        } else {
            success = databaseUtil.saveKeyIfAbsent(key);
        }

        if (!success) {
            throw new IllegalStateException("重复操作");
            //这里可使用自己定义的结果返回类包裹信息,就可以不抛出错误
        }

        try {
            return joinPoint.proceed();
        } finally {
            // 可选:操作完成后清理key,视业务需求决定是否需要
        }
    }

    /**
     * 动态生成幂等键
     */
    private String generateKey(ProceedingJoinPoint joinPoint) {
        // 获取类名
        String className = joinPoint.getTarget().getClass().getSimpleName();
        // 获取方法名
        String methodName = joinPoint.getSignature().getName();
        // 获取参数
        Object[] args = joinPoint.getArgs();
        String argsString = Arrays.toString(args);

        // 原始键内容
        String rawKey = String.format("%s:%s:%s", className, methodName, argsString);

        // 对键进行MD5编码
        return "IDEMPOTENT:"+md5(rawKey);
    }

    private String md5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] hashBytes = md.digest(input.getBytes());
            StringBuilder hexString = new StringBuilder();
            for (byte b : hashBytes) {
                String hex = Integer.toHexString(0xff & b);
                if (hex.length() == 1) hexString.append('0');
                hexString.append(hex);
            }
            return hexString.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5算法不可用", e);
        }
    }
}


 在该代码里,使用的是这个,虽然可以用但不严谨,因为更严谨一点,我们只运行幂等注解被我们的几题的service类里的方法使用,因为如果用在其他类的方法上的话,会造成同一个操作出现两个不同的幂等键,造成混乱。
@Pointcut("@annotation(idempotent)")

所以建议这一行注释掉,然后使用下面面这一行代码,但包的路径需要换

@Pointcut("execution(* com.xxx.service..*(..)) && @annotation(idempotent)")

2.4、IdempotentDatabaseUtil.java

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

@Component
public class IdempotentDatabaseUtil {

    private final JdbcTemplate jdbcTemplate;

    public IdempotentDatabaseUtil(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * 尝试保存幂等键
     *
     * @param key 幂等键
     * @return 如果键不存在并保存成功,则返回true;否则返回false
     */
    public boolean saveKeyIfAbsent(String key) {
        String sql = "INSERT INTO idempotent_keys (idempotent_key) VALUES (?)";
        try {
            jdbcTemplate.update(sql, key);
            return true; // 插入成功
        } catch (Exception e) {
            return false; // 键已存在
        }
    }

    /**
     * 删除幂等键
     *
     * @param key 幂等键
     */
    public void deleteKey(String key) {
        String sql = "DELETE FROM idempotent_keys WHERE idempotent_key = ?";
        jdbcTemplate.update(sql, key);
    }

    /**
     * 检查是否存在幂等键
     *
     * @param key 幂等键
     * @return 存在则返回true,否则返回false
     */
    public boolean exists(String key) {
        String sql = "SELECT COUNT(1) FROM idempotent_keys WHERE idempotent_key = ?";
        Integer count = jdbcTemplate.queryForObject(sql, new Object[]{key}, Integer.class);
        return count != null && count > 0;
    }

    /**
     * 清理过期幂等键(可选,用于定期清理)
     *
     * @param durationInMinutes 清理指定分钟数之前的键
     */
    public void cleanOldKeys(int durationInMinutes) {
        String sql = "DELETE FROM idempotent_keys WHERE created_at < NOW() - INTERVAL ? MINUTE";
        jdbcTemplate.update(sql, durationInMinutes);
    }
}

2.5、RedisUtil.java

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {

    private final StringRedisTemplate redisTemplate;

    public RedisUtil(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public boolean setIfAbsent(String key, String value, long timeout, TimeUnit unit) {
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
        return result != null && result;
    }

    public void delete(String key) {
        redisTemplate.delete(key);
    }
}

3、在业务代码上测试

3.1、redis持久化模式

@Idempotent(mode = Idempotent.Mode.REDIS)

把上面这一行代码扣在方法头上就能用了,如下 

@Override
@Idempotent(mode = Idempotent.Mode.REDIS)
public <T> ReturnStatus<T> createTask(TaskRequest TaskRequest) {
    //业务代码        
}

启动项目,使用postman调用createTask接口

结果显示,成功!

查看RDM中redis的数据

redis幂等键存在,同一个接口同样的参数再调用一次postman

因为已经存在幂等键了,调用失败,再查看idea控制台打印的日志,有”重复操作“的信息,符合实际,测试成功!

3.2、数据库持久化模式

@Idempotent(mode = Idempotent.Mode.DATABASE)

把上面这一行代码扣在方法头上就能用了,如下 

@Override
@Idempotent(mode = Idempotent.Mode.DATABASE)
public <T> ReturnStatus<T> createTask(TaskRequest TaskRequest) {
    //业务代码        
}

启动项目,使用postman调用createTask接口

成功执行,查看一下数据库是否有该数据,yes,存在

查看打印出的日志(需要在application.yml中配置,这一步无关紧要),显示了在idempotent_keys这张表插入数据的sql语句

同一个接口同样的参数再调用一次postman

查看idea控制台打印的日志

由于幂等键存在,所以调用失败,符合实际,测试成功!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

算法编程题-网格中的最短路径

算法编程题-网格中的最短路径 原题描述思路简述代码实现[^1]复杂度分析 原题描述 LeetCode 1293 网格中的最短路径&#xff1a;给定一个m * n的网格&#xff0c;网格中的每一个点的值为0&#xff08;无障碍&#xff09;&#xff0c;为1&#xff08;有障碍&#xff09;&#xf…

Xcode 项目内 OC 混编 Python,调用 Python 函数,并获取返回值(基于 python 的 c函数库)

1:新建 Xcode 工程 2:工程添加 Python.framework 1597052861430.jpg 3:在当前工程下新建一个名字为 googleT 的 python 文件(googleT.py) 1597052584962.jpg 在 googleT.py 文件内写入一个测试 python 函数 def lgf_translate( str ):var1 Hello World!print (str var1)retu…

蓝桥杯每日真题 - 第16天

题目&#xff1a;&#xff08;卡牌&#xff09; 题目描述&#xff08;13届 C&C B组C题&#xff09; 解题思路&#xff1a; 题目分析&#xff1a; 有 n 种卡牌&#xff0c;每种卡牌的现有数量为 a[i]&#xff0c;所需的最大数量为 b[i]&#xff0c;还有 m 张空白卡牌。 每…

计算机网络——路由选择算法

路由算法 路由的计算都是以子网为单位计算的——找到从原子网到目标子网的路径 链路状态算法 序号——&#xff08;源路由器&#xff0c;序号&#xff09;——如果发现这个序号重复或者老了——就不扩散 先测量——再泛洪获得路由 路由转发情况 若S——>W是21则不更改——…

Android - Pixel 6a 手机OS 由 Android 15 降级到 Android 14 操作记录

Pixel 6a 手机由 Android 14 升级到 Android 15了&#xff0c;但是由于一些原因又想降级回 Android 14&#xff0c; 能降吗&#xff1f;该怎么降级呢&#xff1f;本篇文章来记述实际操作过程&#xff0c;希望能给想做相同操作的人一些帮助。 答案当然是能降&#xff0c;而且我…

SpringBoot+React养老院管理系统 附带详细运行指导视频

文章目录 一、项目演示二、项目介绍三、运行截图四、主要代码1.入住合同文件上传2.添加和修改套餐的代码3.查看入住记录代码 一、项目演示 项目演示地址&#xff1a; 视频地址 二、项目介绍 项目描述&#xff1a;这是一个基于SpringBootReact框架开发的养老院管理系统。首先…

Ubuntu安装ollama,并运行ollama和通义千问,使用gradio做界面

Ubuntu安装ollama&#xff0c;并运行ollama和通义千问 安装ollama方式一&#xff1a;方式二 下载安装模型运行大模型运行ollama服务前端的实现python环境安装修改pip国内源前端页面搭建测试前后端联通设计完整的ui 安装ollama 方式一&#xff1a; 访问网站连接&#xff0c;选…

【微软:多模态基础模型】(3)视觉生成

欢迎关注[【youcans的AGI学习笔记】](https://blog.csdn.net/youcans/category_12244543.html&#xff09;原创作品 【微软&#xff1a;多模态基础模型】&#xff08;1&#xff09;从专家到通用助手 【微软&#xff1a;多模态基础模型】&#xff08;2&#xff09;视觉理解 【微…

前端研发高德地图,如何根据经纬度获取地点名称和两点之间的距离?

地理编码与逆地理编码 引入插件&#xff0c;此示例采用异步引入&#xff0c;更多引入方式 https://lbs.amap.com/api/javascript-api-v2/guide/abc/plugins AMap.plugin("AMap.Geocoder", function () {var geocoder new AMap.Geocoder({city: "010", /…

Linux上使用SELinux保护网络服务

前言 SELinux&#xff08;Security-Enhanced Linux&#xff09;是一种安全模块&#xff0c;用于增强基于 Linux 的操作系统的安全性。 它通过强制访问控制&#xff08;MAC&#xff09;机制来限制进程和用户对系统资源的访问权限&#xff0c;从而防止未经授权的操作。 在 SELin…

【Linux】僵尸进程、进程状态简介

本文内容均来自个人笔记并重新梳理&#xff0c;如有错误欢迎指正&#xff01; 如果对您有帮助&#xff0c;烦请点赞、关注、转发、订阅专栏&#xff01; 专栏订阅入口 | 精选文章 | Kubernetes | Docker | Linux | 羊毛资源 | 工具推荐 | 往期精彩文章 【Docker】&#xff08;全…

uniapp 选择 省市区 省市 以及 回显

从gitee仓库可以拿到demo 以及 json省市区 文件 // 这是组件部分 <template><uni-popup ref"popup" type"bottom"><view class"popup"><view class"picker-btn"><view class"left" click"…

Unity Dots下的动画合批工具:GPU ECS Animation Baker

书接上文&#xff0c;为了实现大批量物体的生成&#xff0c;我们准备使用Unity最新的dots系统&#xff0c;在该系统下找到了动画解决方案&#xff1a;GPU ECS Animation Baker。 导入的同时&#xff0c;也需要导入以下两个插件&#xff0c;否则会提示报错&#xff1a; PS&…

windows上部署flask程序

文章目录 前言一、准备工作二、配置 Gunicorn 或 uWSGI1.安装 Waitress2.修改启动文件来使用 Waitress 启动 Flask 应用3.配置反向代理&#xff08;可选&#xff09;4.启动程序访问 三.Flask 程序在 Windows 启动时自动启动1.使用 nssm&#xff08;Non-Sucking Service Manager…

共享单车管理系统项目学习实战

前言 Spring Boot Vue前后端分离 前端&#xff1a;Vue&#xff08;CDN&#xff09; Element axios(前后端交互) BaiDuMap ECharts(图表展示) 后端&#xff1a;Spring Boot Spring MVC(Web) MyBatis Plus(数据库) 数据库:MySQL 验证码请求

python中Pandas操作excel补全内容

补全ID、InStore、Date import random from datetime import datetime, timedeltaimport pandas as pdfile_path r"C:\Users\xb\Desktop\Books_1.xlsx" books pd.read_excel(iofile_path, skiprows3, usecols"C:F", dtype{"ID": str, "I…

40分钟学 Go 语言高并发:Goroutine基础与原理

Day 03 - goroutine基础与原理 1. goroutine创建和调度 1.1 goroutine基本特性 特性说明轻量级初始栈大小仅2KB&#xff0c;可动态增长调度方式协作式调度&#xff0c;由Go运行时管理创建成本创建成本很低&#xff0c;可同时运行数十万个通信方式通过channel进行通信&#x…

Python学习------第十天

数据容器-----元组 定义格式&#xff0c;特点&#xff0c;相关操作 元组一旦定义&#xff0c;就无法修改 元组内只有一个数据&#xff0c;后面必须加逗号 """ #元组 (1,"hello",True) #定义元组 t1 (1,"hello") t2 () t3 tuple() prin…

nwjs崩溃复现、 nwjs-控制台手动操纵、nwjs崩溃调用栈解码、剪切板例子中、nwjs混合模式、xdotool显示nwjs所有进程窗口列表

-1. nwjs在低版本ubuntu运行情况 ubuntu16.04运行nw-v0.93或0.89报错找不到NSS_3.30、GLIBC_2.25 uname -a #Linux Asus 4.15.0-112-generic #113~16.04.1-Ubuntu SMP Fri Jul 10 04:37:08 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux cat /etc/issue #Ubuntu 16.04.7 LTS \n \l…

在自动驾驶进行大数据量因果推理实验时,如何减少无用功,提高实验效率?

在对实验结果做反事实推理时&#xff0c;通常需要对数据进行多次循环&#xff0c;然后对多次循环的结果进行处理&#xff0c;如果只在最后结果结束时&#xff0c;再进行处理&#xff0c;可能会由于反事实过程中某个参数设置错误&#xff0c;导致整个反事实实验出现错误&#xf…