SpringBoot基于redis自定义注解实现后端接口防重复提交校验
时间:2022-12-25 15:22:05|栏目:JAVA代码|点击: 次
一、添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> <version>1.4.4.RELEASE</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.6.3</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.75</version> </dependency>
二、代码实现
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; /** * 间隔时间,单位:秒 * <p> * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据 */ @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<String, Object> 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<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 = 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<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)) { return true; } } } Map<String, Object> cacheMap = new HashMap<String, Object>(); cacheMap.put(url, nowDataMap); redisUtil.setCacheObject(cacheRepeatKey, cacheMap, intervalTime, TimeUnit.SECONDS); 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) { 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<Object, Object> redisTemplate(JedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); //初始化参数和初始化工作 redisTemplate.afterPropertiesSet(); return redisTemplate; } /** * 缓存对象集合中,缓存是以key-value形式保存的, * 当不指定缓存的key时,SpringBoot会使用keyGenerator生成Key。 */ @Override @Bean public KeyGenerator keyGenerator() { return (target, method, params) -> { StringBuilder sb = new StringBuilder(); //类名+方法名 sb.append(target.getClass().getName()); sb.append(method.getName()); for (Object obj : params) { sb.append(obj.toString()); } return sb.toString(); }; } /** * 缓存管理器 * @param connectionFactory 连接工厂 * @return cacheManager */ @Bean public CacheManager cacheManager(JedisConnectionFactory connectionFactory) { //新建一个Jackson2JsonRedis的redis存储的序列化方式 GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); //解决查询缓存转换异常的问题 // 配置序列化(解决乱码的问题) //entryTtl设置过期时间 //serializeValuesWith设置redis存储的序列化方式 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(TIME_TO_LIVE) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer)) .disableCachingNullValues(); //定义要返回的redis缓存管理对象 return RedisCacheManager.builder(connectionFactory).cacheDefaults(config).build(); } }
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 缓存的值 */ 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 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); } }
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 List<String> name){ return "保存Body成功"; } }
三、测试
param传参:
body传参:
上一篇:Spring Boot打包war jar 部署tomcat
栏 目:JAVA代码
本文标题:SpringBoot基于redis自定义注解实现后端接口防重复提交校验
本文地址:http://www.codeinn.net/misctech/222142.html