diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 4319397863ba..777e92d18c31 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -144,7 +144,7 @@ public Mono resolveArgument( return adapter.fromPublisher(mono); } else { - if (errors.hasErrors() && !hasErrorsArgument(parameter)) { + if (errors.hasErrors() && isBindExceptionRequired(parameter)) { throw new WebExchangeBindException(parameter, errors); } return attribute; @@ -219,13 +219,30 @@ protected Mono bindRequestParameters(WebExchangeDataBinder binder, ServerW return binder.bind(exchange); } - private boolean hasErrorsArgument(MethodParameter parameter) { + /** + * Whether to raise a fatal bind exception on validation errors. + * @param parameter the method parameter declaration + * @return {@code true} if the next method parameter is not of type {@link Errors} + * @since 6.2 + */ + protected boolean isBindExceptionRequired(MethodParameter parameter) { int i = parameter.getParameterIndex(); Class[] paramTypes = parameter.getExecutable().getParameterTypes(); - return (paramTypes.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1])); + return !(paramTypes.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1])); } - private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter, ServerWebExchange exchange) { + /** + * Validate the model attribute if applicable. + *

The default implementation checks for {@code @jakarta.validation.Valid}, + * Spring's {@link org.springframework.validation.annotation.Validated}, + * and custom annotations whose name starts with "Valid". + * @param binder the DataBinder to be used + * @param parameter the method parameter declaration + * @param exchange the current exchange + * @see WebExchangeDataBinder#validate(Object...) + * @see org.springframework.validation.SmartValidator#validate(Object, Errors, Object...) + */ + protected void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter, ServerWebExchange exchange) { LocaleContext localeContext = null; try { for (Annotation ann : parameter.getParameterAnnotations()) { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index d54011387cad..fa1f30970498 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -37,6 +37,7 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.bind.support.WebExchangeBindException; +import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; @@ -400,6 +401,29 @@ void bindDataClass() { // TODO: SPR-15871, SPR-15542 + @Test + void protectedMethodsCanBeOverridden() { + // Test that validateIfApplicable and isBindExceptionRequired can be overridden + CustomModelAttributeMethodArgumentResolver resolver = new CustomModelAttributeMethodArgumentResolver( + ReactiveAdapterRegistry.getSharedInstance(), false); + assertThat(resolver.validateIfApplicableCalled).isFalse(); + assertThat(resolver.isBindExceptionRequiredCalled).isFalse(); + + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class); + + // Use invalid data to trigger validation errors, which will call isBindExceptionRequired + try { + resolver.resolveArgument(parameter, this.bindContext, postForm("name=Robert&age=invalid")) + .block(Duration.ZERO); + } + catch (WebExchangeBindException ex) { + // Expected - validation error + } + + assertThat(resolver.validateIfApplicableCalled).isTrue(); + assertThat(resolver.isBindExceptionRequiredCalled).isTrue(); + } + private ModelAttributeMethodArgumentResolver createResolver() { return new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false); @@ -516,7 +540,7 @@ public String getName() { } public int getAge() { - return this.age; + return age; } public int getCount() { @@ -528,4 +552,30 @@ public void setCount(int count) { } } + + /** + * Custom resolver that overrides protected methods to verify they can be extended. + */ + private static class CustomModelAttributeMethodArgumentResolver extends ModelAttributeMethodArgumentResolver { + + boolean validateIfApplicableCalled = false; + boolean isBindExceptionRequiredCalled = false; + + public CustomModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry adapterRegistry, boolean useDefaultResolution) { + super(adapterRegistry, useDefaultResolution); + } + + @Override + protected void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter, ServerWebExchange exchange) { + this.validateIfApplicableCalled = true; + super.validateIfApplicable(binder, parameter, exchange); + } + + @Override + protected boolean isBindExceptionRequired(MethodParameter parameter) { + this.isBindExceptionRequiredCalled = true; + return super.isBindExceptionRequired(parameter); + } + } + }