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。

XML 复制代码
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数据库上,执行以下语句

sql 复制代码
-- 创建用于存储幂等键的表
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

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

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)")

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

java 复制代码
@Pointcut("execution(* com.xxx.service..*(..)) && @annotation(idempotent)")

2.4、IdempotentDatabaseUtil.java

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

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持久化模式

java 复制代码
@Idempotent(mode = Idempotent.Mode.REDIS)

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

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

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

结果显示,成功!

查看RDM中redis的数据

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

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

3.2、数据库持久化模式

java 复制代码
@Idempotent(mode = Idempotent.Mode.DATABASE)

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

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

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

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

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

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

查看idea控制台打印的日志

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

相关推荐
无限进步_几秒前
【C++】验证回文字符串:高效算法详解与优化
java·开发语言·c++·git·算法·github·visual studio
亚历克斯神1 分钟前
Spring Cloud 2026 架构演进
java·spring·微服务
七夜zippoe4 分钟前
Spring Cloud与Dubbo架构哲学对决
java·spring cloud·架构·dubbo·配置中心
海派程序猿5 分钟前
Spring Cloud Config拉取配置过慢导致服务启动延迟的优化技巧
java
阿维的博客日记16 分钟前
为什么不逃逸代表不需要锁,JIT会直接删掉锁
java
William Dawson17 分钟前
CAS的底层实现
java
ffqws_21 分钟前
Spring Boot入门:通过简单的注册功能串联Controller,Service,Mapper。(含有数据库建立,连接,及一些关键注解的讲解)
数据库·spring boot·后端
程序边界27 分钟前
行标识符机制的技术演进与实践(下)——ROWID与实战应用
后端
九英里路28 分钟前
cpp容器——string模拟实现
java·前端·数据结构·c++·算法·容器·字符串
YDS82932 分钟前
大营销平台 —— 抽奖前置规则过滤
java·spring boot·ddd