Still using If

If yes, then you should update your code using the guidelines below.

To avoid unauthorized parameters impacting your business, your web services must implement controller-level parameter checking! In most cases, query parameters can be divided into the following two types:

  • POST and PUT requests using requestBody to pass parameters.

  • GET requests using requestParam/PathVariable to pass parameters.

Minimum requirements:

  • Spring 6.0+

  • SpringBoot 3.0+

The Java API Specification (JSR303) defines the Validation Api standard for Beans, but does not provide an implementation. Hibernate Validation is an implementation of this standard, adding a number of validation annotations such as @Email, @Length, etc. Spring Validation is a secondary encapsulation of Hibernate Validation used to support automatic validation of Spring MVC parameters.

Without further delay, let's take an example of a Spring Boot project and get acquainted with the use of Spring Validation.

In SpringBoot 3.0+

the validation library has been moved to jakarta.validation

<dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
</dependency>

which can be imported by

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

RequestBody

In POST and PUT requests, parameters are usually passed using requestBody. In this case, the backend uses a DTO object to receive them. If a DTO object is marked with the @Validated annotation, then automatic parameter validation can be implemented.

For example, there is an interface for storing user data that requires the length userName was 2-10, the length of the account and password fields was 6-20. If the parameters fail verification, an error will be thrown MethodArgumentNotValidExceptionand Spring will send a 400 (Bad Request) request by default.

public class UserDTO {
    private Long userId;

    (min = 2, max = 10)
    private String userName;


    (min = 6, max = 20)
    private String account;
 

    (min = 6, max = 20)
    private String password;
}

Declaring verification annotations for method parameters:

("/save")
public Result saveUser(  UserDTO userDTO) {
    // ...
    return Result.ok();
}
// А НЕ(!!!)
("/save")
public Result saveUser(UserDTO userDTO) {
  

  if(userDTO.getUserName().getLength >=2 && userDTO.getUserName().getLength <=10)
  {
    ...
  }

  if(...)
  {
    ...
  }
  else{
    ...
  }
  ...
 

  return Result.ok();
}

RequestParam/PathVariable

GET requests are usually used to pass parameters requestParam/PathVariable. If there are many parameters (for example, more than 6), DTO objects should also be used to receive them. Otherwise, it is recommended to place one parameter in the incoming parameters of the method. In this case, the Controller class must be marked with the @Validated annotation and the input parameters declared with a constraint annotation (eg @Min, etc.). If the check fails, an error will be thrown ConstraintViolationException. The code example looks like this:

("/api/user")
public class UserController {
    ("{userId}")
    public Result detail(("userId") (10000000000000000L) Long userId) {
        UserDTO userDTO = new UserDTO();
        userDTO.set...
        return Result.ok(userDTO);
    }
    ("getByAccount")
    public Result getByAccount((min = 6, max = 20)  String  account) {
        UserDTO userDTO = new UserDTO();
        userDTO.set...
        return Result.ok(userDTO);
    }
}

In actual development projects, it is common to use unified exception handling to return friendlier messages, which we are already familiar with.

Before we delve deeper, we must understand what the relationship and difference are

@Valid and @Validated

Both @Valid and @Validated are used to trigger the validation process when processing a request in Spring. However, there are several key differences between them:

  • Origin: @Valid is a standard annotation from the Java Bean Validation specification, also known as JSR-303. It is not Spring specific and can be used in any Java application. On the other hand, @Validated is a Spring-specific annotation provided by Spring itself.

  • Function: @Valid is used to validate a method object or parameter in a method. It is often used when an object is received in an HTTP request and you want to check the fields of that object. @Validated is used to validate method parameters on a Spring bean. It is often used when a Spring bean method has parameters that must be validated.

  • Grouping: Only @Validated supports grouping of constraints. This is useful when different groups of checks are required for the same object under different circumstances.

To summarize, when working within the Spring framework, the best solution is to use @Validated because of its additional features. Use @Valid outside of Spring or when the additional capabilities of @Validated are not required.

Great, now we can get to the interesting parts.

Group validation

In real projects, multiple methods may need to use the same DTO class to obtain parameters, and the validation rules for different methods will likely be different. Currently, simply adding constraint annotations to the fields of a DTO class will not solve this problem. That's why Spring Validation offers a bulk validation option specifically to solve this problem.

In the example below, for example, when saving User, UserId can be null, and when updating Usermeaning UserId must be >=10000000000000000L; The validation rules for other fields are the same in both cases. Sample code using group validation currently looks like this:

public class UserDTO {
    (value = 10000000000000000L, groups = Update.class)
    private Long userId;
    (groups = {Save.class, Update.class})
    (min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;
    (groups = {Save.class, Update.class})
    (min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;
    (groups = {Save.class, Update.class})
    (min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;
    /
     * группа проверок для сохранения
     */
    public interface Save {
    }
    /
     * группа проверок для обновления 
     /
    public interface Update {
    }
}
("/save")
public Result saveUser( (UserDTO.Save.class) UserDTO userDTO) {
    // ...
    return Result.ok();
}
("/update")
public Result updateUser( (UserDTO.Update.class) UserDTO userDTO) {
    // ...
    return Result.ok();
}

As you can see, the grouping occurs in the constraint annotations.

Nested Validation

In the previous examples, all the fields in the DTO class were base data types or String types. However, in real-world scenarios, it is entirely possible that a field could be an object, in which case nested validation can be used.

For example, when storing the user information provided, we also receive information about his performance. It should be noted that this time the corresponding field of the DTO class must be marked with the @Valid annotation.

public class UserDTO {
    (value = 10000000000000000L, groups = Update.class)
    private Long userId;
    (groups = {Save.class, Update.class})
    (min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;
    (groups = {Save.class, Update.class})
    (min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;
    (groups = {Save.class, Update.class})
    (min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;
    (groups = {Save.class, Update.class})

    private Job job;

    public static class Job {
        (value = 1, groups = Update.class)
        private Long jobId;
        (groups = {Save.class, Update.class})
        (min = 2, max = 10, groups = {Save.class, Update.class})
        private String jobName;
        (groups = {Save.class, Update.class})
        (min = 2, max = 10, groups = {Save.class, Update.class})
        private String position;
    }
    public interface Save {
    }
    public interface Update {
    }
}

Nested validation can be used in combination with group validation. Additionally, nested collection validation will check each element in the collection, such as a field List will check every object Job on the list

Collection Validation

For example, the request body directly sends a JSON array to the backend and expects each element in the array to be validated. In this case, if we directly use a list or set of java.util.Collection to receive data, parameters will not be checked! We can use a custom list collection to accept parameters:

Wrap type List and declare the @Valid annotation.

If the check fails, an exception will be thrown NotReadablePropertyExceptionwhich can also be handled using the unified Exception.

For example, if we need to save several objects at once Userthe method in the Controller layer can be written like this:

public class ValidationList<E> implements List<E> {

     // обязательно
    public List<E> list = new ArrayList<>();


    public String toString() {
        return list.toString();
    }
}
("/saveList")
public Result saveList( (UserDTO.Save.class) ValidationList<UserDTO> userList) {
    // ...
    return Result.ok();
}

Custom Validation

Business requirements are always much more complex than the simple checks provided by the framework, so we can define our own checks to meet these needs.

Customizing Spring Validation is very easy. Let's assume that we are setting up verification of encrypted id (consisting of numbers or letters from af and length 32-256). There are two main steps:

Define a custom constraint annotation and implement the interface ConstraintValidatorin which the constraint validator will be specified:

({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
(RUNTIME)
(validatedBy = {EncryptIdValidator.class})
public  EncryptId {
    String message() default "id format error";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {
    private static final Pattern PATTERN = Pattern.compile("^[a-f\d]{32,256}$");
    

    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value != null) {
            Matcher matcher = PATTERN.matcher(value);
            return matcher.find();
        }
        return true;
    }
}
public class XXXDTO {

    private Long id;
    ...
}

This way we can use @EncryptId to check the parameters!

Software validation

The above examples rely on annotations to implement automatic validation, but in some cases we may need to call the validation programmatically. In this case we can inject the object javax.validation.Validator and then call its API.

private javax.validation.Validator globalValidator;
("/saveWithCodingValidate")
public Result saveWithCodingValidate( UserDTO userDTO) {
    Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class);
    if (validate.isEmpty()) {
        // ...
    } else {
        for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) {
            // ...
            System.out.println(userDTOConstraintViolation);
        }
    }
    return Result.ok();
}

Fail Fast

By default, Spring Validation will check all fields before throwing an exception. By adding a few simple tweaks, you can enable Fail Fast mode, which immediately returns an exception when validation fails.

public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            .failFast(true)
            .buildValidatorFactory();
    return validatorFactory.getValidator();
}

Last but not least

Implementation principle

@RequestBody

In Spring MVC RequestResponseBodyMethodProcessor used to parse parameters annotated with @RequestBody and to process return values ​​of methods annotated with @ResponseBody. Obviously, the logic to perform parameter checking should be in the parameter resolver method resolveArgument().

 ...

 /**
  * При неудачной проверке выбрасывает MethodArgumentNotValidException.
  * @throws HttpMessageNotReadableException если {@link RequestBody#required()}
  * является {@code true} и там нет содержимого, или если нет подходящего
  * конвертера для чтения содержимого.
  */
 @Override
 public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
   NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

  parameter = parameter.nestedIfOptional();
  
  //Инкапсулируйте данные запроса в DTO-объект 
  Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());

  if (binderFactory != null) {
   String name = Conventions.getVariableNameForParameter(parameter);
   WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
   if (arg != null) {
    validateIfApplicable(binder, parameter);
    if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
     throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
    }
   }
   if (mavContainer != null) {
    mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
   }
  }

  return adaptArgumentIfNecessary(arg, parameter);
 }

 ...

}

As you can see, resolveArgument() causes validateIfApplicable() for parameter validation.

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
  Annotation[] annotations = parameter.getParameterAnnotations();
  for (Annotation ann : annotations) {
   //определяем подсказки валидации
   Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
   if (validationHints != null) {
    binder.validate(validationHints);
    break;
   }
  }
 }

From here you should understand why annotations @Validated And @Valid can be mixed in this scenario. Let's move on to the implementation WebDataBinder.validate():

/**
  * Вызваем указанные валидаторы, если таковые имеются, с заданными подсказками валидации.
  * <p>Примечание: подсказки валидации могут быть проигнорированы реальным целевым валидатором.
  * @param validationHints один или несколько объектов подсказок для передачи в {@link SmartValidator}
  * @since 3.1
  * @see #setValidator(Validator)
  * @see SmartValidator#validate(Object, Errors, Object...)
  */
 public void validate(Object... validationHints) {
  Object target = getTarget();
  Assert.state(target != null, "No target to validate");
  BindingResult bindingResult = getBindingResult();
  // Вызываем каждый валидатор с одним и тем же результатом привязки
  for (Validator validator : getValidators()) {
   if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator smartValidator) {
    smartValidator.validate(target, bindingResult, validationHints);
   }
   else if (validator != null) {
    validator.validate(target, bindingResult);
   }
  }
 }

It turns out that the base layer ends up calling Hibernate Validator to do the actual validation processing.

Validating Parameters at the Method Level

This validation method involves distributing parameters one at a time across method parameters and declaring constraint annotations before each parameter, which represents method-level parameter validation. In fact, this method can be used for any Spring Bean methods such as Controller/Service etc. The implementation is based on the principle of AOP (aspect-oriented programming). In particular, it involves MethodValidationPostProcessordynamically registering the AOP aspect and then using MethodValidationInterceptor for weaving extensions into pointcut.

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
  implements InitializingBean {

  ...


 @Override
 public void afterPropertiesSet() {
  // Создаем аспект для всех бинов аннотированных '@Validated'. 
  Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
  // Создаем советника для расширений
  this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
 }

 /**
  * Создание рекомендаций AOP для целей валидации метода, которые будут применяться
  * с указателем для указанной аннотации "validated".
  * @param validator поставщик для используемого валидатора
  * @return перехватчик (обычно, но не обязательно,
  * {@link MethodValidationInterceptor} или его подкласс)
  * @since 6.0
  */
 protected Advice createMethodValidationAdvice(Supplier<Validator> validator) {
  return new MethodValidationInterceptor(validator);
 }

}

Let's take a look at MethodValidationInterceptor:

public class MethodValidationInterceptor implements MethodInterceptor {

 private final Supplier<Validator> validator;


 ...


 @Override
 @Nullable
 public Object invoke(MethodInvocation invocation) throws Throwable {
  // Избегайте вызова валидатора на FactoryBean.getObjectType/isSingleton
  if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
   return invocation.proceed();
  }

  Class<?>[] groups = determineValidationGroups(invocation);

  // Стандартный API Bean Validation 1.1
  ExecutableValidator execVal = this.validator.get().forExecutables();
  Method methodToValidate = invocation.getMethod();
  Set<ConstraintViolation<Object>> result;

  Object target = invocation.getThis();
  if (target == null && invocation instanceof ProxyMethodInvocation methodInvocation) {
   // Разрешаем проверку для AOP-прокси без таргета
   target = methodInvocation.getProxy();
  }
  Assert.state(target != null, "Target must not be null");

  try {
   result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
  }
  catch (IllegalArgumentException ex) {
   // Вероятно, имеет место несовпадение типов между интерфейсом и реализацией, как сообщалось в SPR-12237 / HV-1011
   // Давайте попробуем найти связанный метод в классе реализации...
   methodToValidate = BridgeMethodResolver.findBridgedMethod(
     ClassUtils.getMostSpecificMethod(invocation.getMethod(), target.getClass()));
   result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
  }
  if (!result.isEmpty()) {
   throw new ConstraintViolationException(result);
  }

  Object returnValue = invocation.proceed();

  result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups);
  if (!result.isEmpty()) {
   throw new ConstraintViolationException(result);
  }

  return returnValue;
 }

 ...

}

So it turns out that whether it’s parameter validation requestBody or validation at the method level, in the end the work will be done by the Hibernate Validator.

Thank you for your attention! Come to the free open lessons that will be held on the eve of the start of the “Spring Framework Developer” course:

  • March 13: Let's talk about JHipster, touch on Rapid Application Development and look at some use cases. Sign up

  • 20th of March: Let's try to understand the Controller, Service, Repository patterns – what benefits can they bring to us? We will also discuss the features of their use in Spring. Sign up

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *