我有一个使用验证和 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”消息键,而不是由验证框架正确扩展的消息。
具体打印的装订结果显示:
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 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();
}
}