Controller 层数据校验实现思路
背景
我们编写后端代码的时候往往伴随着很多的参数校验,比如 mobile 字段必须接收一个有效的手机号码,sort 只能接收指定的参数进行排序等。
如果在 Controller 层一个个参数校验的话会显得很混乱,也伴随着大量的重复的代码。
我们可以使用 Hibernate Validator 在做这个事情。
代码
Validator 自带了很多参数校验,比如
public class DemoModel {
@NotBlank(message="用户名不能为空")
private String userName;
@NotBlank(message="年龄不能为空")
@Pattern(regexp="^[0-9]{1,2}$",message="年龄不正确")
private String age;
@AssertFalse(message = "必须为false")
private Boolean isFalse;
/**
* 如果是空,则不校验,如果不为空,则校验
*/
@Pattern(regexp="^[0-9]{4}-[0-9]{2}-[0-9]{2}$",message="出生日期格式不正确")
private String birthday;
}
再编写一个全局的异常拦截器:
@ControllerAdvice
@Order(value = Ordered.LOWEST_PRECEDENCE)
public class GlobalExceptionHandler {
// 直接在方法参数列表中使用注解进行校验,不通过时抛出的异常
// e.g. public String testValidator(@Sort @RequestParam String sort)
@ExceptionHandler(ValidationException.class)
@ResponseBody
public Object badArgumentHandler(ValidationException e) {
e.printStackTrace();
if (e instanceof ConstraintViolationException) {
ConstraintViolationException exs = (ConstraintViolationException) e;
Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
for (ConstraintViolation<?> item : violations) {
String message = ((PathImpl) item.getPropertyPath()).getLeafNode().getName() + item.getMessage();
return message;
}
}
return "error";
}
// 校验实体类中字段不通过时抛出的异常
// e.g. @Sort private String sort;
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public Object badArgumentHandler(MethodArgumentNotValidException e) {
e.printStackTrace();
if (e instanceof MethodArgumentNotValidException) {
BindingResult bindingResult = e.getBindingResult();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
StringJoiner stringJoiner = new StringJoiner(" ");
fieldErrors.forEach(errors ->{
stringJoiner.add(errors.getField().toString() + ":" + errors.getDefaultMessage());
});
return stringJoiner.toString();
}
return "error";
}
@ExceptionHandler(Exception.class)
@ResponseBody
public Object seriousHandler(Exception e) {
e.printStackTrace();
return "error";
}
}
我们也可以自定义注解来实现某些特殊业务逻辑的判断:
1.自定义注解,需要有 groups() 和 payload() 两个方法,不然会报错。
2.编写 Validator 实现 ConstraintValidator<A extends Annotation, T> 作为异常判断逻辑
@Target({METHOD, FIELD, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = SortValidator.class)
public @interface Sort {
String message() default "排序字段不支持";
String[] accepts() default {"add_time", "id"};
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class SortValidator implements ConstraintValidator<Sort, String> {
private List<String> valueList;
@Override
public void initialize(Sort sort) {
valueList = new ArrayList<>();
for (String val : sort.accepts()) {
valueList.add(val.toUpperCase());
}
}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (!valueList.contains(s.toUpperCase())) {
return false;
}
return true;
}
}
@RestController
@Validated
public class ExampleController {
@GetMapping("/testValidator")
public String testValidator(@Sort @RequestParam String sort) {
return "success";
}
@GetMapping("/testValidator2")
public String testValidator2(@Validated @RequestBody User user) {
return "success";
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private Integer id;
private String name;
private String address;
private Integer age;
@Sort
private String sort;
}
如果是校验参数,必须在 Controller 上添加 @Validated 注解。
如果是校验实体内字段,必须在需要校验的实体参数前添加@Validated 注解。