使用spring-validation和@RequestParam(required = false)字符串默认值的校验问题

众所周知,使用@RequestParam(required = false) 封装请求参数的时候,如果客户端不提交参数,或者是只声明参数,并不赋值。那么方法的形参值,默认为null(基本数据类型除外)。

一个Controller方法,有2个参数

@GetMapping
public Object update(@RequestParam(value = "number", required = false) Integer number,
				@RequestParam(value = "phone", required = false) String phone) {
	LOGGER.info("number={}, phone={}", number, phone);
	return Message.success(phone);
}

很简单的一个Controller方法。有两个参数,都不是必须的。只是这俩参数的数据类型不同。

// 都不声明参数
http://localhost:8080/test
日志输出:number=null, phone=null

// 都只声明参数,但是不赋值
http://localhost:8080/test?number=&phone=
日志输出出:number=null, phone=

这里可以看出,String类型的参数。在声明,不赋值的情况下。默认值为空字符串。

使用spring-validation遇到@RequestParam(required = false)字符串参数的问题

一个验证手机号码的注解

极其简单,通过正则验证字符串是否是手机号码

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.Pattern;


@Retention(RUNTIME)
@Target(value = { ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@Constraint(validatedBy = {})
@ReportAsSingleViolation
@Pattern(regexp = "^1[3-9]\\d{9}$")
public @interface Phone {
	String message() default "手机号码不正确,只支持大陆手机号码";
	Class<?>[] groups() default {};
	Class<? extends Payload>[] payload() default {};
}

一般这样使用

private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);

@GetMapping
public Object update(@RequestParam(value = "number", required = false) Integer number,
				@RequestParam(value = "phone", required = false) @Phone String phone) {
	LOGGER.info("number={}, phone={}", number, phone);
	return Message.success(phone);
}

这是一个修改接口,允许用户修改自己的手机号码,但手机号码并不是必须的,允许以空字符串的形式存储在数据库。通俗的说就是,phone参数,要么是一个合法的手机号码。要么是空字符串,或者null。

客户端发起了请求

// 假如用户什么也不输入,清空了 phone 输入框,客户端js序列化表单后提交。
http://localhost:8080/test?number=&phone=

果然得到了异常:
javax.validation.ConstraintViolationException: update.phone: 手机号码不正确,只支持大陆手机号码

很显然,空字符串 “”,并不符合手机号码的正则校验。

这种情况就是,在校验规则,和默认值之间,出现了一点点冲突

解决办法

求前端大哥改巴改巴

提交之前,遍历一下请求参数。把空值参数,从请求体中移除。那么后端接收到的形参就是,null。是业务可以接受的数据类型。

修改验证规则

这个也不算难,自己修改一下验证的正则,或者重新实现一个自定义的 ConstraintValidator,允许手机号码为空字符串。但是,也有一个问题,这个注解就不能用在必填的手机号码参数上了。例如:注册业务,因为它的规则是允许空字符串的。

当然,也可以维护多个不同的验证规则注解。

@Phone 验证必须是标准手机号码
@Retention(RUNTIME)
@Target(value = { ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@Constraint(validatedBy = {})
@ReportAsSingleViolation
@Pattern(regexp = "^1[3-9]\\d{9}$")
public @interface Phone {
	String message() default "手机号码不正确,只支持大陆手机号码";
	Class<?>[] groups() default {};
	Class<? extends Payload>[] payload() default {};
}
@PhoneOrEmpty 可以是空字符串或者标准的手机号码
@Retention(RUNTIME)
@Target(value = { ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@Constraint(validatedBy = {})
@ReportAsSingleViolation
@Pattern(regexp = "^(1[3-9]\\d{9})|(.{0})$")  
public @interface PhoneOrEmpty {
	String message() default "手机号码不正确,只支持大陆手机号码";
	Class<?>[] groups() default {};
	Class<? extends Payload>[] payload() default {};
}

好了,这俩可以用在不同的验证地方。唯一的不同就是验证的正则不同。这也是让我觉得不舒服的地方。需要维护两个正则表达式

一种我认为比较"优雅"的方式

还是一样,定义不同的注解来处理不同的验证场景。但是,我并不选择自立门户(单独维护一个正则),而是在@Phone的基础上,进行一个加强

@Retention(RUNTIME)
@Target(value = { ElementType.FIELD, ElementType.PARAMETER })
@Constraint(validatedBy = {})
@ReportAsSingleViolation

@Phone						// 使用已有的@Phone作为校验规则,参数必须是一个合法的手机号码
@Length(max = 0, min = 0)	// 使用Hiberante提供的字符串长度校验规则,在这里,表示惨参数字符串的长度必须:最短0,最长0(就是空字符串)

@ConstraintComposition(CompositionType.OR)	// 核心的来了,这个注解表示“多个验证注解之间的逻辑关系”,这里使用“or”,满足任意即可
public @interface PhoneOrEmpty {
	String message() default "手机号码不正确,只支持大陆手机号码";
	Class<?>[] groups() default {};
	Class<? extends Payload>[] payload() default {};
}

核心的说明,都在上面的注释代码上了。

自定义使用组合Constraint,在原来@Phone的验证规则上,再添加一个 @Length(max = 0, min = 0)规则。使用@ConstraintComposition描述这两个验证规则的逻辑关系。

ConstraintComposition只有一个枚举属性

@Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface ConstraintComposition {
	/**
	 * The value of this element specifies the boolean operator,
	 * namely disjunction (OR), negation of the conjunction (ALL_FALSE),
	 * or, the default, simple conjunction (AND).
	 *
	 * @return the {@code CompositionType} value
	 */
	CompositionType value() default AND;
}
public enum CompositionType {
	OR,   // 多个验证规则中,只要有一个通过就算验证成功
	AND,  // 多个验证规则中,必须全部通过才算成功(默认)
	ALL_FALSE // 多个验证规则中,必须全部失败,才算通过(少见)
}

试试看

Controller

@GetMapping
public Object update (@RequestParam(value = "number", required = false) Integer number,
				@RequestParam(value = "phone", required = false) @PhoneOrEmpty String phone) {
	LOGGER.info("number={}, phone={}", number, phone);
	return Message.success(phone);
}

空参数:空字符串,是合法的

image

合法参数:更是合法

image

非法参数:验证失败

image

1赞