Java教程

聊聊springboot项目如何优雅进行数据校验

本文主要是介绍聊聊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、在需要校验实体的属性上,加上相关注解

示例:

9b082e90a1ebc16c5c0ff0fbdd7d89ca_5ab5ea6118f4e0db295e73d3ece6084c.png

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属性进行分组
7c9ec0e4f112e4d4ee0e092fdc3ba697_a6cf09824730e2550c2c7174510fbfc1.png

在需要校验的控制层方法上加@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、 在需要校验的属性上,加上自定义注解

示例

693aa8f43f6d9d1aa867aec066406b6e_920e17ac7b924ee46314acf6c876e925.png

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文件夹。

国际化文件夹位置如下示例
1b981b786a4ae16d8d76a0bb9c8094cd_fbddb85d1b7e4ba09332f82e96596cc3.png

注: 其中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

示例配置如下

2ec4a07d763bedc12dc61f6f4fe9861b_56d8716681c6d81f2f4912f46ef69556.png

单测效果如下

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项目如何优雅进行数据校验的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!