一、添加依赖
org.springframework.boot spring-boot-starter-redis 1.4.4.RELEASE redis.clients jedis 3.6.3 com.alibaba fastjson 1.2.75
二、代码实现
1.构建可重复读取inputStream的request
package com.hl.springbootrepetitionsubmit.servlet; import javax.servlet.*; import javax.servlet.http.*; import java.io.*; import java.nio.charset.StandardCharsets; /** * 构建可重复读取inputStream的request */ public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper { private final String body; public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException { super(request); request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); body = getBodyString(request); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } public String getBody() { return body; } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); return new ServletInputStream() { @Override public int read() throws IOException { return bais.read(); } @Override public int available() throws IOException { return body.length(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; } /** * 获取Request请求body内容 * * @param request * @return */ private 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) { e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } return sb.toString(); } }
2.重写request
package com.hl.springbootrepetitionsubmit.filter; import com.hl.springbootrepetitionsubmit.servlet.RepeatedlyRequestWrapper; import org.springframework.http.MediaType; import org.springframework.util.StringUtils; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * 过滤器(重写request) */ public class RepeatableFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletRequest requestWrapper = null; //将原生的request变为可重复读取inputStream的request 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); } } }
3.自定义注解
/** * 自定义注解防止表单重复提交 */ @Inherited @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RepeatSubmit { }
4.创建防止重复提交拦截器
package com.hl.springbootrepetitionsubmit.interceptor; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSONObject; import com.hl.springbootrepetitionsubmit.annotation.RepeatSubmit; import com.hl.springbootrepetitionsubmit.config.RedisUtil; import com.hl.springbootrepetitionsubmit.servlet.RepeatedlyRequestWrapper; import org.springframework.beans.factory.annotation.*; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.*; import java.io.IOException; import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.TimeUnit; /** * 防止重复提交拦截器 */ @Component public class RepeatSubmitInterceptor implements HandlerInterceptor { public final String REPEAT_PARAMS = "repeatParams"; public final String REPEAT_TIME = "repeatTime"; /** * 防重提交 redis key */ public final String REPEAT_SUBMIT_KEY = "repeat_submit:"; // 令牌自定义标识 @Value("${token.header}") private String header; @Autowired private RedisUtil redisUtil; /** * 间隔时间,单位:秒 ** 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据 */ @Value("${repeatSubmit.intervalTime}") private int intervalTime; @Override public 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)) { //返回重复提交提示 Map
resultMap = new HashMap<>(); resultMap.put("code", "500"); resultMap.put("msg", request.getRequestURI() + "不允许重复提交,请稍后再试"); try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(JSONObject.toJSONString(resultMap)); } catch (IOException e) { e.printStackTrace(); } return false; } } return true; } else { return preHandle(request, response, handler); } } /** * 验证是否重复提交由子类实现具体的防重复提交的规则 */ public boolean isRepeatSubmit(HttpServletRequest request) { String nowParams = ""; if (request instanceof RepeatedlyRequestWrapper) { RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; nowParams = repeatedlyRequest.getBody(); } // body参数为空,获取Parameter的数据 if (StrUtil.isBlank(nowParams)) { nowParams = JSONObject.toJSONString(request.getParameterMap()); } Map nowDataMap = new HashMap (); nowDataMap.put(REPEAT_PARAMS, nowParams); nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); // 请求地址(作为存放cache的key值) String url = request.getRequestURI(); // 唯一值(没有消息头则使用请求地址) String submitKey = request.getHeader(header); if (StrUtil.isBlank(submitKey)) { submitKey = url; } // 唯一标识(指定key + 消息头) String cacheRepeatKey = REPEAT_SUBMIT_KEY + submitKey; Object sessionObj = redisUtil.getCacheObject(cacheRepeatKey); if (sessionObj != null) { Map sessionMap = (Map ) sessionObj; if (sessionMap.containsKey(url)) { Map preDataMap = (Map ) sessionMap.get(url); if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap)) { return true; } } } Map cacheMap = new HashMap (); cacheMap.put(url, nowDataMap); redisUtil.setCacheObject(cacheRepeatKey, cacheMap, intervalTime, TimeUnit.SECONDS); return false; } /** * 判断参数是否相同 */ private boolean compareParams(Map nowMap, Map preMap) { String nowParams = (String) nowMap.get(REPEAT_PARAMS); String preParams = (String) preMap.get(REPEAT_PARAMS); return nowParams.equals(preParams); } /** * 判断两次间隔时间 */ private boolean compareTime(Map nowMap, Map preMap) { long time1 = (Long) nowMap.get(REPEAT_TIME); long time2 = (Long) preMap.get(REPEAT_TIME); if ((time1 - time2) < (this.intervalTime * 1000L)) { return true; } return false; } }
5.redis配置
package com.hl.springbootrepetitionsubmit.config; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import java.time.Duration; @Configuration public class RedisConfig extends CachingConfigurerSupport { //配置redis的过期时间 private static final Duration TIME_TO_LIVE = Duration.ZERO; @Bean(name = "jedisPoolConfig") public JedisPoolConfig jedisPoolConfig() { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); //控制一个pool可分配多少个jedis实例 jedisPoolConfig.setMaxTotal(500); //最大空闲数 jedisPoolConfig.setMaxIdle(200); //每次释放连接的最大数目,默认是3 jedisPoolConfig.setNumTestsPerEvictionRun(1024); //逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1 jedisPoolConfig.setTimeBetweenEvictionRunsMillis(30000); //连接的最小空闲时间 默认1800000毫秒(30分钟) jedisPoolConfig.setMinEvictableIdleTimeMillis(-1); jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(10000); //最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。 jedisPoolConfig.setMaxWaitMillis(1500); jedisPoolConfig.setTestOnBorrow(true); jedisPoolConfig.setTestWhileIdle(true); jedisPoolConfig.setTestOnReturn(false); jedisPoolConfig.setJmxEnabled(true); jedisPoolConfig.setBlockWhenExhausted(false); return jedisPoolConfig; } @Bean("connectionFactory") public JedisConnectionFactory connectionFactory() { RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); redisStandaloneConfiguration.setHostName("127.0.0.1"); redisStandaloneConfiguration.setDatabase(0); // redisStandaloneConfiguration.setPassword(RedisPassword.of("123456")); redisStandaloneConfiguration.setPort(6379); //获得默认的连接池构造器 JedisClientConfiguration.JedisPoolingClientConfigurationBuilder jpcb = (JedisClientConfiguration.JedisPoolingClientConfigurationBuilder) JedisClientConfiguration.builder(); //指定jedisPoolConifig来修改默认的连接池构造器(真麻烦,滥用设计模式!) jpcb.poolConfig(jedisPoolConfig()); //通过构造器来构造jedis客户端配置 JedisClientConfiguration jedisClientConfiguration = jpcb.build(); //单机配置 + 客户端配置 = jedis连接工厂 return new JedisConnectionFactory(redisStandaloneConfiguration, jedisClientConfiguration); } @Bean("jedisPool") public JedisPool jedisPool(){ return new JedisPool(jedisPoolConfig(),"127.0.0.1",6379); } @Bean public RedisTemplate
6.redis工具类
package com.hl.springbootrepetitionsubmit.config; 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; @Component public class RedisUtil { @Autowired private RedisTemplate redisTemplate; /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 */ publicvoid setCacheObject(final String key, final T value) { redisTemplate.opsForValue().set(key, value); } /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 */ public 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 getCacheObject(final String key) { ValueOperations 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 long setCacheList(final String key, final List dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } /** * 获得缓存的list对象 * * @param key 缓存的键值 * @return 缓存键值对应的数据 */ public List getCacheList(final String key) { return redisTemplate.opsForList().range(key, 0, -1); } /** * 缓存Set * * @param key 缓存键值 * @param dataSet 缓存的数据 * @return 缓存数据的对象 */ public BoundSetOperations setCacheSet(final String key, final Set dataSet) { BoundSetOperations setOperation = redisTemplate.boundSetOps(key); Iterator it = dataSet.iterator(); while (it.hasNext()) { setOperation.add(it.next()); } return setOperation; } /** * 获得缓存的set * * @param key * @return */ public Set getCacheSet(final String key) { return redisTemplate.opsForSet().members(key); } /** * 缓存Map * * @param key * @param dataMap */ public void setCacheMap(final String key, final Map dataMap) { if (dataMap != null) { redisTemplate.opsForHash().putAll(key, dataMap); } } /** * 获得缓存的Map * * @param key * @return */ public Map getCacheMap(final String key) { return redisTemplate.opsForHash().entries(key); } /** * 往Hash中存入数据 * * @param key Redis键 * @param hKey Hash键 * @param value 值 */ public 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 getCacheMapValue(final String key, final String hKey) { HashOperations opsForHash = redisTemplate.opsForHash(); return opsForHash.get(key, hKey); } /** * 获取多个Hash中的数据 * * @param key Redis键 * @param hKeys Hash键集合 * @return Hash对象集合 */ public List getMultiCacheMapValue(final String key, final Collection hKeys) { return redisTemplate.opsForHash().multiGet(key, hKeys); } /** * 获得缓存的基本对象列表 * * @param pattern 字符串前缀 * @return 对象列表 */ public Collection keys(final String pattern) { return redisTemplate.keys(pattern); } }
7.配置拦截器
/** * 防重复提交配置 */ @Configuration public class RepeatSubmitConfig implements WebMvcConfigurer { @Autowired private RepeatSubmitInterceptor repeatSubmitInterceptor; /** * 添加防重复提交拦截 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); } /** * 生成防重复提交过滤器(重写request) * @return FilterRegistrationBean */ @SuppressWarnings({ "rawtypes", "unchecked" }) @Bean public FilterRegistrationBean> createRepeatableFilter() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new RepeatableFilter()); registration.addUrlPatterns("/*"); registration.setName("repeatableFilter"); registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); return registration; } }
8.controller
@RestController @RequestMapping("/repeatSubmit") public class RepeatSubmitController { /** * 保存Param * @param name * @return */ @RepeatSubmit @PostMapping("/saveParam/{name}") public String saveParam(@PathVariable("name")String name){ return "保存Param成功"; } /** * 保存Param * @param name * @return */ @RepeatSubmit @PostMapping("/saveBody") public String saveBody(@RequestBody Listname){ return "保存Body成功"; } }
三、测试
param传参:
body传参:
项目源码
到此这篇关于SpringBoot基于redis自定义注解实现后端接口防重复提交校验的文章就介绍到这了,更多相关SpringBoot 接口防重复提交校验内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!