文:答应我,别再if/else校验请求参数了可以吗
注:中国传统文化,先仔细看,若有用,再点赞, 给自己一点思考的时间
注:微信搜索:CodeCow,关注这个非常 SAO 的程序员
哎!弹指之间
遥想当年,其实我也特别钟情于 if/else 连环写法,来 校验请求入参,上来就是一顿SAO操作:
就现在来说,我们项目都是前后端分离,前后端约定好请求参数,封装成一个对象,前段根据对象来传参,但传入的参数是否为空,怎么判断!
比如举个好理解的简单例子:
请求参数
@Data
@ApiModel(value = "登录请求参数")
public class LoginReqDTO {
@ApiModelProperty(value = "手机号")
private String mobile;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "验证码")
private String code;
}
响应结果
@Data
public class RespResult<T> {
private Integer code;
private String message;
private T data;
}
当前端调用我们的登录接口时,我们需要 判断请求参数是否为空,这时候 SAO代码 出现了:
@RestController
public class LoginController{
@ApiOperation(value = "登录接口")
@PostMapping(value = "/login")
public RespResult login(LoginReqDTO loginReqDTO) {
if (StringUtils.isEmpty(loginReqDTO.getMobile())) {//判断手机号是否为空
return new RespResult(400, "手机号不能为空");
} else if (StringUtils.isEmpty(loginReqDTO.getPassword())) {//判断密码是否为空
return new RespResult(400, "密码不能为空");
} else if (StringUtils.isEmpty(loginReqDTO.getCode())) {//判断验证码是否为空
return new RespResult(400, "验证码不能为空");
} else {
return new RespResult(200, "成功");//我的吗,终于成功了 !
}
}
}
卧CAO!现在请求对象里的参数只有三个,当参数有几十个时,那几十个 if/else 嵌套,不累吗,那可以说是 非常酸爽了……
那么,问题来了:
第一点:你是爽了,别人一阅读(怕是上来就是一 Jao!)
第二点:现在是一个接口,那几十个接口时,那全屏可能只有 if/else了。(哟呵!腻害)
第三点:则是以后如果再复杂一点,或者想要再加条件的话,是不是还的整个if/else,极其不好扩展。
第四点:最后代码若一改,以前的老功能肯定还得重测,岂不疯了……
所以,如果在不看下文的情况下,你一般会如何去对付这些令人头痛的if/else语句呢?
当然有人会说用 循环语句switch/case 来判断是否会优雅一些呢?答案是:有锤子区别,毛区别都没有!
接下来简单讲几种改进方式,别再 if/else走天下了
有Boot自带的参数验证为啥不用
大家肯定学过 boot吧(没学过也不打紧,没吃过猪肉,还没见过猪跑吗!!),因此,为啥不用boot自带的 spring validation,也就是@Validated注解,为啥不用?
首先我们在请求参数上加上 @NotBlank注解,并定义为空时的 message
@Data
@ApiModel(value = "登录请求参数")
public class LoginReqDTO {
@NotBlank(message = "手机号不能为空")
@ApiModelProperty(value = "手机号")
private String mobile;
@NotBlank(message = "密码不能为空")
@ApiModelProperty(value = "密码")
private String password;
@NotBlank(message = "验证码不能为空")
@ApiModelProperty(value = "验证码")
private String code;
}
接下来我们将 请求参数是否为空 交给 spring validation 来判断,只需要定义一个 全局异常处理器来处理异常:
GlobalExceptionHandler :
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = BindException.class)
public RespResult violationException(BindException exception) {
// 不带任何参数访问接口,会抛出 BindException
// 因此,我们只需捕获这个异常,并返回我们设置的 message 即可
String message = exception.getAllErrors().get(0).getDefaultMessage();
return new RespResult(400, message);
}
}
接下来接口调用就变得非常简单了,加个@Validated注解就行了, if/else也灰飞烟灭了:
@RestController
public class LoginController {
@ApiOperation(value = "登录接口")
@PostMapping(value = "/login", consumes = "application/json", produces = "application/json")
public RespResult login(@Validated LoginReqDTO loginReqDTO) {
return new RespResult(200, "成功");
}
}
最后,我们用 空的请求参数 使用Postman访问下登录接口:localhost:8080/login;结果为:
{
"code": 400,
"message": "手机号不能为空",
"data": null
}
是不是感觉很爽,SAO代码( if/else)也没了。
而且,这样一来,假如以后我想扩充条件,只需要去“请求参数对象中添加一个@NotBlank(message = “XXX”)注解”即可,而不是去改以前的代码,这岂不很稳!
example:
@NotBlank(message = "XXX不能为空")
private String XXX;
稳是稳了,但是,假如请求参数不是对象怎么办?
比如,稍微老一点的项目请求参数可能是非实体,那此时的 if/else 是怎么样被 KO 的呢 !
别慌!此时我们只需要简单改造下GlobalExceptionHandler——全局异常处理器:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ConstraintViolationException.class})
public RespResult violationException(Exception exception) {
if (exception instanceof ConstraintViolationException) { //使用关键字instanceof 判断 ConstraintViolationException 是否为 Exception 直接或间接子类
return constraintViolationException((ConstraintViolationException) exception); //调用下面方法,返回结果
}
return new RespResult(500, "server error"); // 否则跑出 server error
}
// 当我们没有此方法,空参访问localhost:8080/login 会抛出ConstraintViolationException 异常
public RespResult constraintViolationException(ConstraintViolationException ex) {
Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
if (!CollectionUtils.isEmpty(constraintViolations)) { //判断是否为空
StringBuilder sb = new StringBuilder();
for (ConstraintViolation constraintViolation : constraintViolations) { //遍历 ConstraintViolation
sb.append(constraintViolation.getMessage()).append(","); // 吧错误信息循环放到sb中, 并以逗号隔开
}
String errorMessage = sb.toString(); // 获得异常信息字符串
return new RespResult(400, errorMessage);
}
return new RespResult(500, "server error"); // 否则跑出 server error
}
}
接下来,我们只需要在请求参数 前加上“** @NotBlank** ”注解,并且在接口所在类加上“ @Validated ” 即可,if/else 同样被 KO 了
@Validated // 此注解别忘了
@RestController
public class LoginController {
@ApiOperation(value = "登录接口")
@PostMapping(value = "/login", consumes = "application/json", produces = "application/json")
public RespResult login(@NotBlank(message = "手机号不能为空") String mobile,
@NotBlank(message = "密码不能为空") String password,
@NotBlank(message = "验证码不能为空") String code) {
return new RespResult(200, "成功");
}
}
我们再次用 空的请求参数 使用Postman访问下登录接口:localhost:8080/login;结果为:
{
"code": 400,
"message": "验证码不能为空,密码不能为空,手机号不能为空,",
"data": null
}
不难发现,把请求参数中所有为空的参数,都验证出来了,它不香吗!
然并卵,实际开发中,并非 3 + 2 - 5 * 0 这么简单
假如有需求,校验请求“手机号格式是否正确”,怎么办!别慌,自定义注解 登场
有自定义注解为啥不用
首先,我们要自定义注解,肯定的先了解注解的本质。
在「java.lang.annotation.Annotation」接口中有这么一句话,用来描述『注解』。
The common interface extended by all annotation types
译:所有的注解类型都继承自这个普通的接口(Annotation)
这句话有点抽象,但却说出了注解的本质。
来来来,我们随便看一个JDK内置注解的定义,比如咋们常用的:@Override
@Target(ElementType.METHOD) //注解放置的目标位置
@Retention(RetentionPolicy.SOURCE) //注解在哪个阶段执行
public @interface Override { //注解
}
不难看出,这是注解 @Override 的定义,其实它本质上就是:
public interface Override extends Annotation{ //继承 Annotation
}
你没有看错,注解的本质就是一个继承了 Annotation 接口的接口
小编有幸在书上看到这样一端描述:
- 一个注解准确意义上来说,只不过是一种特殊的注释而已,如果没有解析它的代码,它可能连注释都不如
注解小编就浅聊到这里了,咋们言归正传,回到 **使用自定义注解 KO if/else ** ,也别让 if/else 久等了。
首先,自定一个参数验证注解:@ValidParam
/**
* Create By CodeCow on 2020/7/21.
* 自定义注解
* @Target:注解放置的目标位置
* @Retention:注解在哪个阶段执行
* @Constraint:指定此注解的实现, 即:验证器(就是下面的:ParamValidator 类)
*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ParamValidator.class)
public @interface ValidParam {
String message() default "手机号格式不正确"; // 校验的失败的时候返回的信息,可以指定默认值
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
接下来我们 编写一个校验器 ,验证该注解
/**
* Create By CodeCow on 2020/7/21.
* 编写校验器 验证该上面我们自定义的注解:@ValidParam
* 我们需要 实现 ConstraintValidator 接口 并且重写 isValid 方法
*/
public class ParamValidator implements ConstraintValidator<ValidParam, String> {
@Override
public void initialize(ValidParam constraintAnnotation) {
// 初始化
}
@Override
public boolean isValid(String param, ConstraintValidatorContext constraintValidatorContext) {
// 开始验证
// 手机号格式(正则语法)
String mobileFormat = "^((13[0-9])|(14[5,7])|(15[^4,\\D])|(17[0,1,3,6-8])|" +
"(18[0-9])|(19[8,9])|(166))[0-9]{8}$";
Pattern pattern = Pattern.compile(mobileFormat);
return pattern.matcher(param).matches(); // 手机号格式正确返回 true,否则 false
}
}
最后,我们修改登录接口,在 请求参数 前,加上我们自定义的注解(@ValidParam)即可:
同样,请求参数的校验,只需一个注解, if/else就被 KO 了
@RestController
public class LoginController {
@ApiOperation(value = "登录接口")
@PostMapping(value = "/login", consumes = "application/json", produces = "application/json")
public RespResult login(@ValidParam String mobile) {
return new RespResult(200, "成功");
}
}
最后,我们再次以 空参数 使用Postman访问下登录接口:localhost:8080/login;结果为:
{
"code": 400,
"message": "手机号格式不正确,",
"data": null
}
后记
好啦,今就先聊到这里吧,本文仅仅是 抛砖引玉 而已,使用了现阶段大家比较“钟情的 if/else” 打了个样,不是说用 if/else 不好,只是希望大家在以后的编码中,不要滥用,代码不要太过“冗余”。
其次,在真实项目中,业务场景不可能像上面那么简单,也就像小编前文所提(实际开发,并非 3 + 2 - 5 * 0 这么简单),所以在您握住鼠标的那一刻,还是的多思考一番,考虑这样写是否合理,是否具有扩展性。
好文推荐
小声BB
- 中国传统文化,先仔细看,若有用,再点赞, 给自己一点思考的时间
- 更多幽默、风趣好文,尽在“ CodeCow ” 程序牛公众号