JAVA Spring

Spring MVC Validation

neal89 2025. 4. 7. 13:26

📌 1. Purpose of Validation

  • Ensure data integrity from client inputs.
  • Enhance user experience by keeping user inputs and showing friendly error messages.

📌 2. Validation Flow (1) Type Conversion Stage

  • During @ModelAttribute binding, type mismatches can occur.
  • Without BindingResult → Spring throws a 400 Bad Request.
  • With BindingResult → Controller is still called, and errors are stored in BindingResult.

(2) Validation Logic ✅ Recommended Practice:

  • Use BindingResult with rejectValue() (for field errors), reject() (for global errors).
  • Automatically adds FieldError and ObjectError.
if (!StringUtils.hasText(item.getItemName())) {
    bindingResult.rejectValue("itemName", "required");
}
if (item.getPrice() != null && item.getQuantity() != null) {
    int resultPrice = item.getPrice() * item.getQuantity();
    if (resultPrice < 10000) {
        bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
    }
}

📌 3. Using BindingResult

  • For field errors:
  • bindingResult.addError(new FieldError(String objectName, String field, @Nullable Object
    rejectedValue, boolean bindingFailure, @Nullable String[] codes, @Nullable
    Object[] arguments, @Nullable String defaultMessage))
    ==> It's a bit too tedious to manage.
    • instead, use -> bindingResult.rejectValue("fieldName", "errorCode");
  • For global errors (object errors):
  • bindingResult.addError(new ObjectError(String objectName, String defaultMessage) )
    • instead, use -> bindingResult.reject("errorCode");

**When you use bindingResult.rejectValue(...), it internally creates a FieldError object and adds it to bindingResult.addError(...).


📌 4. Handling Messages

  • Define messages in errors.properties
  • Message Resolution Hierarchy:
    1. required.item.itemName (object + field)
    2. required.itemName (field only)
    3. required.java.lang.String (type-based)
    4. required (general fallback)
required.item.itemName=Item name is required.
required.itemName=Please enter a name.
required.java.lang.String=This field must be a string.
required=This field is required.

** If there is no message code and no default message, a message of the form ???errorCode??? is used.


📌 5. Validation Improvement Steps

Version Method Description

V1 Manual if checks Simple but repetitive
V2 FieldError, ObjectError Supports input retention
V3 Message codes & errors.properties i18n and reuse
V4 rejectValue(), reject() Cleaner, automatic message lookup
V5 Extract Validator class Separation of concerns
V6 @Validated + @InitBinder Automatic validator binding

📌 6. Custom Validator Separation

  • Create class that implements Validator.
  • Benefits:
    • Reusable validation logic
    • Keeps controllers clean
@Component
public class ItemValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return Item.class.isAssignableFrom(clazz);
    }
    @Override
    public void validate(Object target, Errors errors) {
        Item item = (Item) target;
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName", "required");
        // More logic...
    }
}

📌 7. Using @Validated and @InitBinder

  • Bind the validator to the controller using @InitBinder
  • Automatically invoked when @Validated is used

**@Validated is an annotation that triggers (executes) the validation provided by Spring.
When you attach it with @ModelAttribute or @RequestBody in a controller, validation is automatically performed for that object.

** execution flow

  1. DispatcherServlet receives the HTTP request.
  2. It finds the appropriate controller and handler method using HandlerMapping.
  3. Then, it processes the method parameters using a set of HandlerMethodArgumentResolvers.
  4. If a parameter is annotated with @ModelAttribute (or omitted but still a model object), the ModelAttributeMethodProcessor handles it.
  5. Inside this processor, a WebDataBinder is created using WebDataBinderFactory.
  6. At the moment of WebDataBinder creation, if there is an @InitBinder method in the controller, it is invoked to register custom Validator(s).
  7. If the parameter is annotated with @Validated, the registered Validator’s supports() method is checked, and if matched, its validate() method is executed.

** @Validated and @InitBinder do not work with @RequestBody because @RequestBody uses HttpMessageConverter for binding, whereas @InitBinder and WebDataBinder only apply to @ModelAttribute-based form data binding.

 

**@requestBody???

--> For @RequestBody, @Valid or @Validated triggers JSR-303 Bean Validation via LocalValidatorFactoryBean, not using WebDataBinder or @InitBinder.

@InitBinder
public void init(WebDataBinder binder) {
    binder.addValidators(itemValidator);
}

@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult result) {
    if (result.hasErrors()) {
        return "form";
    }
    return "redirect:/success";
}