早期的参数校验形式
在早期的时候,java的参数校验停留在获取参数之后在代码层面做校验,类似如下操作:
@PostMapping("/test")
public String test(@RequestBody TestRequest request) {
if (StringUtils.isEmpty(request.getName())) {
return "姓名不能为空";
}
if (request.getName().length() > 6) {
return "姓名长度不能超过6";
}
if (StringUtils.isEmpty(request.getPhone())) {
return "电话不能为空";
}
if (!isPhone(request.getPhone())) {
return "电话号码格式不正确";
}
return "SUCCESS";
}
/**
* 校验电话格式
*
* @param phone
* @return
*/
private boolean isPhone(String phone) {
return true;
}
如代码中看到,每一种校验规则都需要在代码里面提现,那么光是参数的校验也会耗费开发人员大量的精力。也许有人会说使用我也可以使用Assert来将一个校验从3行代码变成1行代码,但是即便如此,如果需要校验的参数非常多,并且校验的种类也非常多,那么开发效率也会大幅降低。
既然如此,有没有一种不需要在代码层面就可以解决掉这种痛苦的办法呢?当然是有的,再java6里面推出了一种规范:JSR-303,JSR是Java Specification Requests的缩写,意思是Java 规范提案,又叫做Bean Validation。JSR 303是Java为bean数据合法性校验提供的标准框架。Hibernate Validator是Bean Validation的参考实现
Bean Validation中内置的constraint
以下图片来自互联网
Hibernate Validator 附加的 constraint
使用JSR303规范来做参数校验
有了统一的参数校验规范,现在来对上面早期的参数校验方式做一个重构。
将请求类改造成如下所示:
package com.yezi.springboot219.controller.request;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.*;
/**
* @Description:
* @Author: yezi
* @Date: 2020/6/8 10:11
*/
@Data
public class TestRequest {
@NotEmpty(message = "姓名不能为空")
@Length(min = 1, max = 6, message = "姓名长度必须在1-6之间")
private String name;
@NotBlank(message = "电话号码不能为空")
@Pattern(regexp = "^134[0-8]\\d{7}$|^13[^4]\\d{8}$|^14[5-9]\\d{8}$|^15[^4]\\d{8}$|^16[6]\\d{8}$|^17[0-8]\\d{8}$|^18[\\d]{9}$|^19[8,9]\\d{8}$", message = "电话号码格式不正确")
private String phone;
@Email(message = "邮件格式不正确")
private String email;
@NotNull(message = "年龄不能为空")
@Max(value = 18,message = "年龄不能超过18")
private Integer age;
}
去掉controller中对字段的代码层面校验
@PostMapping("/test")
public String test(@RequestBody @Valid TestRequest request) {
return "SUCCESS";
}
发送测试请求去下:
结果:
{
"timestamp": "2020-06-08T02:59:38.463+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotEmpty.testRequest.name",
"NotEmpty.name",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"testRequest.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "姓名不能为空",
"objectName": "testRequest",
"field": "name",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
},
{
"codes": [
"Pattern.testRequest.phone",
"Pattern.phone",
"Pattern.java.lang.String",
"Pattern"
],
"arguments": [
{
"codes": [
"testRequest.phone",
"phone"
],
"arguments": null,
"defaultMessage": "phone",
"code": "phone"
},
[],
{
"defaultMessage": "^134[0-8]\\d{7}$|^13[^4]\\d{8}$|^14[5-9]\\d{8}$|^15[^4]\\d{8}$|^16[6]\\d{8}$|^17[0-8]\\d{8}$|^18[\\d]{9}$|^19[8,9]\\d{8}$",
"arguments": null,
"codes": [
"^134[0-8]\\d{7}$|^13[^4]\\d{8}$|^14[5-9]\\d{8}$|^15[^4]\\d{8}$|^16[6]\\d{8}$|^17[0-8]\\d{8}$|^18[\\d]{9}$|^19[8,9]\\d{8}$"
]
}
],
"defaultMessage": "电话号码格式不正确",
"objectName": "testRequest",
"field": "phone",
"rejectedValue": "12345678911",
"bindingFailure": false,
"code": "Pattern"
},
{
"codes": [
"Length.testRequest.name",
"Length.name",
"Length.java.lang.String",
"Length"
],
"arguments": [
{
"codes": [
"testRequest.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
},
6,
1
],
"defaultMessage": "姓名长度必须在1-6之间",
"objectName": "testRequest",
"field": "name",
"rejectedValue": "",
"bindingFailure": false,
"code": "Length"
}
],
"message": "Validation failed for object='testRequest'. Error count: 3",
"path": "/user/test"
}
根据结果可知,每个参数都得到了应有的校验,这里由于我们没有统一的异常处理,所以看起来返回结果不是很友好,通常情况下我们在项目中都会做统一的异常处理。再改造一下,代码如下所示:
package com.yezi.springboot219.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Description:
* @Author: yezi
* @Date: 2020/6/8 11:10
*/
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger("GlobalExceptionHandler");
@ExceptionHandler
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public String defaultExceptionHandler(Throwable ex) {
logger.error("error:", ex);
return "FAIL";
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public String validationExceptionHandle(MethodArgumentNotValidException ex) {
logger.error("error:", ex);
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
List<String> msgList = fieldErrors.stream().map(FieldError::getDefaultMessage).collect(Collectors.toList());
return String.join(",", msgList);
}
}
再测试一下:
这样看起来就很清晰了。这里只是为了演示,没有做统一的返回结果封装,在实际开发中,通常会将返回的结果封装成json格式,不同的错误码对应不同的提示,方便前后端开发对接。这里就不再做进一步扩展。
@Valid与@Validated
这2个注解有什么区别呢?
首先看一下他们所属的包:
- @Validated :
org.springframework.validation.annotation.Validated
- @Valid:
javax.validation.Valid
可以看到@Validated属于spring,而@Valid属于javax。
但是在实际的基本使用中,这2者是没有区别的(注意这里说的是基本使用,也就是说,使用@Valid与@Validated是等价的)。
@PostMapping("/test")
public String test(@RequestBody @Validated TestRequest request) {
return "SUCCESS";
}
将注解替换一下,也能得到相同的结果:
看下2个注解的源码
- @Valid
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid {
}
- @Validated
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
Class<?>[] value() default {};
}
通过源码可以看到@Validated可以在类上面使用,并且多了一个value属性,那么这个value属性是干什么的?查看Spring Validation的文档可知,@Validated提供了一个分组功能,在校验参数时,可以根据不同的分组采用不同的校验机制。没有添加分组属性时,默认验证没有分组的验证属性。
分组校验
编写校验分组标识:
package com.yezi.springboot219.bean;
import javax.validation.groups.Default;
/**
* @Description: 校验分组组
* @Author: yezi
* @Date: 2020/6/8 13:19
*/
public class ValidatedGroup {
public interface Add extends Default {}
public interface Delete extends Default {}
public interface Update extends Default {}
public interface Query extends Default {}
}
测试请求类:
package com.yezi.springboot219.controller.request;
import com.yezi.springboot219.bean.ValidatedGroup;
import lombok.Data;
import javax.validation.constraints.*;
import java.math.BigDecimal;
/**
* @Description:
* @Author: yezi
* @Date: 2020/6/8 13:25
*/
@Data
public class TestSaveRequest {
@NotNull(groups = {ValidatedGroup.Update.class, ValidatedGroup.Delete.class}, message = "更新或者删除时id不能为空")
private Long id;
@NotBlank(groups = {ValidatedGroup.Update.class}, message = "更新时姓名不能为空")
private String name;
@NotNull(groups = {ValidatedGroup.Add.class}, message = "新增时余额不能为空")
@Digits(integer = 10, fraction = 2)
private BigDecimal balance;
@NotBlank(groups = {ValidatedGroup.Add.class}, message = "新增时电话不能为空")
@Pattern(regexp = "^134[0-8]\\d{7}$|^13[^4]\\d{8}$|^14[5-9]\\d{8}$|^15[^4]\\d{8}$|^16[6]\\d{8}$|^17[0-8]\\d{8}$|^18[\\d]{9}$|^19[8,9]\\d{8}$", message = "电话号码格式不正确")
private String phone;
@NotBlank(groups = {ValidatedGroup.Add.class}, message = "新增时邮件不能为空")
@Email
private String email;
}
根据我们编写的测试请求类型可预期:
- 校验分组为Add时,会校验balance、phone、email三个请求字段
- 校验分组为Update时,会校验id、name字段
测试如下:
@PostMapping("/testAdd")
public String testAdd(@RequestBody @Validated(value = ValidatedGroup.Add.class) TestSaveRequest request) {
return "SUCCESS";
}
@PostMapping("/testUpdate")
public String testUpdate(@RequestBody @Validated(value = ValidatedGroup.Update.class) TestSaveRequest request) {
return "SUCCESS";
}
testAdd结果:
testUpdate结果:
结果与预期完全符合。@Validated注解在分组校验时候,可以节省很多额外的开发,特别是当新增和更新时,一个需要传递主键一个不需要传递主键的情形,以前需要些一个AddRequest一个UpdateRequest,现在只有需要一个就够了。
嵌套校验
什么是嵌套校验呢?上代码:
package com.yezi.springboot219.controller.request;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* @Description:
* @Author: yezi
* @Date: 2020/6/8 13:25
*/
@Data
public class TestNestRequest {
@NotNull(message = "id不能为空")
private Long id;
@NotNull(message = "嵌套对象请求数据不能为空")
private ItemRequest itemRequest;
}
package com.yezi.springboot219.controller.request;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
/**
* @Description:
* @Author: yezi
* @Date: 2020/6/8 13:50
*/
@Data
public class ItemRequest {
@NotEmpty(message = "name不能为空")
private String name;
}
@PostMapping("/testNest")
public String testNest(@RequestBody @Valid TestNestRequest request) {
return "SUCCESS";
}
测试结果如下:
只校验了id,没有校验嵌套对象中的name属性,如果需要校验嵌套对象中的属性,需要改造一下TestNestRequest类,在itemRequest属性上加上@Valid注解,方能校验嵌套对象中的属性
package com.yezi.springboot219.controller.request;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
/**
* @Description:
* @Author: yezi
* @Date: 2020/6/8 13:25
*/
@Data
public class TestNestRequest {
@NotNull(message = "id不能为空")
private Long id;
@Valid
@NotNull(message = "嵌套对象请求数据不能为空")
private ItemRequest itemRequest;
}
测试结果:
一定要注意, 嵌套验证必须用@Valid注解。