聊聊springboot项目如何优雅进行数据校验
2025/1/1 6:03:15
本文主要是介绍聊聊springboot项目如何优雅进行数据校验,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
前言
在我们日常开发中,数据校验是我们绕不开的一环,而用Spring Validation进行校验,基本上成为我们进行数据校验的首选组件,今天的话题就来聊下如何利用Spring Validation进行优雅校验
Spring Validate
Spring Validate简介
Spring框架的验证功能主要基于JSR 303/JSR 349 Bean Validation规范,这是一套标准的Java注解驱动的数据验证API。Spring提供了对Bean Validation的深度集成,使得在Web应用中进行数据校验变得既强大又简便。
Spring Validate主要功能
- 注解驱动的验证:
Spring支持Bean Validation注解,如@NotNull, @Size, @Pattern, @Email, @Min, @Max等,可以直接在实体类的属性上标注,进行自动的数据校验。
- 自定义校验注解:
开发者可以创建自定义的校验注解和校验器,以适应更复杂或特定于业务的验证逻辑。
- 集成到Spring MVC:
在Spring MVC中,可以使用@Valid或@Validated注解配合BindingResult对象来捕获和处理校验错误,通常在控制器方法的参数中使用。
- 错误消息定制:
可以通过资源文件或直接在注解中定义错误消息,以便向用户提供更友好的错误信息。
- 组验证:
支持按组进行验证,允许在不同的场景下应用不同的验证规则集。
- 嵌套对象验证:
当对象中有嵌套的其他对象时,Spring可以递归地进行验证,确保整个数据结构的有效性。
- 验证器接口Validator:
提供了Validator接口,允许开发者实现自定义的验证逻辑,而不使用注解。
- 错误处理:
提供了灵活的错误处理机制,可以根据业务需求选择合适的错误处理策略,如返回HTTP状态码、重定向到错误页面等。
- 国际化支持:
支持多语言的错误消息,可以通过不同的资源文件为不同语言的用户提供相应的错误信息。
如何使用
1、项目中引入相应的GAV
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
2、在需要校验实体的属性上,加上相关注解
示例:
3、在需要进行校验的控制器方法写上相应注解以及BindingResult
示例
@PostMapping("addOther") public AjaxResult addOther(@Validated @RequestBody UserDTO userDTO, BindingResult bindingResult){ if(bindingResult.hasErrors()){ Map<String,String> errorMap = new LinkedHashMap<>(); bindingResult.getFieldErrors().forEach(fieldError -> { errorMap.put(fieldError.getField(), fieldError.getDefaultMessage()); }); return AjaxResult.fail("数据校验失败",String.valueOf(HttpStatus.BAD_REQUEST.value()),errorMap); } return AjaxResult.success(userDTO); }
注: BindingResult要和实体一一匹配,比如你写的方法有2个实体,则方法需要写成如下
@PostMapping("addOther") public AjaxResult addOther(@Validated @RequestBody UserDTO userDTO, BindingResult bindingResult,@Validated @RequestBody User user,BindingResult useBindingResultr){ }
上面的写法是常规写法,但是存在一些问题,比如有多个方法需要校验,那要写一堆BindingResult ,这样代码可读性就比较差。因此我们可以做如下改造
通过定义全局异常处理器来处理
@RestControllerAdvice(basePackages = "com.github.lybgeek") @RequiredArgsConstructor @Slf4j public class ResultResponseBodyAdvice implements ResponseBodyAdvice<Object> { private final MessageSource messageSource; @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentNotValidException.class) public AjaxResult validationException(MethodArgumentNotValidException exception){ Map<String,String> errorMap = new LinkedHashMap<>(); exception.getBindingResult().getFieldErrors().forEach(fieldError -> { errorMap.put(fieldError.getField(), fieldError.getDefaultMessage()); }); log.error("validate error:{}",exception.getMessage()); String message = messageSource.getMessage("message.validate.error",null,"validate error", LocaleContextHolder.getLocale()); return AjaxResult.fail(message,String.valueOf(HttpStatus.BAD_REQUEST.value()),errorMap); } }
4、分组校验
当有些属性在新增时不需要校验,但在修改需要校验,我们就可以利用分组校验
示例:
实体层添加在相应的校验注解上,并通过group属性进行分组
在需要校验的控制层方法上加@Validated注解,并添加分组属性
示例
@PostMapping("update") public UserDTO update(@Validated(CrudValidate.Update.class) @RequestBody UserDTO userDTO){ UserDTO updateUser = userService.update(userDTO); System.out.println("updateUser:" + updateUser); return updateUser; }
5、自定义注解校验
当Spring Validate提供的原生注解,不满足我们的校验需求时,我们可以通过自定义注解校验
示例
a、 定义自定义校验注解
@Documented @Constraint( validatedBy = {UniqueConstraintValidator.class} ) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public @interface Unique { String message() default "{javax.validation.constraints.Unique.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; Class<? extends UniqueCheckService> checkUniqueBeanClass(); String checkField() default ""; }
b、 自定义校验规则逻辑
@Component @Scope("prototype") @Slf4j public class UniqueConstraintValidator implements ConstraintValidator<Unique,Object>, ApplicationContextAware { private ApplicationContext applicationContext; private UniqueCheckService uniqueCheckService; private String checkField; @Override public void initialize(Unique constraintAnnotation) { uniqueCheckService = applicationContext.getBean(constraintAnnotation.checkUniqueBeanClass()); checkField = constraintAnnotation.checkField(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { if(value == null){ return true; } log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> uniqueCheckService:{},checkField:{},value:{}",uniqueCheckService,checkField,value); return !uniqueCheckService.checkUnique(value,checkField); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }
注: 这边校验规则的bean用原型模式,是为了避免线程安全问题,实际得根据具体业务场景定
c、 在需要校验的属性上,加上自定义注解
示例
5、国际化
当我们的业务有国际化场景需求, Spring Validate也支持校验信息的国际化
示例
以spring boot项目为示例
a、 在项目的application.yml做如下配置
spring: messages: # 注意需要创建messages.properties文件来做兜底 basename: i18n/messages #代表将国际化文件放在i18n文件夹下,并以messages作为文件名前缀,而不是指国际化文件存放在i18n/messages文件夹。 encoding: UTF-8
注: 配置文件中i18n/messages的含义是将国际化文件放在i18n文件夹下,并以messages作为文件名前缀,而不是指国际化文件存放在i18n/messages文件夹。
国际化文件夹位置如下示例
注: 其中messages.properties文件来做兜底。
数据校验国际化相关输出内容配置化,如下
messages_zh_CN.properties
message.id.not.empty=ID不能为空 message.username.not.empty=用户名不能为空 message.username.unique=用户名已经存在 message.password.not.empty=密码不能为空 message.password.length=密码的长度必须在{min}-{max}之间 message.email.format.error=邮箱格式错误 message.mobile.unique=手机号码已经存在 message.mobile.format.error=手机号码格式错误 message.user.not.exist=不存在用户ID为{0}的用户 message.validate.error=数据校验失败 message.exception.error=系统异常
messages_en_US.properties
message.id.not.empty=id must not be empty message.username.not.empty=username must not be empty message.username.unique=username must be unique message.password.not.empty=password must not be empty message.password.length=the password length must be between {min}-{max}. message.mobile.unique=mobile must be unique message.mobile.format.error=mobile format error message.email.format.error=email format error message.user.not.exist=the user with ID {0} does not exist message.validate.error=data validation error message.exception.error=system exception
b、 在需要校验的实体上做如下配置
@Data @AllArgsConstructor @NoArgsConstructor @Builder public class UserDTO { @NotNull(message = "{message.id.not.empty}",groups = {CrudValidate.Update.class}) private Long id; @NotEmpty(message = "{message.username.not.empty}") @Unique(message = "{message.username.unique}",checkUniqueBeanClass = UserService.class) private String username; @NotEmpty(message = "{message.password.not.empty}") @Size(min = 6,max = 32,message = "{message.password.length}") private String password; @Email(message = "{message.email.format.error}") private String email; @Pattern(regexp = "^1[3-9]\\d{9}$",message = "{message.mobile.format.error}") @Unique(message = "{message.mobile.unique}",checkUniqueBeanClass = MobileCheckService.class,checkField = "mobile") private String mobile; }
做了如上配置,如果springboot的版本是在2.6.x版本之后,即可生效。Spring Boot 2.6.x版本之后已支持验证注解message属性引用Spring Boot自身国际化配置。在Spring Boot 2.5.x版本中以及之前,Spring Boot Validation默认只支持读取resources/ValidationMessages.properties系列文件的中的国际化属性,且中文需要进行ASCII转码才可正确显示。具体可以查看官方issue
https://github.com/spring-projects/spring-boot/pull/17530
如果我们想要在2.5.x版本以及之前,使用Spring Boot spring.messages设置的国际化文件,我们可以做如下配置
@Configuration @ComponentScan(basePackages = {"com.github.lybgeek.validate.constraint","com.github.lybgeek.validate.advice"}) public class ValidateAutoConfiguration implements WebMvcConfigurer { @Autowired private MessageSource messageSource; /** * @see <a href="https://github.com/spring-projects/spring-boot/pull/17530">...</a> * 在Spring Boot 2.5.x版本中以及之前,Spring Boot Validation默认只支持读取resources/ValidationMessages.properties系列文件的中的国际化属性, * 且中文需要进行ASCII转码才可正确显示,Spring Boot 2.6.x版本之后已支持验证注解message属性引用Spring Boot自身国际化配置。 * @return */ @Override public Validator getValidator() { LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean(); //仅兼容Spring Boot spring.messages设置的国际化文件和原hibernate-validator的国际化文件 //不再支持resource/ValidationMessages.properties bean.setValidationMessageSource(this.messageSource); return bean; } }
不过这样配置后,就会导致原先ValidationMessages.properties不再生效
c、国际化验证
编写单测
@Test public void testAdd(){ UserDTO userDTO = new UserDTO(); userDTO.setEmail("lisi@qq.com"); userDTO.setPassword("123456"); userDTO.setUsername("lisi"); userDTO.setMobile("13600000006"); String language = getLanguage(); System.out.println("language:"+language); String result = Forest.post(BASE_URL + "user/add") .contentTypeJson() .addHeader("Accept-Language", language) .addBody(JSONUtil.toJsonStr(userDTO)).execute(String.class); System.out.println(result); }
spring国际化,默认是使用 AcceptHeaderLocaleResolver解析器,即通过在header配置"Accept-Language",进行语言传递
示例效果如下
{"message":"data validation error","code":"400","data":{"email":"email format error","mobile":"mobile format error","username":"username must not be empty","password":"the password length must be between 6-32."},"success":false}
在我们实际开发中,前端通过header传递国际化语言可能不大方便,有时候我们会直接把传递的语言放在url的请求参数中,形如
BASE_URL + "user/save?lang="+language
基于上述需求,我们需做如下配置
@Configuration @ComponentScan(basePackages = {"com.github.lybgeek.validate.constraint","com.github.lybgeek.validate.advice"}) public class ValidateAutoConfiguration implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 如果需要支持修改语言,则LocaleResolver要改成SessionLocaleResolver或者其他,不能用默认的 // AcceptHeaderLocaleResolver LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor(); interceptor.setParamName("lang"); registry.addInterceptor(interceptor).addPathPatterns("/**"); } @Bean public LocaleResolver localeResolver() { // 默认AcceptHeaderLocaleResolver实现国际化 SessionLocaleResolver localeResolver = new SessionLocaleResolver(); localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); return localeResolver; } }
单测示例如下
@Test public void testSaveErrorWithI18n(){ UserDTO userDTO = new UserDTO(); userDTO.setEmail("123"); userDTO.setPassword("123"); userDTO.setMobile("123"); String language = getLanguage(); System.out.println("language:"+language); String result = Forest.post(BASE_URL + "user/save?lang="+language) .contentTypeJson() // 当LocaleResolver为AcceptHeaderLocaleResolver,支持header传递Accept-Language,该模式为默认模式 // 本示例我们改成成通过url传递参数,因此我们需做一定改造,该地方查看com.github.lybgeek.validate.autoconfigure.ValidateAutoConfiguration // .addHeader("Accept-Language", language) .addBody(JSONUtil.toJsonStr(userDTO)).execute(String.class); System.out.println(result); }
测试效果
{"message":"数据校验失败","code":"400","data":{"username":"用户名不能为空","password":"密码的长度必须在6-32之间","email":"邮箱格式错误","mobile":"手机号码格式错误"},"success":false}
注: 这边有个小细节要注意,传递语言值需用中划线,而非下划线,比如中文,则需写成zh-CN而非zh_CN
6、通过service层进行校验
有些场景我们可能不是通过controller进行数据校验,而是直接通过service进行校验。我们得做如下配置
示例
/*** * 同时定义了接口和实现类, @Valid加在service接口上, 不是实现类上 */ @Validated public interface UserService extends UniqueCheckService { UserDTO save(@Valid UserDTO userDTO); @Validated(CrudValidate.Update.class) UserDTO update(@Valid UserDTO userDTO); }
注: 同时配置接口和实现,校验注解需要写在接口上,否则会报错
编写service单测
@Test public void testAddErrorWithI18n(){ UserDTO userDTO = new UserDTO(); userDTO.setEmail("12345"); userDTO.setPassword("12345"); userDTO.setMobile("123"); System.out.println(JSONUtil.toJsonStr(userService.save(userDTO))); }
注: 因为示例的springboot版本低于2.6.x,且service层是无法感知WebMvcConfigurer做的校验器变更,因此如果我们需要做国际化配置,配置文件只能调整成ValidationMessages.properties
示例配置如下
单测效果如下
save.userDTO.mobile: 手机号码格式错误 !!!, save.userDTO.username: 用户名不能为空 !!!, save.userDTO.email: 邮箱格式错误 !!!, save.userDTO.password: 密码的长度必须在6-32之间 !!!
走的数据校验提示语,来自ValidationMessages_zh_CN.properties配置
总结
本文主要介绍Spring Validate一些比较常用的校验,这边有个小建议,就是数据校验提示信息,最好做成外部配置化,而非写死在代码里,尤其现在不少企业在探索出海业务,对国际化的支持是一个必选项,在代码写死数据校验提示,不是一个好的选择项
demo链接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-validate
这篇关于聊聊springboot项目如何优雅进行数据校验的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2025-01-03企业在选择工具时,如何评估其背后的技术团队
- 2025-01-03Angular中打造动态多彩标签组件的方法
- 2025-01-03Flask过时了吗?FastAPI才是未来?
- 2025-01-0311个每位开发者都应知道的免费实用网站
- 2025-01-03从REST到GraphQL:为什么以及我是如何完成转型的
- 2025-01-03掌握RAG:从单次问答到连续对话
- 2025-01-03看板软件:好用的个性化健身计划制定工具
- 2025-01-03Springboot企业级开发资料入门教程
- 2025-01-03SpringBoot企业级开发资料入门教程
- 2025-01-03Springboot微服务资料入门教程