前言
本文Spring版本为 SpringBoot-2.0.7,所有源码相关类、方法、代码行都以此版本为基础。
代码行数: 使用 IDEA 的同学通过Maven Projects -> Donwload Sources and Documentation
下载源码及注释文档,保证行数的准确。非常欢迎您指正在文章中出现的错误,包括但不限于 语句错误、描述错误、示例错误、代码理解错误。
参数校验是代码开发中必不可少的一环,一个方法中参数校验套了一个又一个 if-else,繁琐的操作让广大程序员诟病。
本文我们就讲一下 SpringBoot 结合 Hibernate-Validtor 校验参数、简化工作。
开始
spring-boot-starter-web 已经默认整合、提供了 Hibernate-Validator 的功能,只待我们去使用。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.7.RELEASE</version>
</dependency>
创建一个实体类,并添加校验注解。
本小节只做简单的使用演示。
常用注解列表、注解说明、注解用法,以及·自定义校验注解·的教程。JSR 303 - Bean Validation 介绍及最佳实践
public class Student{
@NotNull
private String name;
@NotNull
private String sex;
@Min(0)
@Max(150)
private int age;
...get,set...
}
接着编写 Controller 代码。
// @RestController
// DemoController
@GetMapping("/student")
public String validator(@Validated Student student, BindingResult result) {
if (result.hasErrors()) {
return result.getFieldError().getDefaultMessage();
}
return "ok";
}
启动程序后访问http://{host:prot}/student
,将会返回:
must not be null
访问http://{host:prot}/student?name=zhangsan&sex=Male&age=22
,将会返回:
ok
到这,本期的教程结束...是不可能的。
进阶
上面的教程还太简单,很多事情都很朦胧。
- 书写有没有什么规则?
- 我怎么知道‘must not be null’是指哪个参数?跟没提示一样。
- 每个 Controller 方法都要判断 BindingResult 还是好麻烦!我懒得写!
- 校验规则太少了,能不能自己写规则?
- 我想手动校验怎么办?
书写规则
@Validated 和 @Valid 的异同
@Validated 是 Spring 实现的JSR-303的变体 @Valid ,支持验证组的规范。 设计用于方便使用Spring的JSR-303支持,但不支持JSR-303特定。
@Valid JSR-303标准实现的校验注解。
注解 | 范围 | 嵌套 | 校验组 |
---|---|---|---|
@Validated | 可以标记类、方法、方法参数,不能用在成员属性(字段)上 | 不支持 | 支持 |
@Valid | 可以标记方法、构造函数、方法参数和成员属性(字段)上 | 支持 | 不支持 |
两者都可以用在方法入参上,但都无法单独提供嵌套验证功能,都能配合嵌套验证注解@Valid进行嵌套验证。
嵌套验证示例:
public class ClassRoom{
@NotNull
String name;
@Valid // 嵌套校验,校验参数内部的属性
@NotNull
Student student;
}
@GetMapping("/room") // 此处可使用 @Valid 或 @Validated, 将会进行嵌套校验
public String validator(@Validated ClassRoom classRoom, BindingResult result) {
if (result.hasErrors()) {
return result.getFieldError().getDefaultMessage();
}
return "ok";
}
BindingResult 的使用
BindingResult
必须跟在被校验参数之后,若被校验参数之后没有BindingResult
对象,将会抛出BindException
。
@GetMapping("/room")
public String validator(@Validated ClassRoom classRoom, BindingResult result) {
if (result.hasErrors()) {
return result.getFieldError().getDefaultMessage();
}
return "ok";
}
不要使用 BindingResult
接收,String等简单对象的错误信息。简单对象校验失败,会抛出 ConstraintViolationException
。
主要就是接不着,你要写也算是没关系...
// ❌ 错误用法,也没有特别的错,只是 result 是接不到值。
@GetMapping("/room")
@Validated // 启用校验
public String validator(@NotNull String name, BindingResult result) {
if (result.hasErrors()) {
return result.getFieldError().getDefaultMessage();
}
return "ok";
}
修改校验失败的提示信息
可以通过各个校验注解的message
属性设置更友好的提示信息。
public class ClassRoom{
@NotNull(message = "Classroom name must not be null")
String name;
@Valid
@NotNull
Student student;
}
@GetMapping("/room")
@Validated
public String validator(ClassRoom classRoom, BindingResult result, @NotNull(message = "姓名不能为空") String name) {
if (result.hasErrors()) {
return result.getFieldError().getDefaultMessage();
}
return "ok";
}
message
属性配置国际化的消息也可以的,message
中填写国际化消息的code
,在抛出异常时根据code
处理一下就好了。
@GetMapping("/room")
@Validated
public String validator(@NotNull(message = "demo.message.notnull") String name) {
if (result.hasErrors()) {
return result.getFieldError().getDefaultMessage();
}
return "ok";
}
// message_zh_CN.properties
demo.message.notnull=xxx消息不能为空
// message_en_US.properties
demo.message.notnull=xxx message must no be null
省略 Controller 中的校验判断
可以利用参数校验失败后抛出异常这点,配置·统一异常拦截·,进行异常统一的处理,合理的将错误信息返回给前端。
抛砖(仅做示例):
// @RestControllerAdvice
/* 数据校验处理 */
@ExceptionHandler({BindException.class, ConstraintViolationException.class})
public String validatorExceptionHandler(Exception e) {
String msg = e instanceof BindException ? msgConvertor(((BindException) e).getBindingResult())
: msgConvertor(((ConstraintViolationException) e).getConstraintViolations());
return msg;
}
/**
* 校验消息转换拼接
*
* @param bindingResult
* @return
*/
public static String msgConvertor(BindingResult bindingResult) {
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
StringBuilder sb = new StringBuilder();
fieldErrors.forEach(fieldError -> sb.append(fieldError.getDefaultMessage()).append(","));
return sb.deleteCharAt(sb.length() - 1).toString().toLowerCase();
}
private String msgConvertor(Set<ConstraintViolation<?>> constraintViolations) {
StringBuilder sb = new StringBuilder();
constraintViolations.forEach(violation -> sb.append(violation.getMessage()).append(","));
return sb.deleteCharAt(sb.length() - 1).toString().toLowerCase();
}
注:getMessage
和getDefaultMessage
都是直接获取注解上message
属性的值,
扩展校验注解、校验规则
常用注解列表、注解说明、注解用法,以及·自定义校验注解·的教程。JSR 303 - Bean Validation 介绍及最佳实践
手动校验
若没有手动配置Validator
对象,自然需要从 Spring 容器中获取校验器对象,注入使用。
此处给出一个手动校验的工具类,供大家参考。(lay了...写的自闭,如果对代码有疑问请联系我..持续更新)
代码中提到的与 Spring 集成,主要是对代码返回值的统一。(不支持普通对象...)
若都以注解的message
属性来获取提示消息,可以删除 Spring 相关的代码。
若不以message
属性作为消息,那么可以从bindingResult
中获取字段、类、注解信息,拼装成消息码。
抛砖:
// config
// @Configuration
@Bean
public Validator validator() {
return ValidatorUtils.getValidator();
}
import org.hibernate.validator.HibernateValidator;
import org.springframework.util.ClassUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.DataBinder;
import org.springframework.validation.SmartValidator;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;
/**
* hibernate-validator校验工具类
*/
public class ValidatorUtils {
private static Validator validator;
private static SmartValidator validatorAdapter;
static {
// 快速返回模式
validator = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory()
.getValidator();
}
public static Validator getValidator() {
return validator;
}
private static SmartValidator getValidatorAdapter(Validator validator) {
if (validatorAdapter == null) {
validatorAdapter = new SpringValidatorAdapter(validator);
}
return validatorAdapter;
}
/**
* 校验参数,用于普通参数校验 [未测试!]
*
* @param
*/
public static void validateParams(Object... params) {
Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(params);
if (!constraintViolationSet.isEmpty()) {
throw new ConstraintViolationException(constraintViolationSet);
}
}
/**
* 校验对象
*
* @param object
* @param groups
* @param <T>
*/
public static <T> void validate(T object, Class<?>... groups) {
Set<ConstraintViolation<T>> constraintViolationSet = validator.validate(object, groups);
if (!constraintViolationSet.isEmpty()) {
throw new ConstraintViolationException(constraintViolationSet);
}
}
/**
* 校验对象
* 使用与 Spring 集成的校验方式。
*
* @param object 待校验对象
* @param groups 待校验的组
* @throws BindException
*/
public static <T> void validateBySpring(T object, Class<?>... groups)
throws BindException {
DataBinder dataBinder = getBinder(object);
dataBinder.validate((Object[]) groups);
if (dataBinder.getBindingResult().hasErrors()) {
throw new BindException(dataBinder.getBindingResult());
}
}
private static <T> DataBinder getBinder(T object) {
DataBinder dataBinder = new DataBinder(object, ClassUtils.getShortName(object.getClass()));
dataBinder.setValidator(getValidatorAdapter(validator));
return dataBinder;
}
}
源码经验宝宝[拓展]
为什么 BindingResult
接收不到简单对象的校验信息?
跟进 Spring MVC 源码,发现:SpringMVC 在进行方法参数的注入(将 Http请求参数封装成方法所需的参数)时,不同的对象使用不同的解析器注入对象。
听着好像没什么关系。但其实就是,注入实体对象时使用ModelAttributeMethodProcessor
中的校验方法,而注入 String 对象使用AbstractNamedValueMethodArgumentResolver
中的校验方法。正是这个差异导致了BindingResult
无法接受到简单对象(简单的入参参数类型)的校验信息。
啊?你问我什么是简单对象?emm...
八大基础类型再加上不同解析器支持的类型对象(不同的参数类型),需要看各解析器实现的supportsParameter()
方法,文中提到的简单对象,意思是ModelAttributeMethodProcessor
不支持的所有对象。
获取参数注入解析器
的源码位于HandlerMethodArgumentResolverComposite#resolveArgument():120
:
// HandlerMethodArgumentResolverComposite.class
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 获取 parameter 参数的解析器
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
// 调用解析器获取参数
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
// 获取 parameter 参数的解析器
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
// 从缓存中获取参数对应的解析器
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
// 解析器是否支持该参数类型
if (methodArgumentResolver.supportsParameter(parameter)) {
result = methodArgumentResolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
return result;
}
注入 String 参数时,在AbstractNamedValueMethodArgumentResolver#resolveArgument()
中,不会抛出BindException/ConstraintViolationException
异常、也不会将 BindingResult 传入到方法中。
注入对象时在ModelAttributeMethodProcessor#resolveArgument():154
行的 validateIfApplicable(binder, parameter)
语句,进行了参数校验,校验不通过并且实体对象后不存在BindingResult
对象,则会在this#resolveArgument():156
抛出BindException
。
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// bean 参数绑定和校验
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
// 参数校验
validateIfApplicable(binder, parameter);
// 校验结果包含错误,并且该对象后不存在 BindingResult 对象,就抛出异常
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
// 在对象后注入 BindingResult 对象
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
}
在哪里抛出ConstraintViolationException
?
可能有同学发现了,简单对象注入后并没有抛出异常,那这个参数在哪里被校验呢?
被方法级的拦截器拦住了。
这里的方法拦截器是 MethodValidationInterceptor
:
// MethodValidationInterceptor.class
public Object invoke(MethodInvocation invocation) throws Throwable {
ExecutableValidator execVal = this.validator.forExecutables();
// 校验参数
try {
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
// 解决参数错误异常、再次校验
methodToValidate = BridgeMethodResolver.findBridgedMethod(
ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
// 执行结果
Object returnValue = invocation.proceed();
// 校验返回值
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
over.
本文到此结束。
非常欢迎您指正在文章中出现的错误,包括但不限于 语句错误、描述错误、示例错误、代码理解错误。