当前位置: 代码网 > it编程>编程语言>Java > 自定义注解SpringBoot防重复提交AOP方法详解

自定义注解SpringBoot防重复提交AOP方法详解

2025年12月11日 Java 我要评论
防重复提交流程获取到当前的 httpservletrequest 对象,并记录请求的地址、请求方式、拦截到的类名和方法名等信息。通过 pjp.getargs() 获取请求参数,并将参数转换成字符串,用

防重复提交流程

获取到当前的 httpservletrequest 对象,并记录请求的地址、请求方式、拦截到的类名和方法名等信息。

通过 pjp.getargs() 获取请求参数,并将参数转换成字符串,用于生成唯一标识。

根据请求的地址、参数、唯一标识等信息生成缓存键 cacherepeatkey,用于作为重复提交判断的依据。

通过 pjp.getsignature().getmethod()method.getannotation(preventrepeatsubmit.class) 获取被拦截的方法上的 preventrepeatsubmit 注解,进而获取注解中配置的有效期时间。

使用 redis 分布式锁来判断请求是否重复提交。调用 rediscache.setnxcacheobject() 方法,尝试向缓存中设置键值对,如果设置成功(返回值为 true),则证明没有重复提交。若设置失败(返回值为 false),则抛出 businessexception 异常,表示重复提交。

如果没有重复提交,则执行目标方法,即 pjp.proceed(),并将其返回。

引入依赖

<!--切面-->
<dependency>
   <groupid>org.springframework.boot</groupid>
   <artifactid>spring-boot-starter-aop</artifactid>
</dependency>
<dependency>
   <groupid>com.baomidou</groupid>
   <artifactid>mybatis-plus-boot-starter</artifactid>
   <version>3.5.1</version>
</dependency>
<dependency>
   <groupid>org.springframework.boot</groupid>
   <artifactid>spring-boot-starter-data-redis</artifactid>
</dependency>
<!--rabbitmq 测试依赖-->
<dependency>
   <groupid>org.springframework.amqp</groupid>
   <artifactid>spring-rabbit-test</artifactid>
   <scope>test</scope>
</dependency>
<!-- 数据库-->
<dependency>
    <groupid>mysql</groupid>
    <artifactid>mysql-connector-java</artifactid>
    <version>${mysql.version}</version>
</dependency>
<dependency>
    <groupid>com.alibaba</groupid>
    <artifactid>fastjson</artifactid>
    <version>1.2.47</version>
</dependency>

properties配置

server.port=7125
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/itcast?servertimezone=gmt%2b8&useunicode=true&characterencoding=utf8&autoreconnect=true&allowmultiqueries=true
spring.datasource.username=root
spring.datasource.password=root123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.driver
spring.datasource.hikari.pool-name=hikaricpdatasource
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=180000
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=select 1
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.timeout=10s
spring.redis.password=123
token.header=token

自定义注解

import java.lang.annotation.*;

/**
 * 自定义注解防止表单重复提交
 *
 */
@inherited
@target(elementtype.method)
@retention(retentionpolicy.runtime)
@documented
public @interface preventrepeatsubmit
{
    /**
     * 间隔时间(ms),小于此时间视为重复提交
     */
    public int interval() default 40;

    /**
     * 提示消息
     */
    public string message() default "不允许重复提交,请稍候再试";
}

切面

import com.alibaba.fastjson.json;
import com.example.demo.annotation.preventrepeatsubmit;
import com.example.demo.exception.businessexception;
import com.example.demo.util.httpcodeenum;
import com.example.demo.util.rediscache;
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.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.beans.factory.annotation.value;
import org.springframework.stereotype.component;
import org.springframework.web.context.request.requestcontextholder;
import org.springframework.web.context.request.servletrequestattributes;

import javax.servlet.http.httpservletrequest;
import java.lang.reflect.method;
import java.util.concurrent.timeunit;

@aspect
@component
public class preventrepeatsubmitaspect {
    private static final logger log = loggerfactory.getlogger(preventrepeatsubmitaspect.class);

    // 令牌自定义标识
    @value("${token.header}")
    private string header;

    @autowired
    private rediscache rediscache;

    // 定义一个切入点
    @pointcut("@annotation(com.example.demo.annotation.preventrepeatsubmit)")
    public void preventrepeatsubmit() {

    }

    @around("preventrepeatsubmit()")
    public object checkprs(proceedingjoinpoint pjp) throws throwable {
        log.info("进入preventrepeatsubmit切面");
        //得到request对象
        httpservletrequest request = ((servletrequestattributes) requestcontextholder.getrequestattributes()).getrequest();
        string requesturi = request.getrequesturi();
        log.info("防重复提交的请求地址:{} ,请求方式:{}",requesturi,request.getmethod());
        log.info("防重复提交拦截到的类名:{} ,方法:{}",pjp.gettarget().getclass().getsimplename(),pjp.getsignature().getname());

        //获取请求参数
        object[] args = pjp.getargs();
        string argstr = json.tojsonstring(args);
        //这里替换是为了在redis可视化工具中方便查看
        argstr=argstr.replace(":","#");
        // 唯一值(没有消息头则使用请求地址)
        string submitkey = request.getheader(header).trim();
        // 唯一标识(指定key + url +参数+token)
        string cacherepeatkey = "repeat_submit:" + requesturi+":" +argstr+":"+ submitkey;
        methodsignature ms = (methodsignature) pjp.getsignature();
        method method=ms.getmethod();
        preventrepeatsubmit preventrepeatsubmit=method.getannotation(preventrepeatsubmit.class);
        int interval = preventrepeatsubmit.interval();
        log.info("获取到preventrepeatsubmit的有效期时间"+interval);
        //redis分布式锁
        boolean aboolean = rediscache.setnxcacheobject(cacherepeatkey, 1, preventrepeatsubmit.interval(), timeunit.seconds);
        //aboolean为true则证明没有重复提交
        if(!aboolean){
            //json.tojsonstring(responseresult.errorresult(httpcodeenum.system_error.getcode(),annotation.message())));
            throw new businessexception(httpcodeenum.repeate_error);
        }
        return pjp.proceed();
    }

}

redis工具类

import org.springframework.beans.factory.annotation.autowired;
import org.springframework.data.redis.core.boundsetoperations;
import org.springframework.data.redis.core.hashoperations;
import org.springframework.data.redis.core.redistemplate;
import org.springframework.data.redis.core.valueoperations;
import org.springframework.stereotype.component;

import java.util.*;
import java.util.concurrent.timeunit;

/**
 * spring redis 工具类
 *
 **/
@component
public class rediscache
{
    @autowired
    public redistemplate redistemplate;

    //添加分布式锁
    public <t> boolean setnxcacheobject(final string key, final t value,long lt,timeunit tu)
    {
        return redistemplate.opsforvalue().setifabsent(key,value,lt,tu);
    }

    /**
     * 缓存基本的对象,integer、string、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <t> void setcacheobject(final string key, final t value)
    {
        redistemplate.opsforvalue().set(key, value);
    }

    /**
     * 缓存基本的对象,integer、string、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeunit 时间颗粒度
     */
    public <t> void setcacheobject(final string key, final t value, final integer timeout, final timeunit timeunit)
    {
        redistemplate.opsforvalue().set(key, value, timeout, timeunit);
    }

    /**
     * 设置有效时间
     *
     * @param key redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final string key, final long timeout)
    {
        return expire(key, timeout, timeunit.seconds);
    }

    /**
     * 设置有效时间
     *
     * @param key redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final string key, final long timeout, final timeunit unit)
    {
        return redistemplate.expire(key, timeout, unit);
    }

    /**
     * 获取有效时间
     *
     * @param key redis键
     * @return 有效时间
     */
    public long getexpire(final string key)
    {
        return redistemplate.getexpire(key);
    }

    /**
     * 判断 key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean haskey(string key)
    {
        return redistemplate.haskey(key);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <t> t getcacheobject(final string key)
    {
        valueoperations<string, t> operation = redistemplate.opsforvalue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteobject(final string key)
    {
        return redistemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public boolean deleteobject(final collection collection)
    {
        return redistemplate.delete(collection) > 0;
    }

    /**
     * 缓存list数据
     *
     * @param key 缓存的键值
     * @param datalist 待缓存的list数据
     * @return 缓存的对象
     */
    public <t> long setcachelist(final string key, final list<t> datalist)
    {
        long count = redistemplate.opsforlist().rightpushall(key, datalist);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <t> list<t> getcachelist(final string key)
    {
        return redistemplate.opsforlist().range(key, 0, -1);
    }

    /**
     * 缓存set
     *
     * @param key 缓存键值
     * @param dataset 缓存的数据
     * @return 缓存数据的对象
     */
    public <t> boundsetoperations<string, t> setcacheset(final string key, final set<t> dataset)
    {
        boundsetoperations<string, t> setoperation = redistemplate.boundsetops(key);
        iterator<t> it = dataset.iterator();
        while (it.hasnext())
        {
            setoperation.add(it.next());
        }
        return setoperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <t> set<t> getcacheset(final string key)
    {
        return redistemplate.opsforset().members(key);
    }

    /**
     * 缓存map
     *
     * @param key
     * @param datamap
     */
    public <t> void setcachemap(final string key, final map<string, t> datamap)
    {
        if (datamap != null) {
            redistemplate.opsforhash().putall(key, datamap);
        }
    }

    /**
     * 获得缓存的map
     *
     * @param key
     * @return
     */
    public <t> map<string, t> getcachemap(final string key)
    {
        return redistemplate.opsforhash().entries(key);
    }

    /**
     * 往hash中存入数据
     *
     * @param key redis键
     * @param hkey hash键
     * @param value 值
     */
    public <t> void setcachemapvalue(final string key, final string hkey, final t value)
    {
        redistemplate.opsforhash().put(key, hkey, value);
    }

    /**
     * 获取hash中的数据
     *
     * @param key redis键
     * @param hkey hash键
     * @return hash中的对象
     */
    public <t> t getcachemapvalue(final string key, final string hkey)
    {
        hashoperations<string, string, t> opsforhash = redistemplate.opsforhash();
        return opsforhash.get(key, hkey);
    }

    /**
     * 获取多个hash中的数据
     *
     * @param key redis键
     * @param hkeys hash键集合
     * @return hash对象集合
     */
    public <t> list<t> getmulticachemapvalue(final string key, final collection<object> hkeys)
    {
        return redistemplate.opsforhash().multiget(key, hkeys);
    }

    /**
     * 删除hash中的某条数据
     *
     * @param key redis键
     * @param hkey hash键
     * @return 是否成功
     */
    public boolean deletecachemapvalue(final string key, final string hkey)
    {
        return redistemplate.opsforhash().delete(key, hkey) > 0;
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public collection<string> keys(final string pattern)
    {
        return redistemplate.keys(pattern);
    }
}

controller

import com.example.demo.annotation.preventrepeatsubmit;
import com.example.demo.mapper.stumapper;
import com.example.demo.model.responseresult;
import com.example.demo.model.student;
import org.springframework.validation.annotation.validated;
import org.springframework.web.bind.annotation.*;

import javax.annotation.resource;


@restcontroller
@requestmapping("/test")
@validated
public class testcontroller {

    @resource
    private stumapper stumapper;

    @postmapping("/user")
    @preventrepeatsubmit
    public responseresult<string> user(@requestbody student student) {
        stumapper.insert(student);
        return new responseresult<>("插入成功");
    }

//    @postmapping("/user/{name}/{age}")
//    @preventrepeatsubmit
//    public responseresult<string> user( @pathvariable string name ,@pathvariable integer age) {
//        student student = new student(name,age);
//        stumapper.insert(student);
//        return new responseresult<>("插入成功");
//    }
}

测试

发送请求的请求头

请求体

第一次插入

第二次插入失败

redis缓存

改用restful风格

第一次请求

结果

第二次发起请求失败

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com