diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 0024d5ec26..ed96f42558 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -17,6 +17,7 @@ package org.springframework.web.reactive.result.view; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -38,11 +39,14 @@ import org.springframework.http.MediaType; import org.springframework.ui.Model; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.WebExchangeDataBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.result.AbstractHandlerResultHandler; +import org.springframework.web.reactive.result.method.BindingContext; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.HttpRequestPathHelper; @@ -241,6 +245,7 @@ public class ViewResolutionResultHandler extends AbstractHandlerResultHandler } return resolveAsyncAttributes(model.asMap()) + .doOnSuccess(aVoid -> addBindingResult(result, exchange)) .then(viewsMono) .then(views -> render(views, model.asMap(), exchange)); }); @@ -322,6 +327,24 @@ public class ViewResolutionResultHandler extends AbstractHandlerResultHandler .then(); } + private void addBindingResult(HandlerResult result, ServerWebExchange exchange) { + BindingContext context = result.getBindingContext(); + Map model = context.getModel().asMap(); + model.keySet().stream() + .filter(name -> isBindingCandidate(name, model.get(name))) + .filter(name -> !model.containsKey(BindingResult.MODEL_KEY_PREFIX + name)) + .forEach(name -> { + WebExchangeDataBinder binder = context.createDataBinder(exchange, model.get(name), name); + model.put(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); + }); + } + + private boolean isBindingCandidate(String name, Object value) { + return !name.startsWith(BindingResult.MODEL_KEY_PREFIX) && value != null && + !value.getClass().isArray() && !(value instanceof Collection) && + !(value instanceof Map) && !BeanUtils.isSimpleValueType(value.getClass()); + } + private Mono render(List views, Map model, ServerWebExchange exchange) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 176e8e8d8d..57f2b4206f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -166,7 +166,12 @@ public class ViewResolutionResultHandlerTests { returnType = forClass(TestBean.class); returnValue = new TestBean("Joe"); - String responseBody = "account: {id=123, testBean=TestBean[name=Joe]}"; + String responseBody = "account: {" + + "id=123, " + + "org.springframework.validation.BindingResult.testBean=" + + "org.springframework.validation.BeanPropertyBindingResult: 0 errors, " + + "testBean=TestBean[name=Joe]" + + "}"; testHandle("/account", returnType, returnValue, responseBody, resolver); testHandle("/account", resolvableMethod().annotated(ModelAttribute.class), @@ -239,7 +244,11 @@ public class ViewResolutionResultHandlerTests { .block(Duration.ofSeconds(5)); assertEquals(APPLICATION_JSON, this.response.getHeaders().getContentType()); - assertResponseBody("jsonView: {testBean=TestBean[name=Joe]}"); + assertResponseBody("jsonView: {" + + "org.springframework.validation.BindingResult.testBean=" + + "org.springframework.validation.BeanPropertyBindingResult: 0 errors, " + + "testBean=TestBean[name=Joe]" + + "}"); } @Test @@ -272,7 +281,14 @@ public class ViewResolutionResultHandlerTests { this.request.setUri("/account"); handler.handleResult(this.exchange, result).blockMillis(5000); - assertResponseBody("account: {bean1=TestBean[name=Bean1], bean2=TestBean[name=Bean2]}"); + assertResponseBody("account: {" + + "bean1=TestBean[name=Bean1], " + + "bean2=TestBean[name=Bean2], " + + "org.springframework.validation.BindingResult.bean1=" + + "org.springframework.validation.BeanPropertyBindingResult: 0 errors, " + + "org.springframework.validation.BindingResult.bean2=" + + "org.springframework.validation.BeanPropertyBindingResult: 0 errors" + + "}"); }