1. 常见入参校验
1.1 非空校验
栗子:
校验某些必填字段值,直接使用注解实现
@NotNull
@NotBlank
@NotEmpty
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.NotEmpty;
1.2 字符串校验
栗子:
比如姓名要求不超过10个中文字符,String 表示的日期必须符合某种格式,更多用于数值型的校验,下面给出一个简单的小栗子
public class ValidationUtils {
/** 数字校验 */
public static final String NUM_REGEX = "^\\d+$";
/** 电话号码校验 */
public static final String PHONE_REGEX = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$";
/** 身份证号码校验 */
public static final String ID_CARD_REGEX = "^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$";
/** 中文校验 */
public static final String ZH_REGEX = "[\\u4e00-\\u9fa5]+";
/** 中文校验,限制1-30个字符 */
public static final String ZH_REGEX_LIMIT_30 = "^[\\u4e00-\\u9fa5]{1,30}$";
/**
* 英文、数字、下划线构成
* 首字母只能为英文,末尾不能是下划线
*/
public static final String ROLE_CODE_REGEX = "^[a-z][a-z0-9_]*[a-z]$";
/** 英文、数字组成,不超过 30 个字符 */
public static final String NUM_ENG_LIMIT_30 = "^[A-Za-z0-9]{1,30}$";
/** 日期校验 - yyyy-mm-dd */
public static final String DATE_REGEX = "^\\d{4}-\\d{2}-\\d{2}";
/** 日期校验 - yyyy-mm */
public static final String DATE_MONTH_REGEX = "^\\d{4}-\\d{2}";
/**
* 两位小数限制 - 整数位无限制
* 0.xx 两位小数
* xxxx.xx 两位小数
*/
public static final String NUMBER_D2_REGEX = "^(0\\.\\d{1,2}|[1-9]\\d*(\\.\\d{1,2})?)$";
/**
* 三位小数限制 - 整数位最高 8 位限制
* 0.xx 两位小数
* xxxx.xxx 三位小数
*/
public static final String NUMBER_D3_REGEX = "^(0\\.\\d{1,2}|[1-9]{1,8}\\d*(\\.\\d{1,3})?)$";
/**
* 一位小数限制,整数位最高 2 位 - 最高 99.9
*/
public static final String NUMBER_D1_REGEX = "^(([1-9]?\\d(\\.\\d{1})?)|99.9)$";
/** 小数位数无限制 */
public static final String NUMBER_D_REGEX = "^(0\\.\\d{2}|[1-9]\\d*(\\.\\d+)?)$";
/** 1-99的正整数 hundred */
public static final String NUMBER_BETWEEN_ZERO_HUNDRED_REGEX = "^[1-9]\\d?$";
/** > 0 的正整数 - 最高八位 */
public static final String NUMBER_LIMIT_EIGHT_REGEX = "^[1-9][0-9]{1,8}$";
/**
* 判断字符串是否符合正则表达式
*
* @param str 输入的 string
* @param regex 想要匹配的正则表达式
* @return true - 表示匹配
* false - 表示不匹配
*/
public static boolean matchTargetRegex(String str, String regex) {
boolean isMatch = Pattern.matches(regex, str);
return isMatch;
}
/**
* 判断一个 str 是否符合正则表达式且满足长度限制
*
* @param str
* @param regex
* @param limit
* @return true - 满足表达式且满足长度
*/
public static boolean matchTargetRegex(String str, String regex, int limit) {
if (!matchTargetRegex(str, regex)) {
return false;
}
return str.length() <= limit;
}
/**
* 返回所有匹配正则表达式的子串字符长度和
*
* @param str 输入的 string
* @param regex 想要匹配的正则表达式
* @return -1 说明没有符合的子串
* else 符合正则表达式的所有子串长度和
*/
public static int subMatchCharCount(String str, String regex) {
int count = 0;
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(str);
while (m.find()) {
count += (m.end() - m.start());
}
return count == 0 ? -1 : count;
}
/**
* bigDecimal 数据 < 0 的时候,返回 zero
*
* @param num 传入的数据
* @return
*/
public static BigDecimal minZeroBdDeal(BigDecimal num) {
if (num.compareTo(BigDecimal.ZERO) < 0) {
num = BigDecimal.ZERO;
}
return num;
}
/**
* 检查一个数字(Number类型)是否与给定的正则表达式匹配
* @param num
* @param regex
* @return
*/
public static boolean numMatchRegex(Number num, String regex) {
return num == null || num.toString().matches(regex);
}
/**
* 传入的 bigDecimal 数据是否在当前设置的最高位数内
*
* @param num 传入的 bigDecimal 数据
* @param highLimit 小数点前-整数位的最高位数
* @return true - 在最高范围内
* false - 超出最高位数
*/
public static boolean isInBigDecimalBeforeDot(BigDecimal num, int highLimit) {
int beforeDot = num.precision() - num.scale();
return beforeDot <= highLimit;
}
}
1.3 唯一性校验
场景: 新增修改时保证某些字段的值唯一:
直接查询数据库就好,就像下面这样,and id != ? 是可选项,新增不用添加,修改时添加
select count(*) from table where column = ?and id != ?
对查询到的结果 res (sql语句的结果) 进行判断,true 说明唯一,false 说明不唯一
return res == 0 ?
2. 常见操作校验
2.1 接口防抖
就是防止请求在短时间内多次发送到同一个接口。
方案: 设置一个防抖时间,操作前在 redis 里面生成一个 key,设置过期时间 = 防抖时间,操作时判断有没有 key
有 key 就提示操作过于频繁,没有就创建key
如果想判断相同请求
方案 1: 找到传输数据中的特定标识数据,赋值 k - v
方案 2: 整个入参赋值为 v,每次判断 redis里的 k - v 的 value 是否和这次传输的 value 相同,相同&&在防抖时间内,不做更新,其余情况都更新
(但是不推荐第二种方案,redis 里不适合存储大数据的 value,redis 单线程读取 bigValue 和 bigKey 时,读取速度慢,再加上单线程处理机制,上一次没查完,第二次就要等待查询,容易造成阻塞现象)
举个栗子:优化之前流程里的 Controller 层写法
@RestController
@RequestMapping("/work-operator")
public class WorkOperatorController {
@Autowired
private IWorkOperatorService workOperatorService;
@Resource
private RedisUtil redisUtil;
/** 设置防抖时间,1s */
private final long TIME_LIMIT = 1L;
/**
* 单笔数据新增
* @param saveDTO
* @return
*/
@PostMapping("/create")
public R<Object> create(@Valid @RequestBody WorkOperatorDTO saveDTO, @User UserBO userBO){
// 判断入参是否正确
if (checkDTO(saveDTO) != null) {
return checkDTO(saveDTO);
}
// 入参正确后,可以进入添加方法,此时添加 key: 用户id + 方法名, value: 当前时间戳,防止接口抖动
String key = userBO.getId() + "::" + "createWorkOperator";
// setKeyIfAbsent() : key不存在,设置成功,返回true; key存在,则设置失败,返回false
Boolean lock = redisUtil.tryLock(key, TIME_LIMIT, TimeUnit.SECONDS);
if(lock == null || !lock){
return R.fail(ConstantMessage.REPEAT_REQUEST);
}
return workOperatorService.createWorkOperator(saveDTO) ?
R.ok(ConstantMessage.CREATE_SUCCESS) : R.fail(ConstantMessage.CREATE_FAIL);
}
/** 删除防抖,同理 */
@PostMapping("/delete-batch")
public R<Object> deleteBatch(@RequestBody List<Long> ids){
String key = userBO.getId() + "::" + "delWorkOperator";
Boolean lock = redisUtil.tryLock(key, TIME_LIMIT, TimeUnit.SECONDS);
if(lock == null || !lock){
return R.fail(ConstantMessage.REPEAT_REQUEST);
}
return workOperatorService.deleteBatch(ids) ?
R.ok(ConstantMessage.DELETE_SUCCESS) : R.fail(ConstantMessage.DELETE_FAIL + ConstantMessage.WORK_OPERATOR_UNBIND);
}
}
为了防抖能够成功实现,不可以在操作完成后删除 redis 中的 key 值!
文章末尾给出 redisUtil
2.2 防止同一数据并发提交
场景: 有些重要的数据,我们希望许多有修改权限的人,同意时间内只有一个人修改该数据,简单来说就是给接口在并发情况下加锁。
方案: 使用 redis 来模拟锁,操作前加锁,操作结束释放锁
( 这类结束后释放资源型的代码,都可以使用 try-catch-finally 来处理,让系统自动释放资源 )
举个栗子:
public R<String> submit(@Validated @RequestBody AssessProblemSubmitRO submitVO) {
// 进行校验
// 开始防止并发现象
Boolean lock = redisUtil.tryLock("submit_problem_" + taskDetailId, 3L, TimeUnit.MINUTES);
Long count = 0L;
// 未获取到缓存锁,自循环获取缓存锁,且自循环次数在1000次以内
while ((lock == null || !lock) && count < 1000){
lock = redisUtil.tryLock("submit_problem_" + taskDetailId, 3L, TimeUnit.MINUTES);
++count;
}
// 当前线程没有获取到锁,给出提示
if(lock == null || !lock){
return R.fail("当前道路正在被提交问题中,请稍后再试");
}
try{
// 业务代码
}catch (Exception e){
throw e;
} finally {
// 释放锁
redisUtil.deleteKey("submit_problem_" + taskDetailId);
}
return R.ok("保存成功");
}
对上面的代码的解释:
-
自循环: 多个线程同时进入方法,获取一把锁的时候,可能会导致竞争条件产生,使得几个线程都没有获取锁,这时,就需要进入自循环来再次获取锁。
-
竞争条件 :相当于三个人抢东西,三人互相阻碍,抢了一会儿之后,结果谁都没拿到。
-
结果分析 :最坏的情况就是,进入自循环后,仍然有不断的竞争条件产生,最后搞得所有的线程都没有拿到锁。否则,只有一个线程拿到锁,完成对数据的操作。
2.2.1 如果是某个集合需要做并发操作
上面的存储在 redis 里的键值换成一个集合就好
2.3 redis 工具类
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class RedisUtil {
@Resource
private RedisTemplate redisTemplate;
/**
* 添加 Key 缓存
*
* @param key String key
* @param value Object
* @param <T> Value Type
*/
public <T> void setKey(String key, final T value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 添加 Key 缓存,并设置失效时间
*
* @param key String key
* @param value Object
* @param time Time
* @param unit TimeUnit
* @param <T> Value Type
*/
public <T> void setKey(String key, final T value, long time, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, time, unit);
}
/**
* 批量添加 Key 缓存
*
* @param valuesMap Map String:Object
* @param <T> Value Type
*/
public <T> void setKey(Map<String, T> valuesMap) {
redisTemplate.opsForValue().multiSet(valuesMap);
}
/**
* 批量添加 Key 缓存,并设置失效时间
*
* @param valueMap Map String:Object
* @param expireMillis Map String:Long
* @param <T> Value Type
*/
public <T> void setKey(Map<String, T> valueMap, Map<String, Long> expireMillis) {
redisTemplate.opsForValue().multiSet(valueMap);
setExpire(expireMillis);
}
/**
* 获取 Key 缓存
*
* @param key String key
* @param <T> Value Type
* @return T
*/
public <T> T getKey(final String key) {
ValueOperations<String, T> operations = redisTemplate.opsForValue();
return operations.get(key);
}
/**
* 批量获取 Key 缓存值
*
* @param keys String key array
* @param <T> Value Type
* @return T Array
*/
public <T> List<T> getKey(List<String> keys) {
ValueOperations<String, T> operations = redisTemplate.opsForValue();
return operations.multiGet(keys);
}
/**
* 判断 Key 是否存在
*
* @param key String key
* @return boolean
*/
public boolean hasKey(String key) {
Boolean hasKey = redisTemplate.hasKey(key);
return Boolean.TRUE.equals(hasKey);
}
/**
* 批量获取 Key 缓存
*
* @param pattern Key pattern
* @return Key Set
*/
public Set<String> getKeys(final String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 删除 Key 缓存
*
* @param key Key
*/
public void deleteKey(String key) {
redisTemplate.delete(key);
}
/**
* 批量删除 Key 缓存
*
* @param keys Key Array
*/
public void deleteKey(List<String> keys) {
redisTemplate.delete(keys);
}
/**
* 指定键值失效时间
*
* @param key String key
* @param time Time
* @param unit TimeUnit
*/
public void setExpire(String key, long time, TimeUnit unit) {
if (time > 0) {
redisTemplate.expire(key, time, unit);
}
}
/**
* 批量指定键值失效时间
*
* @param expireMillis Map String:Long
*/
public void setExpire(Map<String, Long> expireMillis) {
if (null != expireMillis && expireMillis.size() > 0) {
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.execute((RedisCallback<Object>) connection -> {
expireMillis.forEach((key, expire) -> {
byte[] serialize = stringRedisSerializer.serialize(key);
if (null != serialize) {
connection.pExpire(serialize, expire);
}
});
return null;
});
}
}
/**
* 指定键值在指定时间失效
*
* @param key String key
* @param date Date
*/
public void setExpireAt(String key, Date date) {
Date current = new Date();
if (date.getTime() >= current.getTime()) {
redisTemplate.expireAt(key, date);
}
}
/**
* 获取 Key 失效时间
*
* @param key String key
* @param unit TimeUnit
* @return 剩余失效时长
*/
public long getExpire(String key, TimeUnit unit) {
Long expire = redisTemplate.getExpire(key, unit);
if (null != expire) {
return expire;
}
return 0L;
}
public Boolean tryLock(String key, Long expireTime, TimeUnit timeUnit){
return redisTemplate.opsForValue().setIfAbsent(key, "", expireTime, timeUnit);
}
public Long realeaseLock(String key){
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
return (Long) redisTemplate.execute(redisScript, Collections.singletonList(key), "");
}
public Long realeaseLocks(List<String> keys){
String luaScript = "for _, key in ipairs(KEYS) do redis.call('del', key) end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
return (Long) redisTemplate.execute(redisScript, keys, "");
}
public Long loginIncrement(String key){
return redisTemplate.opsForValue().increment(key);
}
}