diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java index 6d9180850ca..c2282a7dc25 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java @@ -85,6 +85,9 @@ public class MethodValidationAdapter { private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + @Nullable + private BindingResultNameResolver objectNameResolver; + /** * Create an instance using a default JSR-303 validator underneath. @@ -157,6 +160,19 @@ public class MethodValidationAdapter { return this.parameterNameDiscoverer; } + /** + * Configure a resolver for {@link BindingResult} method parameters to match + * the behavior of the higher level programming model, e.g. how the name of + * {@code @ModelAttribute} or {@code @RequestBody} is determined in Spring MVC. + *
If this is not configured, then {@link #createBindingResult} will apply + * default behavior to resolve the name to use. + * behavior applies. + * @param nameResolver the resolver to use + */ + public void setBindingResultNameResolver(BindingResultNameResolver nameResolver) { + this.objectNameResolver = nameResolver; + } + /** * Use this method determine the validation groups to pass into @@ -307,6 +323,10 @@ public class MethodValidationAdapter { /** * Select an object name and create a {@link BindingResult} for the argument. + * You can configure a {@link #setBindingResultNameResolver(BindingResultNameResolver) + * bindingResultNameResolver} to determine in a way that matches the specific + * programming model, e.g. {@code @ModelAttribute} or {@code @RequestBody} arguments + * in Spring MVC. *
By default, the name is based on the parameter name, or for a return type on * {@link Conventions#getVariableNameForReturnType(Method, Class, Object)}. *
If a name cannot be determined for any reason, e.g. a return value with
@@ -316,22 +336,30 @@ public class MethodValidationAdapter {
* @return the determined name
*/
private BindingResult createBindingResult(MethodParameter parameter, @Nullable Object argument) {
- // TODO: allow external customization via Function (e.g. from @ModelAttribute + Conventions based on type)
- String objectName = parameter.getParameterName();
- int index = parameter.getParameterIndex();
- if (index == -1) {
- try {
- Method method = parameter.getMethod();
- if (method != null) {
- Class> resolvedType = GenericTypeResolver.resolveReturnType(method, parameter.getContainingClass());
- objectName = Conventions.getVariableNameForReturnType(method, resolvedType, argument);
- }
+ String objectName = null;
+ if (this.objectNameResolver != null) {
+ objectName = this.objectNameResolver.resolveName(parameter, argument);
+ }
+ else {
+ if (parameter.getParameterIndex() != -1) {
+ objectName = parameter.getParameterName();
}
- catch (IllegalArgumentException ex) {
- // insufficient type information
+ else {
+ try {
+ Method method = parameter.getMethod();
+ if (method != null) {
+ Class> containingClass = parameter.getContainingClass();
+ Class> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass);
+ objectName = Conventions.getVariableNameForReturnType(method, resolvedType, argument);
+ }
+ }
+ catch (IllegalArgumentException ex) {
+ // insufficient type information
+ }
}
}
if (objectName == null) {
+ int index = parameter.getParameterIndex();
objectName = (parameter.getExecutable().getName() + (index != -1 ? ".arg" + index : ""));
}
BeanPropertyBindingResult result = new BeanPropertyBindingResult(argument, objectName);
@@ -340,6 +368,22 @@ public class MethodValidationAdapter {
}
+ /**
+ * Contract to determine the object name of an {@code @Valid} method parameter.
+ */
+ public interface BindingResultNameResolver {
+
+ /**
+ * Determine the name for the given method parameter.
+ * @param parameter the method parameter
+ * @param value the argument or return value
+ * @return the name to use
+ */
+ String resolveName(MethodParameter parameter, @Nullable Object value);
+
+ }
+
+
/**
* Builds a validation result for a value method parameter with constraints
* declared directly on it.
diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java
index df10ce558a1..239abf0d792 100644
--- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java
+++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java
@@ -39,13 +39,14 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class MethodValidationAdapterTests {
- private static final MethodValidationAdapter validationAdapter = new MethodValidationAdapter();
-
private static final Person faustino1234 = new Person("Faustino1234");
private static final Person cayetana6789 = new Person("Cayetana6789");
+ private final MethodValidationAdapter validationAdapter = new MethodValidationAdapter();
+
+
@Test
void validateArguments() {
MyService target = new MyService();
@@ -83,6 +84,28 @@ public class MethodValidationAdapterTests {
});
}
+ @Test
+ void validateArgumentWithCustomObjectName() {
+ MyService target = new MyService();
+ Method method = getMethod(target, "addStudent");
+
+ this.validationAdapter.setBindingResultNameResolver((parameter, value) -> "studentToAdd");
+
+ validateArguments(target, method, new Object[] {faustino1234, new Person("Joe"), 1}, ex -> {
+
+ assertThat(ex.getConstraintViolations()).hasSize(1);
+ assertThat(ex.getAllValidationResults()).hasSize(1);
+
+ assertBeanResult(ex.getBeanResults().get(0), 0, "studentToAdd", faustino1234, List.of(
+ """
+ Field error in object 'studentToAdd' on field 'name': rejected value [Faustino1234]; \
+ codes [Size.studentToAdd.name,Size.name,Size.java.lang.String,Size]; \
+ arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
+ codes [studentToAdd.name,name]; arguments []; default message [name],10,1]; \
+ default message [size must be between 1 and 10]"""));
+ });
+ }
+
@Test
void validateReturnValue() {
MyService target = new MyService();
@@ -158,14 +181,14 @@ public class MethodValidationAdapterTests {
Object target, Method method, Object[] arguments, Consumer