1、原生SpringBoot环境
环境信息
- SpringBoot : 2.3.8.RELEASE
- hibernate-validator : 6.1.7.Final
验证
hibernate-validator主要用于验证前段请求过来的参数是否满足条件
controller层:
@RestController @RequestMapping("valid") public class TestController { @PostMapping("/test") public Person test(@Valid @RequestBody Person person) { return person; } }
实体类
public class Person { @NotBlank() private String name; @Range(min = 2, max = 100) private Integer age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
从实体类中可以看出主要有两个验证条件
- person.name不能为空
- person.age在2~100之间
如果在此时用不满足条件的数据直接调用接口会返回404,看不到验证出错信息,这是因为Spring拦截了验证异常,直接返回了404
想要不返回404需要自定义异常拦截器,把验证异常自己来处理掉
@RestControllerAdvice public class GlobalExceptionHandler { @ResponseStatus(HttpStatus.OK) @ExceptionHandler({BindException.class}) public String bindExceptionHandler(final BindException e) { String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(" ; ")); return "{\"errors\":\"" + message + "\"}"; } @ResponseStatus(HttpStatus.OK) @ExceptionHandler(MethodArgumentNotValidException.class) public String handler(final MethodArgumentNotValidException e) { String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(" ; ")); return "{\"errors\":\"" + message + "\"}"; } @ResponseStatus(HttpStatus.OK) @ExceptionHandler(ConstraintViolationException.class) public String handler(final ConstraintViolationException e) { String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(" ; ")); return "{\"errors\":\"" + message + "\"}"; } }
验证异常就是这三种情况:
- BindException
- MethodArgumentNotValidException
- ConstraintViolationException
增加全局异常处理后,再次调用接口:
这种处理后报错信息存在两个问题
- 如何国际化,返回英文和中文
- 提示信息不明确,能不能自定义
国际化问题
Spring提供了i18N解决方式,跟断点也能发现校验失败后也是走的这套流程,核心类为AcceptHeaderLocaleResolver
从代码中可以看到语言是通过获取header中的Accept-Language切换的,这里也是固定写死的,如果要切换成其他方式或者其他header值,可以通过继承LocaleChangeInterceptor 覆盖preHandle方法,装备自定义bean使用
自定义校验信息
1、如果不考虑国际化
这种情况最简单,直接在验证Bean中修改
@NotBlank(message = "名字不能为空吆") private String name; @Range(min = 2, max = 100, message = "年龄需要在2~100岁之间") private Integer age;
2、考虑国际化
这种情况就不能简单的写死message了,需要通过hibernate-valid提供的国际化方式
通过hibernate源码可以看到国际化信息都存在一系列的properties文件中,这就给我提供了覆盖的机会,通过在resource下定义同名的国际化文件
首先,修改bean的message为el表达式方式
@NotBlank(message = "{person.name}") private String name; @Range(min = 2, max = 100, message = "{person.age}") private Integer age;
然后,把key定义在自己的国际化文件中:(一定要注意编码格式)
调用接口后结果:
如果此时需要自定义国际化文件名和位置,则需要增加如下配置类
@Configuration public class MessageConfig { public ResourceBundleMessageSource getMessageSource() throws Exception { ResourceBundleMessageSource resourceBundle = new ResourceBundleMessageSource(); resourceBundle.setDefaultEncoding(StandardCharsets.UTF_8.name()); //指定国际化文件路径 resourceBundle.setBasenames("i18n/valid"); return resourceBundle; } @Bean public Validator getValidator() throws Exception { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.setValidationMessageSource(getMessageSource()); return validator; } @Bean public MethodValidationPostProcessor validationPostProcessor() throws Exception { MethodValidationPostProcessor processor = new MethodValidationPostProcessor(); //指定请求验证器 processor.setValidator(getValidator()); return processor; } }
通过上述配置就可以达到使用指定国际化文件实现验证消息自定义的效果
2、DropWizard工程
DropWizard是一个轻量级的微服务开发框架,本身对hibernate-valid也做了一层封装,但是没有国际化方案
需要使用hibernate-valid原始国际化方式,通过设置默认语言来实现,
如果不需要默认方式,可以跳过此章节
实体类:
public class Person { @NotBlank() private String name; @Range(min = 2, max = 100) private Integer age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
资源类
@Service @Path("/test") public class TestResource { @POST @Path("/test") @Consumes({APPLICATION_JSON}) @Produces({APPLICATION_JSON}) public String test(Person person, @Context final HttpServletRequest request) { Set<ConstraintViolation<Person>> validate = getValidator().validate(person); if (!validate.isEmpty()) { StringJoiner message = new StringJoiner(";"); for (Object aValidate : validate) { message.add(((ConstraintViolation) aValidate).getMessage()); } return "{\"result\":\"" + message.toString() + "\"}"; } return "{\"result\":\"ok\"}"; } }
1、只需要自定义消息体
这种场景只需要在实体类中修改message即可
public class Person { @NotBlank(message = "姓名不能为空") private String name; @Range(min = 2, max = 100, message = "年龄需要在2~100之间") private Integer age; }
调用接口会就会返回固定的信息:
2、只需要国际化
这种场景时使用hibernate-valid自带的国际化消息,可以通过header中Accept-Language来控制语言类型。比如en-US,zh-CN
然后再后端获取header后,设置语言
修改实体类
public class Person { @NotBlank() private String name; @Range(min = 2, max = 100) private Integer age; }
修改资源类
@POST @Path("/test") @Consumes({APPLICATION_JSON}) @Produces({APPLICATION_JSON}) public String test(Person person, @Context final HttpServletRequest request) { //获取header中语言 String language = request.getHeader("Accept-Language"); Set<ConstraintViolation<Person>> validate = getValidator(language).validate(person); if (!validate.isEmpty()) { StringJoiner message = new StringJoiner(";"); for (Object aValidate : validate) { message.add(((ConstraintViolation) aValidate).getMessage()); } return "{\"result\":\"" + message.toString() + "\"}"; } return "{\"result\":\"ok\"}"; } private static Validator getValidator(String language) { // 设置语言 Locale.setDefault(language.contains("zh") ? Locale.CHINA : Locale.ENGLISH); return Validation.byDefaultProvider().configure() .buildValidatorFactory() .getValidator(); }
调用接口效果: