众所周知,使用@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);
}