简体   繁体   中英

Spring Boot custom validator doesn't return to Controller

I am developing basic reset password flow. This is my DTO:

@Getter
@Setter
@FieldsValueMatch(field = "password", fieldMatch = "confirmPassword", message = "Passwords do not match")
public class PasswordResetForm {
    @Size(min = 8, message = "Password needs to have at least 8 characters")
    private String password;
    private String confirmPassword;
}

There is FieldsValueMatch annotation:

@Constraint(validatedBy = FieldsValueMatchValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldsValueMatch {
    String message() default "Fields values don't match!";

    String field();

    String fieldMatch();

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

And validator:

public class FieldsValueMatchValidator implements ConstraintValidator<FieldsValueMatch, Object> {

    private String field;
    private String fieldMatch;

    @Override
    public void initialize(FieldsValueMatch constraintAnnotation) {
        this.field = constraintAnnotation.field();
        this.fieldMatch = constraintAnnotation.fieldMatch();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        Object fieldValue = new BeanWrapperImpl(value).getPropertyValue(field);
        Object fieldMatchValue = new BeanWrapperImpl(value).getPropertyValue(fieldMatch);

        if (fieldValue != null) {
            return fieldValue.equals(fieldMatchValue);
        } else {
            return fieldMatchValue == null;
        }
    }
}

This is my controller:

@Controller
public class ResetPasswordController {

    @ModelAttribute("passwordResetForm")
    public PasswordResetForm passwordResetForm() {
        return new PasswordResetForm();
    }

    @GetMapping("/reset-password")
    public String showResetPasswordForm(final Model model) {
        return "reset-password";
    }

    @PostMapping("/reset-password-result")
    public String resetPassword(@Valid final PasswordResetForm passwordResetForm,
                                final BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return "reset-password";
    }
        // reset password logic
        return "redirect:/reset-password-success";
    }
}

and part of Thymeleaf page:

<form th:action="@{/reset-password-result}" th:object="${passwordResetForm}" method="post">
    <div>
        <div class="input-group">
            <input id="password"
                   class="form-input"
                   placeholder="Set a new password"
                   type="password"
                   th:field="*{password}"/>
        </div>
        <div th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></div>
    </div>
    <div>
        <div class="input-group">
            <input id="confirmPassword"
                   class="form-input"
                   placeholder="Re-type a new password"
                   type="password"
                   th:field="*{confirmPassword}"/>
        </div>
        <div th:if="${#fields.hasErrors('confirmPassword')}" th:errors="*{confirmPassword}"></div>
    </div>
    <div class="form-group">
        <button type="submit" class="form-btn">SET</button>
    </div>
</form>

And now, when I enter different passwords in both inputs, I get the following message in terminal:

2021-03-26 11:13:39.315  WARN 1340 [nio-8080-exec-7] 
s.w.s.h.AbstractHandlerExceptionResolver : Resolved 
[org.springframework.validation.BindException: 
org.springframework.validation.BeanPropertyBindingResult: 1 errors
Error in object 'passwordResetForm': codes 
[FieldsValueMatch.passwordResetForm,FieldsValueMatch]; arguments 
[org.springframework.context.support.DefaultMessageSourceResolvable: codes [passwordResetForm.,]; arguments []; default message [],password,confirmPassword]; default message [Passwords do not match]]

and instant 400 result code. My resetPassword method in the Controller is not called, so instead my Thymeleaf page I get Whitelabel Error Page. The same happens when I put password shorter than 8 characters. What am I doing wrong?

I'd appreciate your help!

Try to add @Validated annotation on ResetPasswordController class (before/after @Controller annotation). This should enabled validation

You define @FieldsValueMatch as a class-level constraint. Probably the generated default constraint violation is causing the problem, since in this case no explicit property path is specified for the created constraint violation.

The same happens when I put password shorter than 8 characters.

Regardless of the other field-level validation (= @Size ), the @FieldsValueMatch will be executed in any case, which is probably why you are still facing the same problem.

So, adjusting the FieldsValueMatchValidator implementation - by setting the property path for the created constraint violation and providing a custom error message - should fix the problem:

public class FieldsValueMatchValidator implements 
    ConstraintValidator<FieldsValueMatch, Object> {
    // ...
    private String message;

    @Override
    public void initialize(FieldsValueMatch constraintAnnotation) {
        // ...
        this.message = constraintAnnotation.message();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        //...
        boolean valid;

        if (fieldValue != null) {
            valid = fieldValue.equals(fieldMatchValue);
        } else {
            valid = fieldMatchValue == null;
        }

        if (!valid){
            context.buildConstraintViolationWithTemplate(this.message) // setting the custom message
                    .addPropertyNode(this.field) // setting property path
                    .addConstraintViolation() // creating the new constraint violation 
                    .disableDefaultConstraintViolation() // disabling the default constraint violation
                    ;
        }

        return valid;
    }
}

Check your method arguments sequence.

Method Arguments

You must declare an Errors , or BindingResult argument immediately after the validated method argument.

So, if your code actually looks like following, your problem will be reproduced.

    @PostMapping("/reset-password-result")
    public String resetPassword(@Valid PasswordResetForm passwordResetForm, Model model,
        BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "reset-password";
        }
        // reset password logic
        return "redirect:/reset-password-success";
    }

thanks for all the replies. For no reason, it just started to work without any change from my side... I don't know what happened, maybe I needed to restart my web browser or something like this...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM