有参数传递的地方都少不了参数校验。在web开发中,前端的参数校验是为了用户体验,后端的参数校验是为了安全。
试想一下,如果在controller层中没有经过任何校验的参数通过service层、dao层一路来到了数据库就可能导致严重的后果,最好的结果是查不出数据,
严重一点就是报错,如果这些没有被校验的参数中包含了恶意代码,那就可能导致更严重的后果。
因此,对于请求参数,一般上都需要进行参数合法性校验的,原先的写法时一个个字段一个个去判断,这种方式太不通用了,并且当参数较多的时候,代码回显的很臃肿,例如:
这是一个 bad practise !
所以java的JSR 303: Bean Validation
规范就是解决这个问题的。
JSR 303
只是个规范,并没有具体的实现,目前通常都是由hibernate-validator
进行统一参数校验。
JSR303定义的基础校验类型:
Constraint
|
详细信息
|
---|---|
@Null |
被注释的元素必须为 null |
@NotNull |
被注释的元素必须不为 null |
@AssertTrue |
被注释的元素必须为 true |
@AssertFalse |
被注释的元素必须为 false |
@Min(value) |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) |
被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) |
被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past |
被注释的元素必须是一个过去的日期 |
@Future |
被注释的元素必须是一个将来的日期 |
@Pattern(value) |
被注释的元素必须符合指定的正则表达式 |
Hibernate Validator 中附加的 constraint :
Constraint
|
详细信息
|
---|---|
@Email |
被注释的元素必须是电子邮箱地址 |
@Length |
被注释的字符串的大小必须在指定的范围内 |
@NotEmpty |
被注释的字符串的必须非空 |
@Range |
被注释的元素必须在合适的范围内 |
下面通过一个例子来使用一下:
@Data
@ApiModel
(
"回帖风控放行后发短消息提醒"
)
public
class
PostCheckPassDTO {
@NotNull
(
message =
"回帖用户puid不能为空"
)
@ApiModelProperty
(
"回帖用户puid,必传"
)
private
Long puid;
@NotNull
(
message =
"回帖用户username不能为空"
)
@ApiModelProperty
(
"回帖用户username,必传"
)
private
String username;
@NotNull
(
message =
"帖子id不能为空"
)
@ApiModelProperty
(
"帖子id,必传"
)
private
Long tid;
@NotNull
(
message =
"回帖id不能为空"
)
@ApiModelProperty
(
"回帖id"
)
private
Long pid;
@NotNull
(
message =
"回帖内容不能为空"
)
@ApiModelProperty
(
"回帖内容"
)
private
String content;
@ApiModelProperty
(
"回帖引用回帖id"
)
private
Long quote;
}
|
要注意加上@Valid注解开启校验
@PostMapping
(
"sendRemindForCheckPassPosts"
)
public
DataResult<Boolean> sendRemindForCheckPassPosts(
@Valid
@RequestBody
PostCheckPassDTO postCheckPassDTO){
userBbsWriteService.sendRemindForCheckPassPosts(postCheckPassDTO);
return
DataResult.ok(
true
);
}
|
这时候我们把参数里面的username去掉,请求一下接口,响应结果如下:
看到在校验不通过时,返回的异常信息是不友好的,此时可利用统一异常处理,对校验异常进行特殊处理,
特别说明下,对于异常处理类,共有以下几种情况(被@RequestBody和@RequestParam注解的请求实体,校验异常类是不同的),上代码:
@Slf4j
@ControllerAdvice
public
class
GlobalExceptionHandler {
/**
* 异常处理
* @param e
* @return
*/
@ResponseBody
@ExceptionHandler
(value = Exception.
class
)
public
DataResult exceptionHandler(Exception e) {
log.error(
"GlobalExceptionHandler.exceptionHandler , 异常信息"
,e);
/**cat error 上报*/
Cat.logErrorWithCategory(
"GlobalExceptionHandler"
,
"global error"
,e);
return
DataResult.fail(e.getMessage());
}
/**
* 业务异常
* @param e
* @return
*/
@ResponseBody
@ExceptionHandler
(value = BplCommonException.
class
)
public
DataResult bplCommonExceptionHandler(BplCommonException e) {
log.warn(
""
,e);
return
DataResult.fail(e.getMessage());
}
/**
* 处理所有RequestBody注解参数验证异常
* @param e
* @return
*/
@ExceptionHandler
(MethodArgumentNotValidException.
class
)
@ResponseBody
public
DataResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
log.warn(e.getMessage());
return
DataResult.fail(e.getBindingResult().getAllErrors().get(
0
).getDefaultMessage());
}
/**
* 处理所有RequestParam注解数据验证异常
* @param ex
* @return
*/
@ExceptionHandler
(BindException.
class
)
public
DataResult handleBindException(BindException ex) {
FieldError fieldError = ex.getBindingResult().getFieldError();
log.warn(
"必填校验异常:{}({})"
, fieldError.getDefaultMessage(),fieldError.getField());
return
DataResult.fail(ex.getBindingResult().getAllErrors().get(
0
).getDefaultMessage());
}
}
|
添加全局异常处理器后,这时我们再次请求接口,看看相应结果:
上面是最通用也是最常见的使用方式,可以解决绝大部分参数校验的问题。还有剩下的小部分,我们可以通过自定义校验注解的方式解决, 比如我们定义一个检查版本号的注解:
@Documented
//指定注解的处理类
@Constraint
(validatedBy = {VersionValidatorHandler.
class
})
@Target
({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention
(RUNTIME)
public
@interface
ConstantVersion {
String message()
default
"{constraint.default.const.message}"
;
Class<?>[] groups()
default
{};
Class<?
extends
Payload>[] payload()
default
{};
String value();
}
|
注解处理类 VersionValidatorHandler
public
class
VersionValidatorHandler
implements
ConstraintValidator<Constant, String> {
private
String constant;
@Override
public
void
initialize(Constant constraintAnnotation) {
//获取设置的字段值
this
.constant = constraintAnnotation.value();
}
@Override
public
boolean
isValid(String value, ConstraintValidatorContext context) {
//判断参数是否等于设置的字段值,返回结果
return
constant.equals(value);
}
}
|
注解的使用:
@ConstantVersion
(message =
"verson只能为1.0.0"
,value=
"1.0.0"
)
String version;
|
Spring Validation实现原理分析