如何进行参数校验
1. 不好的参数校验
- 对于用户输入的数据来说,不只前端要校验数据,后端也要对数进行校验,比如入参是否可以为空、入参长度是否满足你的期望长度
- 比如你的数据库长度设置的
varchar(16),对方直接来了个36的,那么数据库直接异常
- 如果以if判断来进行参数校验,代码就会非常的难看
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/add")
public ResponseEntity<String> add(User user) {
if(user.getName()==null) {
return ResponseResult.fail("用户名不可为空!");
} else if(user.getName().length()<5 || user.getName().length()>15){
return ResponseResult.fail("用户名长度在 5-15 之间");
}
if(user.getAge()< 1 || user.getAge()> 150) {
return ResponseResult.fail("你敢是乌龟吗?");
}
return ResponseEntity.ok("success");
}
}
- 针对这个问题,Java开者在Java API规范 (JSR303) 定义了Bean校验的标准validation-api,但没有提供实现。
- hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。
- Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。
2. 开始
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- 对bean的属性添加校验注解
- 可以单独使用一个DTO
- 每个注解指定
message属性,为错误信息提示
- 有哪些常用的校验?(在下面)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
@NotBlank
private String username;
@NotBlank
private String password;
private String sex;
@Email
private String email;
@Pattern(regexp = "^[0-9]{11}$", message = "手机号格式不正确")
private String phone;
}
- 在Controller的方法入参上使用
@Valid注解,参数校验的值放在BindingResult中
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping
public ResponseResult addUser(@Valid User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String, String> map = bindingResult.getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
return new ResponseResult(ResponseStatus.FAIL.getCode(), "数据格式校验失败", map) ;
}
log.info("用户信息:{}",user);
return ResponseResult.success("添加成功!");
}
}
3. 统一异常处理
- 在数据校验失败时会发出
BindException异常,可以使用spring的统一异常处理器进行处理
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping
public ResponseResult addUser(@Valid User user) {
log.info("用户信息:{}",user);
return ResponseResult.success("添加成功!");
}
}
@RestControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(BindException.class)
public ResponseResult validationException(BindException e) {
Map<String, String> map = e.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
return new ResponseResult(ResponseStatus.FAIL.getCode(), "数据格式校验失败", map) ;
}
}
4. 分组校验
- 我们一般在数据增删改查时,通常增加时使用数据库的主键自增,也就是增加时主键必须为null,删改查时主键必须不为null
- 这里同一bean中出现了两种校验场景,可以使用分组校验
- 【分组】即为接口,接口中只是作为一个标识,无需任何代码
public interface AddValid {
}
public interface UpdateValid {
}
- 设置分组后,校验注解可以指定组
- 没有设置groups属性的注解,使得分组校验时不会校验
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Null(groups = {AddValid.class})
@NotNull(groups = {UpdateValid.class})
private Long id;
@NotBlank(groups = {AddValid.class, UpdateValid.class})
private String username;
@NotBlank(groups = {AddValid.class})
private String password;
private String sex;
@Email(groups = {AddValid.class, UpdateValid.class})
private String email;
@Pattern(regexp = "^[0-9]{11}$", message = "手机号格式不正确", groups = {AddValid.class, UpdateValid.class})
private String phone;
}
- 使用分组校验,形参使用
@Validated注解,value属性指定组
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping
public ResponseResult addUser(@Validated({AddValid.class}) User user) {
log.info("用户信息:{}",user);
return ResponseResult.success("添加成功!");
}
@PutMapping
public ResponseResult updateUser(@Validated(UpdateValid.class) User user) {
log.info("用户信息:{}",user);
return ResponseResult.success("修改成功");
}
}
5. 常用的校验注解
@AssertFalse 被注释的元素只能为false
@AssertTrue 被注释的元素只能为true
@DecimalMax 被注释的元素必须小于或等于{value}
@DecimalMin 被注释的元素必须大于或等于{value}
@Digits 被注释的元素数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
@Email 被注释的元素不是一个合法的电子邮件地址
@Future 被注释的元素需要是一个将来的时间
@FutureOrPresent 被注释的元素需要是一个将来或现在的时间
@Max 被注释的元素最大不能超过{value}
@Min 被注释的元素最小不能小于{value}
@Negative 被注释的元素必须是负数
@NegativeOrZero 被注释的元素必须是负数或零
@NotBlank 被注释的元素不能为空、空白字符串
@NotEmpty 被注释的元素不能为空
@NotNull 被注释的元素不能为null
@Null 被注释的元素必须为null
@Past 被注释的元素需要是一个过去的时间
@PastOrPresent 被注释的元素需要是一个过去或现在的时间
@Pattern 被注释的元素需要匹配正则表达式"{regexp}"
@Positive 被注释的元素必须是正数
@PositiveOrZero 被注释的元素必须是正数或零
@Size 被注释的元素个数必须在{min}和{max}之间
- hibernate validation是对这个规范的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等
@CreditCardNumber 被注释的元素不合法的信用卡号码
@Currency 被注释的元素不合法的货币 (必须是{value}其中之一)
@EAN 被注释的元素不合法的{type}条形码
@Email 被注释的元素不是一个合法的电子邮件地址 (已过期)
@Length 被注释的元素长度需要在{min}和{max}之间
@CodePointLength 被注释的元素长度需要在{min}和{max}之间
@LuhnCheck 被注释的元素${validatedValue}的校验码不合法, Luhn模10校验和不匹配
@Mod10Check 被注释的元素${validatedValue}的校验码不合法, 模10校验和不匹配
@Mod11Check 被注释的元素${validatedValue}的校验码不合法, 模11校验和不匹配
@ModCheck 被注释的元素${validatedValue}的校验码不合法, ${modType}校验和不匹配 (已过期)
@NotBlank 被注释的元素不能为空 (已过期)
@NotEmpty 被注释的元素不能为空 (已过期)
@ParametersScriptAssert 被注释的元素执行脚本表达式"{script}"没有返回期望结果
@Range 被注释的元素需要在{min}和{max}之间
@SafeHtml 被注释的元素可能有不安全的HTML内容
@ScriptAssert 被注释的元素执行脚本表达式"{script}"没有返回期望结果
@URL 被注释的元素需要是一个合法的URL
@DurationMax 被注释的元素必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
@DurationMin 被注释的元素必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
6. 自定义validation
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {TelephoneNumberValidator.class})
public @interface TelephoneNumber {
String message() default "Invalid telephone number";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
public class TelephoneNumberValidator implements ConstraintValidator<TelephoneNumber, String> {
private static final String REGEX_TEL = "0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}|13[0-9]\\d{8}|15[1089]\\d{8}";
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
try {
return Pattern.matches(REGEX_TEL, s);
} catch (Exception e) {
return false;
}
}
}
@Data
@Builder
@ApiModel(value = "User", subTypes = {AddressParam.class})
public class UserParam implements Serializable {
private static final long serialVersionUID = 1L;
@NotEmpty(message = "{user.msg.userId.notEmpty}", groups = {EditValidationGroup.class})
private String userId;
@TelephoneNumber(message = "invalid telephone number")
private String telephone;
}