防止重复提交 最佳实践

在这里插入图片描述

文章目录

          • 一、方案评估
            • 1. 前端
            • 2. 后端方案
          • 二、代码实战
            • 2.1. 依赖
            • 2.2. yml配置
            • 2.2. 相关配置类
            • 2.3. 实体类
            • 2.4. 相关工具类
            • 2.5. 操作消息提醒
            • 2.6. 过滤器
            • 2.2. 拦截器
            • 2.7.重复提交测试
            • 2.8. 效果图

一、方案评估
1. 前端
  • 提交后屏蔽提交按钮
2. 后端方案

实现原理
1.自定义重复提交注解(noRepeatSubmit)
2.对于防止重复提交的Controller里的方法伤加上注解
3.新增Aspect切入点,为noRepeatSubmit加入切入点
4.每次提交表单时,aspect都会保存当前key到redis(设置过期时间)
5.重复提交时aspect会判断当前redis是否又该key,若有则进行拦截
在这里插入图片描述

二、代码实战
2.1. 依赖
        <!-- redis 缓存操作 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- pool 对象池 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.11.1</version></dependency><!--常用工具类 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.12.0</version></dependency><!-- JSON工具类 --><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.13.2.2</version></dependency><!-- 阿里JSON解析器 --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.80</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
2.2. yml配置
spring:# redis 配置redis:# 地址host: localhost# 端口,默认为6379port: 6379# 数据库索引database: 0# 密码password: 123456# 连接超时时间timeout: 10slettuce:pool:# 连接池中的最小空闲连接min-idle: 0# 连接池中的最大空闲连接max-idle: 8# 连接池的最大数据库连接数max-active: 8# #连接池最大阻塞等待时间(使用负值表示没有限制)max-wait: -1ms# token配置
token:# 令牌自定义标识header: Authorization# 令牌密钥secret: abcdefghijklmnopqrstuvwxyz# 令牌有效期(默认30分钟)expireTime: 30
2.2. 相关配置类

Redis使用FastJson序列化

package com.gblfy.config;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.util.Assert;import java.nio.charset.Charset;/*** Redis使用FastJson序列化** @author gblfy* @date 2022-04-05*/
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {@SuppressWarnings("unused")private ObjectMapper objectMapper = new ObjectMapper();public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;static {ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJson2JsonRedisSerializer(Class<T> clazz) {super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException {if (t == null) {return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException {if (bytes == null || bytes.length <= 0) {return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz);}public void setObjectMapper(ObjectMapper objectMapper) {Assert.notNull(objectMapper, "'objectMapper' must not be null");this.objectMapper = objectMapper;}protected JavaType getJavaType(Class<?> clazz) {return TypeFactory.defaultInstance().constructType(clazz);}
}

RedisConfig

package com.gblfy.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.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
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.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.StringRedisSerializer;/*** redis配置** @author gblfy* @date 2022-04-05*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {@Bean@SuppressWarnings(value = {"unchecked", "rawtypes"})public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);ObjectMapper mapper = new ObjectMapper();mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);serializer.setObjectMapper(mapper);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}@Beanpublic DefaultRedisScript<Long> limitScript() {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(limitScriptText());redisScript.setResultType(Long.class);return redisScript;}/*** 限流脚本*/private String limitScriptText() {return "local key = KEYS[1]\n" +"local count = tonumber(ARGV[1])\n" +"local time = tonumber(ARGV[2])\n" +"local current = redis.call('get', key);\n" +"if current and tonumber(current) > count then\n" +"    return tonumber(current);\n" +"end\n" +"current = redis.call('incr', key)\n" +"if tonumber(current) == 1 then\n" +"    redis.call('expire', key, time)\n" +"end\n" +"return tonumber(current);";}
}

Filter配置

package com.gblfy.config;import com.gblfy.filter.RepeatableFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** Filter配置** @author gblfy* @date 2022-04-05*/
@Configuration
public class FilterConfig {@SuppressWarnings({"rawtypes", "unchecked"})@Beanpublic FilterRegistrationBean someFilterRegistration() {FilterRegistrationBean registration = new FilterRegistrationBean();registration.setFilter(new RepeatableFilter());registration.addUrlPatterns("/*");registration.setName("repeatableFilter");registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);return registration;}}

mvc通用配置

package com.gblfy.config;import com.gblfy.interceptor.RepeatSubmitInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** 通用配置** @author gblfy* @date 2022-04-05*/
@Configuration
public class ResourcesConfig implements WebMvcConfigurer {@Autowiredprivate RepeatSubmitInterceptor repeatSubmitInterceptor;@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {}/*** 自定义拦截规则*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");}/*** 跨域配置*/@Beanpublic CorsFilter corsFilter() {CorsConfiguration config = new CorsConfiguration();config.setAllowCredentials(true);// 设置访问源地址config.addAllowedOriginPattern("*");// 设置访问源请求头config.addAllowedHeader("*");// 设置访问源请求方法config.addAllowedMethod("*");// 有效期 1800秒config.setMaxAge(1800L);// 添加映射路径,拦截一切请求UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", config);// 返回新的CorsFilterreturn new CorsFilter(source);}
}
2.3. 实体类
package com.gblfy.entity;import lombok.Data;/*** 测试实体类** @Author gblfy* @Date 2022-04-05 20:51**/
@Data
public class User {private int age;private String name;
}
2.4. 相关工具类

通用http工具封装

package com.gblfy.utils.html;import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import javax.servlet.ServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;/*** 通用http工具封装** @author gblfy* @date 2022-04-05*/
public class HttpHelper {private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class);public static String getBodyString(ServletRequest request) {StringBuilder sb = new StringBuilder();BufferedReader reader = null;try (InputStream inputStream = request.getInputStream()) {reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));String line = "";while ((line = reader.readLine()) != null) {sb.append(line);}} catch (IOException e) {LOGGER.warn("getBodyString出现问题!");} finally {if (reader != null) {try {reader.close();} catch (IOException e) {LOGGER.error(ExceptionUtils.getMessage(e));}}}return sb.toString();}
}

spring redis 工具类

package com.gblfy.utils;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 工具类** @author gblfy* @date 2022-04-05**/
@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisCache {@Autowiredpublic RedisTemplate redisTemplate;/*** 缓存基本的对象,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 缓存键值* @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 long deleteObject(final Collection collection) {return redisTemplate.delete(collection);}/*** 缓存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* @param hKey*/public void delCacheMapValue(final String key, final String hKey) {HashOperations hashOperations = redisTemplate.opsForHash();hashOperations.delete(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);}/*** 获得缓存的基本对象列表** @param pattern 字符串前缀* @return 对象列表*/public Collection<String> keys(final String pattern) {return redisTemplate.keys(pattern);}
}

客户端工具类

package com.gblfy.utils;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 客户端工具类** @author gblfy* @date 2022-04-05*/
public class ServletUtils {/*** 将字符串渲染到客户端** @param response 渲染对象* @param string   待渲染的字符串*/public static void renderString(HttpServletResponse response, String string) {try {response.setStatus(200);response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().print(string);} catch (IOException e) {e.printStackTrace();}}
}

字符串工具类

package com.gblfy.utils;/*** 字符串工具类** @author gblfy* @date 2022-04-05*/
public class StringUtils extends org.apache.commons.lang3.StringUtils {/*** * 判断一个对象是否非空** @param object Object* @return true:非空 false:空*/public static boolean isNotNull(Object object) {return !isNull(object);}/*** * 判断一个对象是否为空** @param object Object* @return true:为空 false:非空*/public static boolean isNull(Object object) {return object == null;}
}
2.5. 操作消息提醒
package com.gblfy.result;import com.gblfy.utils.StringUtils;import java.util.HashMap;/*** 操作消息提醒** @author gblfy* @date 2022-04-05*/
public class AjaxResult extends HashMap<String, Object> {private static final long serialVersionUID = 1L;/*** 状态码*/public static final String CODE_TAG = "code";/*** 返回内容*/public static final String MSG_TAG = "msg";/*** 数据对象*/public static final String DATA_TAG = "data";/*** 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。*/public AjaxResult() {}/*** 初始化一个新创建的 AjaxResult 对象** @param code 状态码* @param msg  返回内容*/public AjaxResult(int code, String msg) {super.put(CODE_TAG, code);super.put(MSG_TAG, msg);}/*** 初始化一个新创建的 AjaxResult 对象** @param code 状态码* @param msg  返回内容* @param data 数据对象*/public AjaxResult(int code, String msg, Object data) {super.put(CODE_TAG, code);super.put(MSG_TAG, msg);if (StringUtils.isNotNull(data)) {super.put(DATA_TAG, data);}}/*** 返回成功消息** @return 成功消息*/public static AjaxResult success() {return AjaxResult.success("操作成功");}/*** 返回成功数据** @return 成功消息*/public static AjaxResult success(Object data) {return AjaxResult.success("操作成功", data);}/*** 返回成功消息** @param msg 返回内容* @return 成功消息*/public static AjaxResult success(String msg) {return AjaxResult.success(msg, null);}/*** 返回成功消息** @param msg  返回内容* @param data 数据对象* @return 成功消息*/public static AjaxResult success(String msg, Object data) {return new AjaxResult(200, msg, data);}/*** 返回错误消息** @return*/public static AjaxResult error() {return AjaxResult.error("操作失败");}/*** 返回错误消息** @param msg 返回内容* @return 警告消息*/public static AjaxResult error(String msg) {return AjaxResult.error(msg, null);}/*** 返回错误消息** @param msg  返回内容* @param data 数据对象* @return 警告消息*/public static AjaxResult error(String msg, Object data) {return new AjaxResult(500, msg, data);}/*** 返回错误消息** @param code 状态码* @param msg  返回内容* @return 警告消息*/public static AjaxResult error(int code, String msg) {return new AjaxResult(code, msg, null);}/*** 方便链式调用** @param key   键* @param value 值* @return 数据对象*/@Overridepublic AjaxResult put(String key, Object value) {super.put(key, value);return this;}
}
2.6. 过滤器

Repeatable 过滤器

package com.gblfy.filter;import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;/*** Repeatable 过滤器** @author gblfy* @date 2022-04-05*/
public class RepeatableFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {ServletRequest requestWrapper = null;if (request instanceof HttpServletRequest&& StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);}if (null == requestWrapper) {chain.doFilter(request, response);} else {chain.doFilter(requestWrapper, response);}}@Overridepublic void destroy() {}
}

构建可重复读取inputStream的request

package com.gblfy.filter;import com.gblfy.utils.html.HttpHelper;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;/*** 构建可重复读取inputStream的request** @author gblfy* @date 2022-04-05*/
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {private final byte[] body;public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {super(request);request.setCharacterEncoding("UTF-8");response.setCharacterEncoding("UTF-8");body = HttpHelper.getBodyString(request).getBytes("UTF-8");}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException {final ByteArrayInputStream bais = new ByteArrayInputStream(body);return new ServletInputStream() {@Overridepublic int read() throws IOException {return bais.read();}@Overridepublic int available() throws IOException {return body.length;}@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener readListener) {}};}
}
2.2. 拦截器

防止重复提交拦截器

package com.gblfy.interceptor;import com.alibaba.fastjson.JSONObject;
import com.gblfy.annotation.RepeatSubmit;
import com.gblfy.result.AjaxResult;
import com.gblfy.utils.ServletUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;/*** 防止重复提交拦截器** @author gblfy* @date 2022-04-05*/
@Component
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);if (annotation != null) {if (this.isRepeatSubmit(request, annotation)) {AjaxResult ajaxResult = AjaxResult.error(annotation.message());ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));return false;}}return true;} else {return true;}}/*** 验证是否重复提交由子类实现具体的防重复提交的规则** @param request* @return* @throws Exception*/public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
}
package com.gblfy.interceptor.impl;import com.alibaba.fastjson.JSONObject;
import com.gblfy.annotation.RepeatSubmit;
import com.gblfy.filter.RepeatedlyRequestWrapper;
import com.gblfy.interceptor.RepeatSubmitInterceptor;
import com.gblfy.utils.html.HttpHelper;
import com.gblfy.utils.RedisCache;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** 判断请求url和数据是否和上一次相同,* 如果和上次相同,则是重复提交表单。 有效时间为10秒内。** @author gblfy* @date 2022-04-05*/
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor {public final String REPEAT_PARAMS = "repeatParams";public final String REPEAT_TIME = "repeatTime";/*** 防重提交 redis key*/public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";// 令牌自定义标识@Value("${token.header}")private String header;@Autowiredprivate RedisCache redisCache;@SuppressWarnings("unchecked")@Overridepublic boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) {String nowParams = "";if (request instanceof RepeatedlyRequestWrapper) {RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;nowParams = HttpHelper.getBodyString(repeatedlyRequest);}// body参数为空,获取Parameter的数据if (StringUtils.isEmpty(nowParams)) {nowParams = JSONObject.toJSONString(request.getParameterMap());}Map<String, Object> nowDataMap = new HashMap<String, Object>();nowDataMap.put(REPEAT_PARAMS, nowParams);nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());// 请求地址(作为存放cache的key值)String url = request.getRequestURI();// 唯一值(没有消息头则使用请求地址)String submitKey = StringUtils.trimToEmpty(request.getHeader(header));// 唯一标识(指定key + url + 消息头)String cacheRepeatKey = REPEAT_SUBMIT_KEY + url + submitKey;Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);if (sessionObj != null) {Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;if (sessionMap.containsKey(url)) {Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())) {return true;}}}Map<String, Object> cacheMap = new HashMap<String, Object>();cacheMap.put(url, nowDataMap);redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);return false;}/*** 判断参数是否相同*/private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) {String nowParams = (String) nowMap.get(REPEAT_PARAMS);String preParams = (String) preMap.get(REPEAT_PARAMS);return nowParams.equals(preParams);}/*** 判断两次间隔时间*/private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval) {long time1 = (Long) nowMap.get(REPEAT_TIME);long time2 = (Long) preMap.get(REPEAT_TIME);if ((time1 - time2) < interval) {return true;}return false;}
}
2.7.重复提交测试
package com.gblfy.controller;import com.gblfy.annotation.RepeatSubmit;
import com.gblfy.entity.User;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;/*** 重复提交测试** @Author gblfy* @Date 2022-04-05 20:51**/
@RestController
public class RepeatSubmitController {@PostMapping("/save")@RepeatSubmitpublic String save(@RequestBody User user) {return "插入数据库成功";}
}
2.8. 效果图

在这里插入图片描述

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

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

相关文章

java实现 - 树的层序遍历

树&#xff1a; 树实体结构&#xff1a; Data public class Tree {//树的data值private String dataStr;//树的第一个孩子节点private Tree firstChild;//树的下一个孩子节点private Tree nextBrother; }代码实现&#xff1a; public class TreeTraversal {//队列&#xff…

金山云发布全新Serverless产品 云原生基础设施再升级

随着企业数字化转型的深入&#xff0c;云计算正全面步入2.0时代&#xff0c;即为云而生的阶段。以云原生为代表的理念&#xff0c;已经成为进一步释放云计算价值的核心推动力。 1月6日&#xff0c;金山云举行了云原生媒体沟通会&#xff0c;金山云副总裁、合伙人钱一峰在会上正…

如何提升微服务的幸福感

前言 随着微服务的流行&#xff0c;越来越多公司使用了微服务框架&#xff0c;微服务以其的高内聚、低耦合等特性&#xff0c;提供了更好的容错性&#xff0c;也更适应业务的快速迭代&#xff0c;为开发人员带来了很多的便利性。但是随着业务的发展&#xff0c;微服务拆分越来…

nacos未授权访问漏洞【原理扫描】

解决方案 vim /nacos/conf/application.properties添加 #开启认证配置 nacos.core.auth.enabledtrue

求AOE图的 拓扑排序 及关键路径长度(java实现)

文章目录1.AOE图&#xff1a;2.AOE图邻接链表存储结构&#xff1a;3.代码实现3.1.实体及参数初始化3.2.代码实现3.3.输出1.AOE图&#xff1a; 2.AOE图邻接链表存储结构&#xff1a; 3.代码实现 3.1.实体及参数初始化 //邻接表的链表节点 Data public class LinkedNode {//邻接…

陈旸:清华博士的模型信仰

云栖号资讯&#xff1a;【点击查看更多行业资讯】 在这里您可以找到不同行业的第一手的上云资讯&#xff0c;还在等什么&#xff0c;快来&#xff01; 简介&#xff1a; 陈旸是典型的天才学霸。10岁开始编程&#xff0c;亚洲奖、国奖拿到手软&#xff1b;创业做新媒体&#xff…

创业公司用 Serverless,到底香不香?

来源 | Serverless责编 | 晋兆雨头图 | 付费下载于视觉中国在过去的 5 年里&#xff0c;使用云厂商处理应用后台的流行程度大幅飙升。其一&#xff0c;初创企业主采用 Serverless 方式&#xff0c;以节省基础设施成本&#xff0c;并随用随付。随着公司规模的扩大&#xff0c;依…

Too many files with unapproved license: 2 See RAT report

解决方案 mvn -Prelease-nacos -Dmaven.test.skiptrue -Dpmd.skiptrue -Dcheckstyle.skiptrue -Drat.numUnapprovedLicenses100 clean install -U或者 mvn -Prelease-nacos -Dmaven.test.skiptrue -Drat.numUnapprovedLicenses100 clean install -U

高速公路智能化转型,阿里云高速云控平台如何赋能?

目前我国高速公路通车里程位居世界第一&#xff0c;但"高速路不高速"却时常发生&#xff0c;每逢出行高峰期&#xff0c;高速公路的拥堵状况会愈发严重。我国高速出行主要面临的痛点是安全和拥堵&#xff0c;主要是由路网利用不均衡、数据价值挖掘不够、协同管理平台…

2021 云原生开门红,金山云发布全新云原生全景图

据云原生计算基金会&#xff08;CNCF&#xff09;数据显示&#xff0c;当前企业已经在广泛使用云原生技术&#xff0c;容器应用已成常态&#xff0c; 2019 年 84&#xff05; 的公司在生产中使用容器&#xff0c;而 2016 年仅为 3&#xff05;。据阿里达摩院最新2021年科技趋势…

阿里云峰会 | 深化城市计算场景能力,为企业数智化建设提供助推力

在2020阿里云峰会上&#xff0c;阿里云边缘计算技术负责人杨敬宇表示&#xff1a;边缘计算将成为企业数智化进程中重要助推力&#xff0c;而构建城市计算是阿里云边缘计算的核心方向。在会上&#xff0c;杨敬宇还首次公开了智慧高速、云游戏、驾驶辅助等基于城市场景&#xff0…

nacos 适配达梦、人大金仓数据库

文章目录一、准备工作1. 阅读官网文档2. 下载源码&#xff0c;按官网更详细3. 下载达梦、人大金仓数据库驱动二、修改nacos源码2.1. 引入驱动依赖2.2. 引用数据库2.3. 修改配置2.4. 添加属性2.5. 指定驱动名称三、构建3.1. 进入源代码目录3.2. 执行构建3.3. 查看构建包3.4. 最后…

阿里云峰会 | 高并发扛不住、复杂查询慢、数据存不下?

阿里云峰会直播地址 2020年6月9日&#xff0c;“全速重构”2020阿里云线上峰会即将隆重召开。在此次峰会上&#xff0c;阿里云数据库重磅发布云原生分布式数据库 PolarDB-X 、云原生数据仓库AnalyticDB、数据库自治服务DAS、云数据库专属集群、图数据库GDB、云数据库Cassandra版…

软件设计师 - 超键、无损连接、函数依赖

1.闭包 在函数依赖集F下由α函数确定的所有属性的集合为F下α的闭包&#xff0c;记为α 。 闭包算法&#xff1a; result:α; while(result发生变化)dofor each 函数依赖β→γ in F dobeginif β∈result then result:result∪γ;end2.超键 方法一&#xff1a;函数依赖集F下…

赛题解析|初赛赛道三:服务网格控制面分治体系构建

首届云原生编程挑战赛正在报名中&#xff0c;初赛共有三个赛道&#xff0c;题目如下&#xff1a; 赛道一&#xff1a;实现一个分布式统计和过滤的链路追踪 赛道二&#xff1a;实现规模化容器静态布局和动态迁移 赛道三&#xff1a;服务网格控制面分治体系构建 立即报名&#…

使用 SQL 语句实现一个年会抽奖程序

作者 | 董旭阳 责编 | 张文头图 | CSDN 下载自视觉中国出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;年关将近&#xff0c;抽奖想必是大家在公司年会上最期待的活动了。如果老板让你做一个年会抽奖的程序&#xff0c;你会怎么实现呢&#xff1f;今天给大家介绍一…

杨飞:擅长顺势而为,收获家业两成

云栖号资讯&#xff1a;【点击查看更多行业资讯】 在这里您可以找到不同行业的第一手的上云资讯&#xff0c;还在等什么&#xff0c;快来&#xff01; 简介&#xff1a; 对比大多数开发者来说&#xff0c;杨飞的职业路线可以说是大相径庭。从大厂到创业公司&#xff0c;从一线城…

Springboot 下 EasyExcel 的数据导入导出

文章目录1.环境准备1.0. excel数据1.1. pom1.2. excle映射实体1.3. 自定义日期转换器1.4.自定义异常2. 数据导出3. 数据导入3.1. excel解析监听类3.2. excel导入1.环境准备 1.0. excel数据 1.1. pom <dependency><groupId>org.springframework.boot</groupId&g…

springboot spring-cloud spring-cloud nacos 整合模板

文章目录二、coding实战2.1. 版本对照2.2. 线上采用版本2.3. yml文件配置2.4. pom依赖2.5. 效果图二、coding实战 2.1. 版本对照 先阅读->版本说明 2.2. 线上采用版本 Spring Cloud Alibaba VersionSpring Cloud VersionSpring Boot VersionNacos Version2.2.7.RELEAS…

CSDN居然免费送会员? 赶紧来领!

距离春节还有不到一个月你准备好给家人的春节礼物了吗&#xff1f;疫情下&#xff0c;为了让程序猿同学开心加班小编提前准备了一份牛年大礼 周五福利日&#xff0c;人人都可免费领会员&#xff01;助你提前实现CSDN会员卡自由&#xff01;奖品多多&#xff0c;不仅有CSDN月卡会…