一、引子
要对方法的参数进行校验,最简单暴力的写法是这个样子:
public static void utilA(String a,BigDecimal b){
if (StringUtils.isEmpty(a)){
System.out.println("a不可为空");
return;
}
if (b == null){
System.out.println("b不可为空");
return;
}
if (b.compareTo(BigDecimal.ZERO) != 1){
System.out.println("b的取值范围不正确");
return;
}
System.out.println("do something");
}
这样做从功能角度来说一点问题也没有。
但是从代码的长期维护性上来说,代码复用率低,校验规则一旦多起来很难维护,而且怎么看怎么显得笨拙,对于有一点追求的工程师来说,这么一大坨还是挺难接受的。
虽然有一些诸如 Preconditions(com.google) 的解决方案,但很难适应所有的场景,用起来也没到非常得心应有的地步。
二、如何优雅地校验参数
下面介绍Spring官方推荐的,语义清晰的优雅的方法级别校验(入参校验、返回值校验)
2-1、“官方指导意见”
Spring官方在SpringBoot文档中,关于参数校验(Validation)给出的解决方案是这样的:
- Validation
The method validation feature supported by Bean Validation 1.1 is automatically enabled as long as a JSR-303 implementation (such as Hibernate validator) is on the classpath. This lets bean methods be annotated with javax.validation constraints on their parameters and/or on their return value. Target classes with such annotated methods need to be annotated with the @Validated annotation at the type level for their methods to be searched for inline constraint annotations.
For instance, the following service triggers the validation of the first argument, making sure its size is between 8 and 10:
@Service
@Validated
public class MyBean {
public Archive findByCodeAndAuthor(@Size(min = 8, max = 10) String code,
Author author) {
...
}
}
Spring Boot 官网文档 《37. Validation》
也就是说,使用 JSR-303 规范,直接利用注解进行参数校验。
(JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation,官方参考实现是 Hibernate Validator)
2-2、注解用法说明
- 对于简单类型参数(非Bean),直接在参数前,使用注解添加约束规则。比如 @NotNull @Length 等(参看上面的官网示例)
- 在类名前追加 @Validated 注解,否则添加的约束规则不生效。
- 方法被调用时,如果传入的实际参数与约束规则不符,会直接抛出 ConstraintViolationException ,表明参数校验失败
- 对于Bean类型的参数,在Bean内部的各个字段上面追加约束注解,然后在方法的参数前面添加 @Valid 注解即可。示例:
public class CreateProjectReqVO extends BaseVO {
/**
* 请求序列号
*/
@NotNull(message = "请求序列号不可为空")
private Integer requestNo;
/**
* 项目名称
*/
@NotNull(message = "项目名称不可为空")
private String projectName;
……
}
/**
* 项目创建
*/
public CreateProjectRespVO createProject(@Valid CreateProjectReqVO reqVO) {
……
对于Bean里面套Bean的,同样在外层Bean里面写@Valid即可。
- 常用校验注解
@AssertTrue / @AssertFalse
验证适用字段:boolean
注解说明:验证值是否为true / false
@DecimalMax / @DecimalMin
验证适用字段:BigDecimal,BigInteger,String,byte,short,int,long
注解说明:验证值是否小于或者等于指定的小数值,要注意小数存在精度问题
@Digits
验证适用字段:BigDecimal,BigInteger,String,byte,short,int,long
注解说明:验证值的数字构成是否合法
属性说明:integer:指定整数部分的数字的位数。fraction: 指定小数部分的数字的位数。
@Future / @Past
验证适用字段:Date,Calendar
注解说明:验证值是否在当前时间之后 / 之前
属性说明:公共
@Max / @Min
验证适用字段:BigDecimal,BigInteger,String,byte,short,int,long
注解说明:验证值是否小于或者等于指定的整数值
属性说明:公共
注意事项:建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单提交的值为“”时无法转换为int
@NotNull / @Null
验证适用字段:引用数据类型
注解说明:验证值是否为非空 / 空
属性说明:公共
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为Null或者是EMPTY.
@NotBlank 与 @NotEmpty 的区别:空格(" ")对于 NotEmpty 是合法的,而 NotBlank 会抛出校验异常
@Pattern
验证适用字段:String
注解说明:验证值是否配备正则表达式
属性说明:regexp:正则表达式flags: 指定Pattern.Flag 的数组,表示正则表达式的相关选项。
@Size
验证适用字段:String,Collection,Map,数组
注解说明:验证值是否满足长度要求
属性说明:max:指定最大长度,min:指定最小长度。
@Length(min=, max=):专门应用于String类型
@Valid
验证适用字段:递归的对关联对象进行校验
注解说明:如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验(是否进行递归验证)
属性说明:无
@Range(min=, max=) 被指定的元素必须在合适的范围内
@CreditCardNumber信用卡验证
@Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
@URL(protocol=,host=, port=,regexp=, flags=)
2-3、进阶用法提示
校验规则是可以自己定义的,也就是说可以自己写注解类。具体写法本文略
约束规则可以“分组”(group),比如在A场景下只需要一部分规则生效,B场景下需要全部生效,那么直接在注解上指定生效范围即可。具体写法本文略
方法的返回值也是可以校验的,在方法前面追加约束规则即可:
public @NotNull CreateProjectRespVO createProject(CreateProjectReqVO reqVO) {
- 约束用的注解,一般需要带上message参数,方便自定义错误信息,比如前面例子中的“@NotNull(message = "项目名称不可为空")”。而这个message参数是支持EL表达式的
@NotNull(message = "${member.id.null}")
再定义一个比如叫做 messages.properties 的配置文件来统一管理错误信息
member.id.null=用户编号不能为空
约束规则支持正则表达式。具体写法本文略
支持跨参数校验(即通过直接写注解的方式验证多个参数之间的逻辑关系)
三、需要注意的坑
注解不能(只)放在实现类上
还是之前举的例子,如果是这么写的话
public interface IProjectService {
/**
* 项目创建
*/
CreateProjectRespVO createProject(CreateProjectReqVO reqVO);
@Service
public class ProjectServiceImpl implements IProjectService {
/**
* 项目创建
*/
@Override
public CreateProjectRespVO createProject(@Valid CreateProjectReqVO reqVO) {
……
在进行校验时会发生“javax.validation.ConstraintDeclarationException”异常(注意跟校验不通过发生的异常不是一个)
解决方法:
@Override父类/接口的方法,入参约束只能写在父类/接口上面。
或者两边都写上也可(但是这样维护时容易出问题,不推荐)。
另外 @Validated 这个注解写在哪边都可以。
四、如何优雅地处理校验结果
知道怎么写注解了,这事还不算完。
使用上面的方式做校验,当校验不通过时,是通过抛出异常(ConstraintViolationException)进行反馈的。所以异常怎么处理也是必须要考虑的问题。
尤其是需要向调用方正常返回处理结果的时候,无论如何也不能直接返一个e.printStackTrace()吧。
否则就白“优雅”了。
4-1、方法一:改为手动触发校验
首先,去掉类上面的@Validated注解,将自动触发改为手动触发。
然后,为了全工程公用,注入一个校验器
@Configuration
public class ProjectBeanFactory {
@Bean(name = "projectValidator")
public Validator getValidator() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
return factory.getValidator();
}
}
之后,追加一个公共方法
@Autowired
@Qualifier("projectValidator")
private Validator validator;
/**
* 根据VO注解检查入参
*/
private <T> RespVO checkParam(T t) {
RespVO result = new RespVO();
// 参数校验
Set<ConstraintViolation<T>> violations = validator.validate(t);
if (violations.size() > 0) {
Set<String> messages = new HashSet<>(violations.size());
result.setResult(RespCodeEnum.PARAMS_CHECK_FAIL.getCode());
messages.addAll(violations.stream()
.map(violation -> String.format("%s ( '%s' ): %s", violation.getPropertyPath().toString(),
violation.getInvalidValue(), violation.getMessage()))
.collect(Collectors.toList()));
result.setMsg(messages.toString());
return result;
}
result.setResult(RespCodeEnum.SUCCESS.getCode());
result.setMsg(RespCodeEnum.SUCCESS.getDesc());
return result;
}
在需要做参数校验的方法里面,调用这个共通处理即可
- 缺点:无法校验简单类型参数(非Bean的参数);无法校验返回值
4-2、方法二:自定义ExceptionHandler拦截校验异常
@ControllerAdvice
public class ValidationExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
ResponseEntity<Set<String>> handleConstraintViolation(ConstraintViolationException e) {
Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
Set<String> messages = new HashSet<>(constraintViolations.size());
messages.addAll(constraintViolations.stream()
.map(violation -> String.format("%s ( '%s' ): %s", violation.getPropertyPath().toString(),
violation.getInvalidValue(), violation.getMessage()))
.collect(Collectors.toList()));
return new ResponseEntity<>(messages, HttpStatus.BAD_REQUEST);
}
}