Spring + Thymeleaf + Validation 忽略验证注释上的自定义消息并使用自己的

问题描述 投票:0回答:1

我有一个使用验证和 thymeleaf (3.1.2.RELEASE) 的典型 Spring Boot (3.3.2) MVC 应用程序,我发现 Thymeleaf 似乎忽略了验证注释中指定的 i18n 消息,而是尝试其他消息。换句话说,如果注释指定“{A}”作为消息,thymeleaf 会尝试使用不同的消息键,该键似乎是注释的名称+属性的路径(例如,对于附加到“test”bean 中的“name”属性,它尝试使用“Size.test.name”代替)。

提炼问题,各部分如下

豆:

@Validated
public class TestDTO {

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


    @NotNull(message = "{ui.errors.required}")
    @Size(min = 7, max = 100, message = "{ui.errors.nameLength}")
    private String name;
}

控制器:

package org.agoraspeakers.server.controllers.test;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class TestController {

    @GetMapping("/test-start")
    public String startClubRegistration(final HttpServletRequest request, Model model) {
        TestDTO test = new TestDTO();
        model.addAttribute("test", test);
        return "test-form";
    }

    @PostMapping("/test-post")
    public String registerClub(@Valid @ModelAttribute("test") TestDTO test, BindingResult bindingResult, final HttpServletRequest request, Model model) {
        System.out.println(bindingResult);
        return "test-form";
    }
}

表格

<!DOCTYPE html>
<html lang="en"  xmlns:th="http://www.thymeleaf.org">

<body >
    <form th:action="@{/test-post}" th:object="${test}" method="POST" enctype="multipart/form-data">
        <ul>
            <li th:each="e : ${#fields.detailedErrors()}" th:class="${e.global}? globalerr : fielderr">
                <span th:text="${e.global}? '*' : ${e.fieldName}">The field name</span> |
                <span th:text="${e.message}">The error message</span>
            </li>
        </ul>
        <input type="text" th:field="*{name}" size="60">
        <button type="submit">Submit</button>
</body>
</html>

现在,出于某种原因,thymeleaf 坚持渲染一些奇怪的“Size.test.name”消息键,而不是由验证框架正确扩展的消息。

form sumbission outcome

具体打印的装订结果显示:

Field error in object 'test' on field 'name': rejected value []; codes [Size.test.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [test.name,name]; arguments []; default message [name],50,2]; default message [The name must be between 7 and 100 characters.]

@Size 注释中的自定义消息“ui.errors.nameLength”已正确扩展为“名称必须在 7 到 100 个字符之间。”

为什么 thymeleaf 尝试使用不同的消息,以及如何强制它使用已指定的消息?

经过大量调试,我有预感,它可能与

shouldRenderDefaultMessage
中返回 false 的
SpringValidatorAdapter
() 方法有关,但又不知道该怎么办,我可能完全偏离了轨道关于这个

public boolean shouldRenderDefaultMessage() {
    return this.adapter != null && this.violation != null ? this.adapter.requiresMessageFormat(this.violation) : SpringValidatorAdapter.containsSpringStylePlaceholder(this.getDefaultMessage());
}

我在这里搜索类似的问题,但没有找到。

无论如何,我的期望是 thymeleaf 应该使用验证中明确指定的消息。毕竟,在验证注释中显式指示消息的全部目的是覆盖任何默认值。

spring-boot spring-mvc spring-validator spring-thymeleaf
1个回答
0
投票

所以,我想通了。 事实上,Spring DataBinding 完全忽略了验证注释的消息参数,其行为被编码在默认的

processConstraintViolations
SpringValidatorAdapter
方法中。

不幸的是,决定发生错误时返回哪些键代码的类是基于

MessageCodesResolver
接口的,并且那里定义的方法要求您提供错误代码(消息键),而无法访问原始注释验证失败.

我想出的解决方案是创建一个自定义的 ValidationAdapter 并重写处理约束违规的方法。不理想,但它有效。此更改版本尊重您在注释消息中写入的任何内容 - 无论是文字消息还是关键引用。

自定义验证适配器

import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import jakarta.validation.metadata.ConstraintDescriptor;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.NotReadablePropertyException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;

import java.util.Iterator;
import java.util.Set;

public class CustomValidationAdapter extends SpringValidatorAdapter {

    public CustomValidationAdapter(Validator validator) {
        super(validator);
    }
    protected void processConstraintViolations(Set<ConstraintViolation<Object>> violations, Errors errors) {
        Iterator violationIterator = violations.iterator();

        while(true) {
            ConstraintViolation violation;
            String field;
            FieldError fieldError;
            do {
                if (!violationIterator.hasNext()) {
                    return;
                }

                violation = (ConstraintViolation)violationIterator.next();
                field = this.determineField(violation);
                fieldError = errors.getFieldError(field);
            } while(fieldError != null && fieldError.isBindingFailure());

            try {
                ConstraintDescriptor<?> cd = violation.getConstraintDescriptor();
                String errorCode = this.determineErrorCode(cd);
                Object[] errorArgs = this.getArgumentsForConstraint(errors.getObjectName(), field, cd);
                if (errors instanceof BindingResult bindingResult) {
                    String nestedField = bindingResult.getNestedPath() + field;

                    String template = violation.getMessageTemplate();
                    String message = violation.getMessage();
                    String[] codes = null;
                    if (template != null && (template.startsWith("{") && template.endsWith("}"))) {
                        template = template.substring(1, template.length() - 1);
                        String[] errorCodes = nestedField.isEmpty()?bindingResult.resolveMessageCodes(errorCode): bindingResult.resolveMessageCodes(errorCode, field);
                        codes = ArrayUtils.addAll(new String[] { template },errorCodes);
                    }
                    if (nestedField.isEmpty()) {
                        ObjectError error = new ObjectError(errors.getObjectName(), codes, errorArgs,  message);
                        bindingResult.addError(error);
                    } else {
                        Object rejectedValue = this.getRejectedValue(field, violation, bindingResult);
                        FieldError error = new FieldError(errors.getObjectName(), nestedField, rejectedValue, false, codes, errorArgs, message);
                        bindingResult.addError(error);
                    }
                } else {
                    errors.rejectValue(field, errorCode, errorArgs, violation.getMessage());
                }
            } catch (NotReadablePropertyException var15) {
                NotReadablePropertyException ex = var15;
                throw new IllegalStateException("JSR-303 validated property '" + field + "' does not have a corresponding accessor for Spring data binding - check your DataBinder's configuration (bean property versus direct field access)", ex);
            }
        }
    }

}

配置

import jakarta.validation.Validation;
import jakarta.validation.ValidatorFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.Validator;

@Configuration
public class ValidatorConfig {

    @Bean
    public Validator customValidator() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        return new CustomValidationAdapter(factory.getValidator());
    }

    @Bean
    public org.springframework.validation.Validator getValidator() {
        return customValidator();
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.