注册一个网站域名一年需要多少钱,手机排行榜2022前十名,html5响应式模板,什么网站可以接单做设计方案1.项目用到,不是核心
我们干系统开发,不免要考虑一个点#xff0c;数据的重复提交。
我想我们之前如果要校验数据重复提交要求#xff0c;会怎么干?会在业务层#xff0c;对数据库操作#xff0c;查询数据是否存在,存在就禁止插入数据; 但是吧,我们每次crud操作都会连接…1.项目用到,不是核心
我们干系统开发,不免要考虑一个点数据的重复提交。
我想我们之前如果要校验数据重复提交要求会怎么干?会在业务层对数据库操作查询数据是否存在,存在就禁止插入数据; 但是吧,我们每次crud操作都会连接一次数据库,也就是占用内存,那么在项目中大量crud操作面前,我们通过这种方式来实现数据的重复提交显然不大可取。因此我们采用通过 redis 拦截器来实现防止数据重复提交。来分担数据库连接的压力。
数据重复提交有啥坏处 数据完整性如果用户在短时间内多次提交相同的表单可能会导致数据重复或产生不一致的数据。用户体验如果用户不小心重复提交了表单而系统没有进行相应的处理用户可能会收到错误或重复的信息这会影响用户体验。性能考虑大量的重复提交可能会对服务器造成不必要的负担影响系统的性能。安全考虑在某些场景下重复提交可能会被用于发起攻击如DoS攻击。 我们要考虑一个事情就是我们要验证数据的重复提交: 首先第一次提交的数据肯定是要被存储的当而第二次往后,每次提交数据都会与之前的数据产生比对从而验证数据重复提交,但是通常情况下我们不仅要对提交数据重复性校验还有前后提交时间差的校验。 下面,就有我通过redis 拦截器来实现如何防止数据重复提交。 思路: 我们对需要验证重复提交的数据加上自定义注解限制提交时间段然后在拦截器中读取第一次提交内容和时间点存储到redi中当第二次提交时会拿到新的数据和时间点与存储到redis对比。如果提交2次时间段小于限制提交时间段(拦截器拿到自定义注解的值)就算重复提交。 项目依赖
dependencies!--boot-web--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!--hutool--dependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion5.8.11/versionexclusionsexclusiongroupIdcn.hutool/groupIdartifactIdhutool-json/artifactId/exclusion/exclusions/dependency!--fastJson2--dependencygroupIdcom.alibaba.fastjson2/groupIdartifactIdfastjson2/artifactIdversion2.0.19.graal/version/dependency!--redis--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependency!--commons-pools连接池,lettuce没有内置的数据库连接池所以要用第三方的 --dependencygroupIdorg.apache.commons/groupIdartifactIdcommons-pool2/artifactId/dependency!--boot-test--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency/dependenciesdependencyManagementdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-dependencies/artifactIdversion2.6.13/versiontypepom/typescopeimport/scope/dependency/dependencies/dependencyManagement
application.yml 主要是redis的配置。 spring:# redis 配置redis:# 地址host: 192.168.233.131# 端口默认为6379port: 6379# 数据库索引database: 0# 密码password:# 连接超时时间timeout: 10slettuce:pool:# 连接池中的最小空闲连接min-idle: 0# 连接池中的最大空闲连接max-idle: 8# 连接池的最大数据库连接数max-active: 8# #连接池最大阻塞等待时间使用负值表示没有限制max-wait: -1msFastJson2JsonRedisSerializer 主要负责对存入redis的key、value进行序列化。 /*** Redis使用FastJson序列化** author jzm*/
public class FastJson2JsonRedisSerializerT implements RedisSerializerT
{public static final Charset DEFAULT_CHARSET Charset.forName(UTF-8);static final Filter AUTO_TYPE_FILTER JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);private ClassT clazz;public FastJson2JsonRedisSerializer(ClassT clazz){super();this.clazz clazz;}Overridepublic byte[] serialize(T t) throws SerializationException{if (t null){return new byte[0];}return JSON.toJSONString(t, JSONWriter.Feature.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, AUTO_TYPE_FILTER);}
}
RedisConfig redis相关配置。定义通过redisTemplate,设置到redis中的key、value的序列化方式。 /*** redis配置** author jzm*/
Configuration
EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{BeanSuppressWarnings(value {unchecked, rawtypes})// 设置key、value的序列化方式public RedisTemplateObject, Object redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplateObject, Object template new RedisTemplate();template.setConnectionFactory(connectionFactory);FastJson2JsonRedisSerializer serializer new FastJson2JsonRedisSerializer(Object.class);// 使用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 DefaultRedisScriptLong limitScript(){DefaultRedisScriptLong 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);;}
}
WebAppConfig web mvc的相关配置。这里主要是注册自定义拦截器。 /*** web 配置** author: jzm* date: 2024-01-25 11:30**/Configuration
public class WebAppConfig implements WebMvcConfigurer
{Autowiredprivate SameUrlDataInterceptor sameUrlDataInterceptor;// 注册拦截器Overridepublic void addInterceptors(InterceptorRegistry registry){// 可添加多个registry.addInterceptor(sameUrlDataInterceptor).addPathPatterns(/**);}
}
FilterConfig 过滤器配置。注册自定义过滤器。 /*** 过滤器配置** author: jzm* date: 2024-01-26 08:53**/Configuration
public class FilterConfig
{Autowiredprivate RepeatableFilter repeatableFilter;SuppressWarnings({rawtypes, unchecked})Beanpublic FilterRegistrationBean someFilterRegistration(){FilterRegistrationBean registration new FilterRegistrationBean();registration.setFilter(repeatableFilter);registration.addUrlPatterns(/*);registration.setName(repeatableFilter);registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);return registration;}
}项目用到的常量类 常量类。直接CV。 /*** 缓存的key 常量** author jzm*/
public class CacheConstants
{/*** 登录用户 redis key*/public static final String LOGIN_TOKEN_KEY login_tokens:;/*** 验证码 redis key*/public static final String CAPTCHA_CODE_KEY captcha_codes:;/*** 参数管理 cache key*/public static final String SYS_CONFIG_KEY sys_config:;/*** 字典管理 cache key*/public static final String SYS_DICT_KEY sys_dict:;/*** 防重提交 redis key*/public static final String REPEAT_SUBMIT_KEY repeat_submit:;/*** 限流 redis key*/public static final String RATE_LIMIT_KEY rate_limit:;/*** 登录账户密码错误次数 redis key*/public static final String PWD_ERR_CNT_KEY pwd_err_cnt:;
}
/*** 通用常量信息** author jzm*/
public class Constants
{/*** UTF-8 字符集*/public static final String UTF8 UTF-8;/*** GBK 字符集*/public static final String GBK GBK;/*** www主域*/public static final String WWW www.;/*** http请求*/public static final String HTTP http://;/*** https请求*/public static final String HTTPS https://;/*** 通用成功标识*/public static final String SUCCESS 0;/*** 通用失败标识*/public static final String FAIL 1;/*** 登录成功*/public static final String LOGIN_SUCCESS Success;/*** 注销*/public static final String LOGOUT Logout;/*** 注册*/public static final String REGISTER Register;/*** 登录失败*/public static final String LOGIN_FAIL Error;/*** 所有权限标识*/public static final String ALL_PERMISSION *:*:*;/*** 管理员角色权限标识*/public static final String SUPER_ADMIN admin;/*** 角色权限分隔符*/public static final String ROLE_DELIMETER ,;/*** 权限标识分隔符*/public static final String PERMISSION_DELIMETER ,;/*** 验证码有效期分钟*/public static final Integer CAPTCHA_EXPIRATION 2;/*** 令牌*/public static final String TOKEN token;/*** 令牌前缀*/public static final String TOKEN_PREFIX Bearer ;/*** 令牌前缀*/public static final String LOGIN_USER_KEY login_user_key;/*** 用户ID*/public static final String JWT_USERID userid;/*** 用户名称*/public static final String JWT_USERNAME sub;/*** 用户头像*/public static final String JWT_AVATAR avatar;/*** 创建时间*/public static final String JWT_CREATED created;/*** 用户权限*/public static final String JWT_AUTHORITIES authorities;/*** 资源映射路径 前缀*/public static final String RESOURCE_PREFIX /profile;/*** RMI 远程方法调用*/public static final String LOOKUP_RMI rmi:;/*** LDAP 远程方法调用*/public static final String LOOKUP_LDAP ldap:;/*** LDAPS 远程方法调用*/public static final String LOOKUP_LDAPS ldaps:;/*** 自动识别json对象白名单配置仅允许解析的包名范围越小越安全*/public static final String[] JSON_WHITELIST_STR {org.springframework, com.ruoyi};/*** 定时任务白名单配置仅允许访问的包名如其他需要可以自行添加*/public static final String[] JOB_WHITELIST_STR {com.ruoyi};/*** 定时任务违规的字符*/public static final String[] JOB_ERROR_STR {java.net.URL, javax.naming.InitialContext, org.yaml.snakeyaml,org.springframework, org.apache, com.ruoyi.common.utils.file, com.ruoyi.common.config};
}
/*** 返回状态码* * author jzm*/
public class HttpStatus
{/*** 操作成功*/public static final int SUCCESS 200;/*** 对象创建成功*/public static final int CREATED 201;/*** 请求已经被接受*/public static final int ACCEPTED 202;/*** 操作已经执行成功但是没有返回数据*/public static final int NO_CONTENT 204;/*** 资源已被移除*/public static final int MOVED_PERM 301;/*** 重定向*/public static final int SEE_OTHER 303;/*** 资源没有被修改*/public static final int NOT_MODIFIED 304;/*** 参数列表错误缺少格式不匹配*/public static final int BAD_REQUEST 400;/*** 未授权*/public static final int UNAUTHORIZED 401;/*** 访问受限授权过期*/public static final int FORBIDDEN 403;/*** 资源服务未找到*/public static final int NOT_FOUND 404;/*** 不允许的http方法*/public static final int BAD_METHOD 405;/*** 资源冲突或者资源被锁*/public static final int CONFLICT 409;/*** 不支持的数据媒体类型*/public static final int UNSUPPORTED_TYPE 415;/*** 系统内部错误*/public static final int ERROR 500;/*** 接口未实现*/public static final int NOT_IMPLEMENTED 501;/*** 系统警告消息*/public static final int WARN 601;
}项目用到的工具类 也是直接。CV。 RedisCache
/*** redis 工具类** author jzm**/
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 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){ValueOperationsString, 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 ListT dataList){Long count redisTemplate.opsForList().rightPushAll(key, dataList);return count null ? 0 : count;}/*** 获得缓存的list对象** param key 缓存的键值* return 缓存键值对应的数据*/public T ListT getCacheList(final String key){return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set** param key 缓存键值* param dataSet 缓存的数据* return 缓存数据的对象*/public T BoundSetOperationsString, T setCacheSet(final String key, final SetT dataSet){BoundSetOperationsString, T setOperation redisTemplate.boundSetOps(key);IteratorT it dataSet.iterator();while (it.hasNext()){setOperation.add(it.next());}return setOperation;}/*** 获得缓存的set** param key* return*/public T SetT getCacheSet(final String key){return redisTemplate.opsForSet().members(key);}/*** 缓存Map** param key* param dataMap*/public T void setCacheMap(final String key, final MapString, T dataMap){if (dataMap ! null){redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map** param key* return*/public T MapString, 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){HashOperationsString, String, T opsForHash redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 获取多个Hash中的数据** param key Redis键* param hKeys Hash键集合* return Hash对象集合*/public T ListT getMultiCacheMapValue(final String key, final CollectionObject 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 CollectionString keys(final String pattern){return redisTemplate.keys(pattern);}
}
HttpHelper 主要是为了读取http请求体的数据。 /*** 通用http工具封装** author ruoyi*/
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(Exceptions:, e.getMessage());}}}return sb.toString();}
}
StringUtils
/*** 字符串工具类** author jzm*/
public class StringUtils extends StrUtil
{/*** 空字符串*/private static final String NULLSTR ;/*** 下划线*/private static final char SEPARATOR _;/*** 获取参数不为空值** param value defaultValue 要判断的value* return value 返回值*/public static T T nvl(T value, T defaultValue){return value ! null ? value : defaultValue;}/*** * 判断一个Collection是否为空 包含ListSetQueue** param coll 要判断的Collection* return true为空 false非空*/public static boolean isEmpty(Collection? coll){return isNull(coll) || coll.isEmpty();}/*** * 判断一个Collection是否非空包含ListSetQueue** param coll 要判断的Collection* return true非空 false空*/public static boolean isNotEmpty(Collection? coll){return !isEmpty(coll);}/*** * 判断一个对象数组是否为空** param objects 要判断的对象数组* * return true为空 false非空*/public static boolean isEmpty(Object[] objects){return isNull(objects) || (objects.length 0);}/*** * 判断一个对象数组是否非空** param objects 要判断的对象数组* return true非空 false空*/public static boolean isNotEmpty(Object[] objects){return !isEmpty(objects);}/*** * 判断一个Map是否为空** param map 要判断的Map* return true为空 false非空*/public static boolean isEmpty(Map?, ? map){return isNull(map) || map.isEmpty();}/*** * 判断一个Map是否为空** param map 要判断的Map* return true非空 false空*/public static boolean isNotEmpty(Map?, ? map){return !isEmpty(map);}/*** * 判断一个字符串是否为空串** param str String* return true为空 false非空*/public static boolean isEmpty(String str){return isNull(str) || NULLSTR.equals(str.trim());}/*** * 判断一个字符串是否为非空串** param str String* return true非空串 false空串*/public static boolean isNotEmpty(String str){return !isEmpty(str);}/*** * 判断一个对象是否为空** param object Object* return true为空 false非空*/public static boolean isNull(Object object){return object null;}/*** * 判断一个对象是否非空** param object Object* return true非空 false空*/public static boolean isNotNull(Object object){return !isNull(object);}public static boolean inStringIgnoreCase(String str, String... strs){if (str ! null strs ! null){for (String s : strs){if (str.equalsIgnoreCase(s)){return true;}}}return false;}
}
ServletUtils 客户端工具类 /*** 客户端工具类** author Jzm*/
public class ServletUtils
{/*** 获取String参数*/public static String getParameter(String name){return getRequest().getParameter(name);}/*** 获取String参数*/public static String getParameter(String name, String defaultValue){return Convert.toStr(getRequest().getParameter(name), defaultValue);}/*** 获取Integer参数*/public static Integer getParameterToInt(String name){return Convert.toInt(getRequest().getParameter(name));}/*** 获取Integer参数*/public static Integer getParameterToInt(String name, Integer defaultValue){return Convert.toInt(getRequest().getParameter(name), defaultValue);}/*** 获取Boolean参数*/public static Boolean getParameterToBool(String name){return Convert.toBool(getRequest().getParameter(name));}/*** 获取Boolean参数*/public static Boolean getParameterToBool(String name, Boolean defaultValue){return Convert.toBool(getRequest().getParameter(name), defaultValue);}/*** 获得所有请求参数** param request 请求对象{link ServletRequest}* return Map*/public static MapString, String[] getParams(ServletRequest request){final MapString, String[] map request.getParameterMap();return Collections.unmodifiableMap(map);}/*** 获得所有请求参数** param request 请求对象{link ServletRequest}* return Map*/public static MapString, String getParamMap(ServletRequest request){MapString, String params new HashMap();for (Map.EntryString, String[] entry : getParams(request).entrySet()){params.put(entry.getKey(), StringUtils.join(,, entry.getValue()));}return params;}/*** 获取request*/public static HttpServletRequest getRequest(){return getRequestAttributes().getRequest();}/*** 获取response*/public static HttpServletResponse getResponse(){return getRequestAttributes().getResponse();}/*** 获取session*/public static HttpSession getSession(){return getRequest().getSession();}public static ServletRequestAttributes getRequestAttributes(){RequestAttributes attributes RequestContextHolder.getRequestAttributes();return (ServletRequestAttributes) attributes;}/*** 将字符串渲染到客户端** 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();}}/*** 是否是Ajax异步请求** param request*/public static boolean isAjaxRequest(HttpServletRequest request){String accept request.getHeader(accept);if (accept ! null accept.contains(application/json)){return true;}String xRequestedWith request.getHeader(X-Requested-With);if (xRequestedWith ! null xRequestedWith.contains(XMLHttpRequest)){return true;}String uri request.getRequestURI();if (StringUtils.inStringIgnoreCase(uri, .json, .xml)){return true;}String ajax request.getParameter(__ajax);return StringUtils.inStringIgnoreCase(ajax, json, xml);}/*** 内容编码** param str 内容* return 编码后的内容*/public static String urlEncode(String str){try{return URLEncoder.encode(str, Constants.UTF8);} catch (UnsupportedEncodingException e){return StringUtils.EMPTY;}}/*** 内容解码** param str 内容* return 解码后的内容*/public static String urlDecode(String str){try{return URLDecoder.decode(str, Constants.UTF8);} catch (UnsupportedEncodingException e){return StringUtils.EMPTY;}}
}
项目用到的模型 AjaxResult 公共响应类 /*** 操作消息提醒** author jzm*/
public class AjaxResult extends HashMapString, 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(HttpStatus.SUCCESS, msg, data);}/*** 返回警告消息** param msg 返回内容* return 警告消息*/public static AjaxResult warn(String msg){return AjaxResult.warn(msg, null);}/*** 返回警告消息** param msg 返回内容* param data 数据对象* return 警告消息*/public static AjaxResult warn(String msg, Object data){return new AjaxResult(HttpStatus.WARN, 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(HttpStatus.ERROR, msg, data);}/*** 返回错误消息** param code 状态码* param msg 返回内容* return 错误消息*/public static AjaxResult error(int code, String msg){return new AjaxResult(code, msg, null);}/*** 是否为成功消息** return 结果*/public boolean isSuccess(){return Objects.equals(HttpStatus.SUCCESS, this.get(CODE_TAG));}/*** 是否为警告消息** return 结果*/public boolean isWarn(){return Objects.equals(HttpStatus.WARN, this.get(CODE_TAG));}/*** 是否为错误消息** return 结果*/public boolean isError(){return Objects.equals(HttpStatus.ERROR, this.get(CODE_TAG));}/*** 方便链式调用** param key 键* param value 值* return 数据对象*/Overridepublic AjaxResult put(String key, Object value){super.put(key, value);return this;}
}
2.核心 RepeatSubmit 重复提交注解。主要用来设置前后提交数据时间差,至少要大于的时间差的上限。 /*** 自定义注解防止表单重复提交** author jzm*/
Inherited
Target(ElementType.METHOD)
Retention(RetentionPolicy.RUNTIME)
Documented
public interface RepeatSubmit
{/*** 间隔时间(ms)小于此时间视为重复提交*/public int interval() default 5000;/*** 提示消息*/public String message() default 不允许重复提交请稍候再试;
} RepeatSubmitInterceptor 我们重复提交拦截器的抽象类。我们主要把 preHandle()方法给实现了但是具体判断是否重复提交的逻辑交给子类来实现。好处是,灵活度高,代码可读性强。当我们有其他相似功能拦截器需要实现时,也只需要继承该类即可。 /*** 拦截器** author: jzm* date: 2024-01-24 21:20**/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 repeatSubmit method.getAnnotation(RepeatSubmit.class); // 能拿到处理方法if (repeatSubmit ! null){if (this.isRepeatSubmit(request, repeatSubmit)) // 我们只有加了这个注解才表示限制重复提交{AjaxResult result AjaxResult.error(repeatSubmit.message());ServletUtils.renderString(response, JSONUtil.toJsonStr(result));return false;}}return true;} else{return true;}}/*** 验证是否重复提交由子类实现具体的防重复提交的规则** param request 请求信息* param annotation 防重复注解参数* return 结果* throws Exception*/public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
}
SameUrlDataInterceptor 我们要具体判断数据是否重复提交的子类。最后,将这个注入spring容器里面,然后我们在webmvc里面进行配置就可以正常使用了。 /*** 判断请求url和数据是否和上一次相同* 如果和上次相同则是重复提交表单。 有效时间为10秒内。** author jzm*/
// 我们使用拦截器防止重复提交
// 现在我们知道,为什么不用面向切面了? 切面需要拦截controller里面的方法,但是若依controller分布比较分散
// 用拦截器,会拦截controller的映射接口
// 首先,我们知道 Handler能够获得映射为方法的Method
Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{AutowiredRedisCache redisCache;public final String header Authorization;public final String REPEAT_PARAMS repeatParams;public final String REPEAT_TIME repeatTime;Overridepublic boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation){String nowParams ;// 拿请求body里面的内容// 拷贝副本--给拦截器读取 clone拷贝不显示,对于引用对象是引用拷贝...,//if (request instanceof RequestReaderHttpServletRequestWrapper){RequestReaderHttpServletRequestWrapper requestWrapper (RequestReaderHttpServletRequestWrapper) request;nowParams HttpHelper.getBodyString(requestWrapper);}// body参数为空获取Parameter的数据if (StringUtils.isEmpty(nowParams)){nowParams JSONUtil.toJsonStr(request.getParameterMap());}// 当前数据映射,提交参数、提交时间MapString, Object nowDataMap new HashMapString, Object();nowDataMap.put(REPEAT_PARAMS, nowParams);nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());// 请求地址作为存放cache的key值String uri request.getRequestURI();// 唯一值没有消息头则使用请求地址String submitKey StringUtils.trimToEmpty(request.getHeader(header));// 唯一标识指定key url 消息头String cacheRepeatKey CacheConstants.REPEAT_SUBMIT_KEY uri submitKey;// 如果 null,代表提交过Object sessionObj redisCache.getCacheObject(cacheRepeatKey);if (sessionObj ! null){MapString, Object sessionMap (MapString, Object) sessionObj;if (sessionMap.containsKey(uri)){MapString, Object preDataMap (MapString, Object) sessionMap.get(uri);// 两次提交内容一致 提交时间间隔差 要求时间段if (compareParams(nowDataMap, preDataMap) compareTime(nowDataMap, preDataMap, annotation.interval())){return true;}}}HashMapString, Object cacheMap new HashMap();cacheMap.put(uri, nowDataMap);redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);// 最后设置这上一个缓存对象重复提交时间return false;}/*** 判断参数是否相同*/private boolean compareParams(MapString, Object nowMap, MapString, Object preMap){String nowParams (String) nowMap.get(REPEAT_PARAMS);String preParams (String) preMap.get(REPEAT_PARAMS);return nowParams.equals(preParams);}/*** 判断两次间隔时间*/private boolean compareTime(MapString, Object nowMap, MapString, 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;}
}RepeatedlyRequestWrapper 关于为什么要这个东西呢?我们post请求,拦截器要预先读取HtppServletRequest里面的body的数据,是通过io的方式,都知道io读取完毕之后,之前的数据是变为null的但是当我么后面的接口来委派的时候,也是通过io读取body。这时候bodu里面是null的。那么鸡儿就会报io错。 因此我们需要这个类建立复制流。 /*** 将请求包装,用来建立复制流** author jzm*/
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
{private final byte[] body;public RepeatedlyRequestWrapper(HttpServletRequest request) throws IOException{super(request);body HttpHelper.getBodyString(request).getBytes(Charset.forName(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 boolean isFinished(){return false;}Overridepublic boolean isReady(){return false;}Overridepublic void setReadListener(ReadListener readListener){}};}
}
RepeatableFilter 一般情况下,还用到Filterl来对reques来进行包装成wrapper。然后传递到拦截器。 /*** Repeatable 过滤器** author jzm*/
Component
public class RepeatableFilter implements Filter
{Overridepublic void init(FilterConfig filterConfig) throws ServletException{}Override// 我们下面对于包装,前提是application/jsonpublic 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 RequestReaderHttpServletRequestWrapper((HttpServletRequest) request);}if (null requestWrapper){chain.doFilter(request, response);} else{chain.doFilter(requestWrapper, response);}}Overridepublic void destroy(){}
}
3.测试 测试用到的to /*** 测试to** author: jzm* date: 2024-01-25 14:26**/public class TestTo
{public TestTo(){}public String getName(){return name;}public void setName(String name){this.name name;}public Integer getAge(){return age;}public void setAge(Integer age){this.age age;}private String name;private Integer age;} 我们新建controller用来测试。分别对get路径参数、post请求体中的数据进行来校验。 *** 测试控制器** author: jzm* date: 2024-01-25 11:10**/RestController
ResponseBody
public class BaseController
{private Logger log LoggerFactory.getLogger(BaseController.class);RequestMapping(value /get/test, method {RequestMethod.GET})RepeatSubmit(interval 10 * 1000, message 对不起您重复提交get请求!)public AjaxResult getTest(RequestParam(name) String name, RequestParam(age) Integer age){String res get_test: name age;log.info(res);return AjaxResult.success(res);}RequestMapping(value /post/test, method {RequestMethod.POST})RepeatSubmit(interval 10 * 1000, message 对不起重复提交post请求)public AjaxResult postTest(RequestBody TestTo testTo){String res post_test testTo.getName() testTo.getAge();log.info(res);return AjaxResult.success(res);}
}我们启动项目,利用Apifox 来进行测试:
这时候打开Resp:免费Redis图形化界面RESP下载地址和连接步骤_resp下载-CSDN博客 发现数据是成功存入的,剩余7s过期,在10s之内,也就是数据没过期之前在发送一次。 因此,确实数据重复提交了。 post请求测试。