JSR3030框架与Vaildation参数校验(转载)
前言
Web开发中JS校验可以涵盖大部分的校验职责,如用户名唯一性,生日格式,邮箱格式校验等等常用的校验。但是为了避免用户绕过浏览器,服务端的数据校验也是必要的,可以防止脏数据落到数据库中,本文将要介绍的Validation 来对数据进行校验。
简单叙述下JSR303/JSR-349
,Hibernate Validation
,Spring Validation
之间的关系。
JSR303是一项标准,JSR-349是其的升级版本,添加了一些新特性,他们规定一些校验规范即校验注解,如@Null
,@NotNull
,@Pattern
,他们位于javax.validation.constraints
包下,只提供规范不提供实现。
而Hibernate Validation
是对这个规范的实践(不要将hibernate和数据库orm框架联系在一起),他提供了相应的实现,并增加了一些其他校验注解,如@Email
,@Length
,@Range
等等,他们位于org.hibernate.validator.constraints
包下。
而万能的spring为了给开发者提供便捷,对Hibernate Validation进行了二次封装,显示校验validated bean时,你可以使用Spring Validation
或者Hibernate Validation,而Spring Validation另一个特性,便是其在SpringMVC模块中添加了自动校验,并将校验信息封装进了特定的类中。这无疑便捷了我们的Web开发。
本文主要介绍在SpringMVC中自动校验的机制。
引入依赖
我们使用Maven构建SpringBoot应用来进行Demo演示。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
- 1
- 2
- 3
- 4
- 5
- 6
我们只需要引入spring-boot-starter-web
依赖即可,如果查看其子依赖,可以发现如下的依赖:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
验证了我之前的描述,web模块使用了hibernate-validation
,并且databind模块也提供了相应的数据绑定功能。
创建需要被校验的实体类
public class Foo {
@NotBlank
private String name;
@Min(18)
private Integer age;
@Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手机号码格式错误")
@NotBlank(message = "手机号码不能为空")
private String phone;
@Email(message = "邮箱格式错误")
private String email;
//getter setter...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
使用一些比较常用的校验注解,还是比较浅显易懂的,字段上的注解名称即可推断出校验内容,每一个注解都包含了message字段,用于校验失败时作为提示信息,特殊的校验注解,如Pattern(正则校验),还可以自己添加正则表达式。
在@Controller中校验数据
SpringMVC为我们提供了自动封装表单参数的功能,一个添加了参数校验的典型Controller如下所示。
@RestController
public class FooController {
@RequestMapping("/foo")
public String foo(@Validated Foo foo <1>, BindingResult bindingResult <2>) {
if(bindingResult.hasErrors()){
for (FieldError fieldError : bindingResult.getFieldErrors()) {
//...
}
return "fail";
}
return "success";
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
值得注意的地方:
-
参数Foo前需要加上
@Validated
注解,表明需要spring对其进行校验,而校验的信息会存放到其后的BindingResult
中。注意,必须相邻,如果有多个参数需要校验,形式可以如下。foo(@Validated Foo foo, BindingResult fooBindingResult ,@Validated Bar bar, BindingResult barBindingResult);
即一个校验类对应一个校验结果。 -
校验结果会被自动填充,在Controller中可以根据业务逻辑来决定具体的操作,如跳转到错误页面。
一个最基本的校验就完成了,总结下框架已经提供了哪些校验:
我们对上面实现的校验入口进行一次测试请求: 访问以下接口可以得到如下的debug信息:
实验告诉我们,校验结果起了作用。并且,可以发现当发生多个错误,Spring Validation不会在第一个错误发生后立即停止,而是继续试错,告诉我们所有的错误。debug可以查看到更多丰富的错误信息,这些都是Spring Validation为我们提供的便捷特性,基本适用于大多数场景。
你可能不满足于简单的校验特性,下面进行一些补充。
全局校验处理
对每一个要校验的参数后面加上BindingResult
未免太过繁琐,我们可以对其全局处理。
以下面这个为例:
@Data
public class TableBO {
@NotBlank
private String id;
@NotBlank
private String data;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
再Controller参数校验时,只需要添加@Validated
注解:
@PutMapping(value = "/updateById", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResultVO updateTableDataById(@Validated @RequestBody TableBO tableBO) {
return standBookService.modifyCellData(tableBO);
}
- 1
- 2
- 3
- 4
编写一个全局异常类,并拦截MethodArgumentNotValidException
异常,如下:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
BindingResult bindingResult = e.getBindingResult();
StringBuilder sb = new StringBuilder("校验失败:");
for (FieldError fieldError : bindingResult.getFieldErrors()) {
sb.append(fieldError.getField() + ":" + fieldError.getDefaultMessage() + ", ");
}
return ResultVOUtils.error(ResultEnum.PARAM_ERROR.getCode(), sb.toString());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
通过fieldError.getField()
获取校验未通过的项,fieldError.getDefaultMessage()
获取默认的错误信息,例如:
{
"code": 17,
"message": "校验失败:data:不能为空, id:不能为空, ",
"data": null
}
分组校验
如果同一个类,在不同的使用场景下有不同的校验规则,那么可以使用分组校验。未成年人是不能喝酒的,而在其他场景下我们不做特殊的限制,这个需求如何体现同一个实体,不同的校验规则呢?
改写注解,添加分组:
Class Foo{
@Min(value = 18,groups = {Adult.class})
private Integer age;
public interface Adult{}
}
这样表明,只有在Adult分组下,18岁的限制才会起作用。Controller层改写:
@RequestMapping("/drink")
public String drink(@Validated({Foo.Adult.class}) Foo foo, BindingResult bindingResult) {
if(bindingResult.hasErrors()){
for (FieldError fieldError : bindingResult.getFieldErrors()) {
//...
}
return "fail";
}
return "success";
}
@RequestMapping("/live")
public String live(@Validated Foo foo, BindingResult bindingResult) {
if(bindingResult.hasErrors()){
for (FieldError fieldError : bindingResult.getFieldErrors()) {
//...
}
return "fail";
}
return "success";
}
drink方法限定需要进行Adult校验,而live方法则不做限制。
自定义校验
业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。自定义Spring Validation非常简单,主要分为两步。
(1)自定义校验注解
我们尝试添加一个“字符串不能包含空格”的限制。
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CannotHaveBlankValidator.class})<1>
public @interface CannotHaveBlank {
//默认错误消息
String message() default "不能包含空格";
//分组
Class<?>[] groups() default {};
//负载
Class<? extends Payload>[] payload() default {};
//指定多个时使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
CannotHaveBlank[] value();
}
}
- 1
我们不需要关注太多东西,使用 Spring Validation 的原则便是便捷我们的开发。
- 自定义注解中指定了这个注解真正的验证者类。
(2)编写真正的校验类
public class CannotHaveBlankValidator implements <1> ConstraintValidator<CannotHaveBlank, String> {
@Override
public void initialize(CannotHaveBlank constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context <2>) {
//null时不进行校验
if (value != null && value.contains(" ")) {
<3>
//获取默认提示信息
String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
System.out.println("default message :" + defaultConstraintMessageTemplate);
//禁用默认提示信息
context.disableDefaultConstraintViolation();
//设置提示语
context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation();
return false;
}
return true;
}
}
- 所有的验证者都需要实现
ConstraintValidator
接口,它的接口也很形象,包含一个初始化事件方法,和一个判断是否合法的方法。
public interface ConstraintValidator<A extends Annotation, T> {
void initialize(A constraintAnnotation);
boolean isValid(T value, ConstraintValidatorContext context);
}
-
ConstraintValidatorContext
这个上下文包含了认证中所有的信息,我们可以利用这个上下文实现获取默认错误提示信息,禁用错误提示信息,改写错误提示信息等操作。 -
列出一些典型校验操作,或许可以对你产生启示作用。
值得注意的一点是,自定义注解可以用在METHOD, FIELD
, ANNOTATION_TYPE
, CONSTRUCTOR,
PARAMETER
之上,ConstraintValidator的第二个泛型参数T,是需要被校验的类型。
手动校验
可能在某些场景下需要我们手动校验,即使用校验器对需要被校验的实体发起validate,同步获得校验结果。
@Autowired
Validator globalValidator; <1>
@RequestMapping("/validate")
public String validate() {
Foo foo = new Foo();
foo.setAge(22);
foo.setEmail("000");
Set<ConstraintViolation<Foo>> set = globalValidator.validate(foo);<2>
for (ConstraintViolation<Foo> constraintViolation : set) {
System.out.println(constraintViolation.getMessage());
}
return "success";
}
-
真正使用过Validator接口的读者会发现有两个接口,一个是位于javax.validation包下,另一个位于org.springframework.validation包***意我们这里使用的是前者javax.validation,后者是spring自己内置的校验接口,
LocalValidatorFactoryBean
同时实现了这两个接口。 -
此处校验接口最终的实现类便是LocalValidatorFactoryBean。
基于方法校验
@RestController
@Validated <1>
public class BarController {
@RequestMapping("/bar")
public <2.1> @NotBlank String (@Min(18) Integer age <2.2>) {
System.out.println("age : " + age);
return "";
}
@ExceptionHandler(ConstraintViolationException.class)
public Map handleConstraintViolationException(ConstraintViolationException cve){
Set<ConstraintViolation<?>> cves = cve.getConstraintViolations();<3>
for (ConstraintViolation<?> constraintViolation : cves) {
System.out.println(constraintViolation.getMessage());
}
Map map = new HashMap();
map.put("errorCode",500);
return map;
}
}
- 1为类添加@Validated注解
-
校验方法的返回值和入参
-
添加一个异常处理器,可以获得没有通过校验的属性相关信息
使用校验框架的一些想法
理论上Spring Validation可以实现很多复杂的校验,你甚至可以使你的Validator获取ApplicationContext,获取spring容器中所有的资源,进行诸如数据库校验,注入其他校验工具,完成组合校验(如前后密码一致)等等操作。
但是寻求一个易用性和封装复杂性之间的平衡点是我们作为工具使用者应该考虑的,我推崇的方式,是仅仅使用自带的注解和自定义注解,完成一些简单的,可复用的校验。而对于复杂的校验,则包含在业务代码之中,毕竟如用户名是否存在这样的校验,仅仅依靠数据库查询还不够,为了避免并发问题,还是得加上唯一索引之类的额外工作,不是吗?
转载自作者:https://blog.****.net/yuanlaijike/article/details/83017609,非常感谢提供此文