You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Validation自定义+多字段通用验证方案选型咨询

Clean, Reusable Spring Validation Solution for Your DTOs

Great question! Let's build a scalable, maintainable solution that eliminates annotation duplication, handles different field types (emails vs passwords), and properly feeds validation errors to your Thymeleaf frontend.


1. Eliminate Annotation Duplication with Meta-Annotations

Instead of repeating @Email, @Max, @NotNull across fields, create meta-annotations that bundle all required validations for specific field types. This keeps your DTOs clean and consistent.

Example: Meta-annotation for Emails

@Target({FIELD})
@Retention(RUNTIME)
@NotNull(message = "E-posta bos olamaz")
@NotEmpty(message = "E-posta bos olamaz")
@Max(value = 150, message = "E-posta maksimum 150 karakter olabilir")
@Email(message = "Gecerli bir e-posta adresi giriniz")
@Documented
public @interface ValidEmail {
    // Default attributes for constraint compatibility
    String message() default "Geçersiz e-posta";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Example: Meta-annotation for Passwords

For passwords, skip the @Email rule and add relevant validations (like length or complexity):

@Target({FIELD})
@Retention(RUNTIME)
@NotNull(message = "Parola bos olamaz")
@NotEmpty(message = "Parola bos olamaz")
@Size(min = 8, max = 100, message = "Parola en az 8 en fazla 100 karakter olmalidir")
// Add custom password complexity validation here if needed
@Documented
public @interface ValidPassword {
    String message() default "Geçersiz parola";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Simplified DTOs

Now your DTOs become concise and readable:

@FieldMatch(first = "email", second = "emailConfirm", message = "E-posta adresleri eslesmeli")
public class EmailDTO {
    @ValidEmail
    private String email;
    
    @ValidEmail
    private String emailConfirm;
}
@FieldMatch(first = "password", second = "passwordConfirm", message = "Parolalar eslesmeli")
public class PasswordDTO {
    @ValidPassword
    private String password;
    
    @ValidPassword
    private String passwordConfirm;
}

2. Handle Optional Fields with Validation Groups

For scenarios where fields can be empty (e.g., optional profile updates), use validation groups to toggle required checks.

Step 1: Define Marker Interfaces

Create empty interfaces to represent validation scenarios:

public interface RequiredValidation {}
public interface OptionalValidation {}

Step 2: Update Meta-Annotations

Move @NotNull/@NotEmpty to the RequiredValidation group so they only apply when needed:

@Target({FIELD})
@Retention(RUNTIME)
@NotNull(message = "E-posta bos olamaz", groups = RequiredValidation.class)
@NotEmpty(message = "E-posta bos olamaz", groups = RequiredValidation.class)
@Max(value = 150, message = "E-posta maksimum 150 karakter olabilir")
@Email(message = "Gecerli bir e-posta adresi giriniz")
@Documented
public @interface ValidEmail {
    // ... attributes
}

Step 3: Validate with Groups in Controllers

Specify which group to use when validating DTOs:

// For mandatory email submission (e.g., registration)
@PostMapping("/register")
public String register(@Validated(RequiredValidation.class) EmailDTO dto, BindingResult result) {
    if (result.hasErrors()) {
        return "register-form";
    }
    // Process registration
    return "success";
}

// For optional email updates (e.g., profile edit)
@PostMapping("/update-profile")
public String updateProfile(@Validated(OptionalValidation.class) EmailDTO dto, BindingResult result) {
    if (result.hasErrors()) {
        return "profile-form";
    }
    // Update profile (empty email will skip required checks, but valid email format is enforced if provided)
    return "success";
}

3. Improve FieldMatchValidator for Better Error Feedback

Your existing FieldMatchValidator works well, but tweak it to show errors on both fields (optional) and log exceptions instead of ignoring them:

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {
    private String firstFieldName;
    private String secondFieldName;
    private String message;
    private static final Logger log = LoggerFactory.getLogger(FieldMatchValidator.class);

    @Override
    public void initialize(final FieldMatch constraintAnnotation) {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
        message = constraintAnnotation.message();
    }

    @Override
    public boolean isValid(final Object value, final ConstraintValidatorContext context) {
        boolean valid = true;
        try {
            final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
            final Object secondObj = BeanUtils.getProperty(value, secondFieldName);
            valid = (firstObj == null && secondObj == null) || (firstObj != null && firstObj.equals(secondObj));
        } catch (final Exception e) {
            log.error("Failed to retrieve fields for FieldMatch validation", e);
            valid = false;
        }

        if (!valid){
            // Add error to both fields for better user feedback
            context.buildConstraintViolationWithTemplate(message)
                .addPropertyNode(firstFieldName)
                .addConstraintViolation();
            context.buildConstraintViolationWithTemplate(message)
                .addPropertyNode(secondFieldName)
                .addConstraintViolation();
            context.disableDefaultConstraintViolation();
        }
        return valid;
    }
}

4. Display Errors in Thymeleaf

Use Thymeleaf's built-in field error utilities to show validation messages next to your form fields:

<div class="form-group">
    <label for="email">E-posta</label>
    <input type="email" th:field="*{email}" class="form-control" id="email">
    <div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="text-danger"></div>
</div>
<div class="form-group">
    <label for="emailConfirm">E-posta Tekrar</label>
    <input type="email" th:field="*{emailConfirm}" class="form-control" id="emailConfirm">
    <div th:if="${#fields.hasErrors('emailConfirm')}" th:errors="*{emailConfirm}" class="text-danger"></div>
</div>

Key Takeaways

  • Meta-annotations eliminate repetitive code and keep DTOs clean.
  • Validation groups handle optional vs mandatory field scenarios flexibly.
  • Your existing @FieldMatch annotation is fully reusable across all DTOs (emails, passwords, etc.).
  • Proper error binding ensures Thymeleaf can display all validation messages to users.

内容的提问来源于stack exchange,提问作者Caner Aydın

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 08:01:10