Document that ModelMap is not a supported argument type in WebFlux

Prior to this commit, the "Method Arguments" documentation for WebFlux
in the reference manual stated that WebFlux controller methods can
accept arguments of type Map, Model, or ModelMap to access the model.
However, ModelMap is actually not supported and results in exception
due to a type mismatch.

This commit updates the documentation to reflect this.

In addition, this commit updates related Javadoc and tests to avoid
mentioning or using ModelMap in WebFlux.

Closes gh-33107
This commit is contained in:
Sam Brannen 2024-06-27 11:33:50 +02:00
parent 1cf5264163
commit 8b11ee9ee2
8 changed files with 45 additions and 53 deletions

View File

@ -77,7 +77,7 @@ and others) and is equivalent to `required=false`.
| For access to a part in a `multipart/form-data` request. Supports reactive types.
See xref:web/webflux/controller/ann-methods/multipart-forms.adoc[Multipart Content] and xref:web/webflux/reactive-spring.adoc#webflux-multipart[Multipart Data].
| `java.util.Map`, `org.springframework.ui.Model`, and `org.springframework.ui.ModelMap`.
| `java.util.Map` or `org.springframework.ui.Model`
| For access to the model that is used in HTML controllers and is exposed to templates as
part of view rendering.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -46,7 +46,7 @@ public class ConcurrentModel extends ConcurrentHashMap<String, Object> implement
}
/**
* Construct a new {@code ModelMap} containing the supplied attribute
* Construct a new {@code ConcurrentModel} containing the supplied attribute
* under the supplied name.
* @see #addAttribute(String, Object)
*/
@ -55,8 +55,8 @@ public class ConcurrentModel extends ConcurrentHashMap<String, Object> implement
}
/**
* Construct a new {@code ModelMap} containing the supplied attribute.
* Uses attribute name generation to generate the key for the supplied model
* Construct a new {@code ConcurrentModel} containing the supplied attribute.
* <p>Uses attribute name generation to generate the key for the supplied model
* object.
* @see #addAttribute(Object)
*/

View File

@ -1,6 +1,6 @@
/**
* Generic support for UI layer concepts.
* Provides a generic ModelMap for model holding.
* <p>Provides generic {@code Model} and {@code ModelMap} holders for model attributes.
*/
@NonNullApi
@NonNullFields

View File

@ -18,11 +18,11 @@ package org.springframework.web.reactive.result.method.annotation;
import java.util.HashSet;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.server.WebSession;
import org.springframework.web.testfixture.server.MockWebSession;
@ -31,7 +31,8 @@ import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test fixture with {@link SessionAttributesHandler}.
* Tests for {@link SessionAttributesHandler}.
*
* @author Rossen Stoyanchev
*/
class SessionAttributesHandlerTests {
@ -86,11 +87,11 @@ class SessionAttributesHandlerTests {
@Test
void storeAttributes() {
ModelMap model = new ModelMap();
model.put("attr1", "value1");
model.put("attr2", "value2");
model.put("attr3", new TestBean());
Map<String, Object> model = Map.of(
"attr1", "value1",
"attr2", "value2",
"attr3", new TestBean()
);
WebSession session = new MockWebSession();
sessionAttributesHandler.storeAttributes(session, model);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,7 +20,6 @@ import java.util.List;
import java.util.Map;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.ui.ModelMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
@ -38,7 +37,7 @@ public class DummyMacroRequestContext {
private final ServerWebExchange exchange;
private final ModelMap model;
private final Map<String, Object> model;
private final GenericApplicationContext context;
@ -46,7 +45,7 @@ public class DummyMacroRequestContext {
private String contextPath;
public DummyMacroRequestContext(ServerWebExchange exchange, ModelMap model, GenericApplicationContext context) {
public DummyMacroRequestContext(ServerWebExchange exchange, Map<String, Object> model, GenericApplicationContext context) {
this.exchange = exchange;
this.model = model;
this.context = context;

View File

@ -17,11 +17,10 @@
package org.springframework.web.reactive.result.view;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
@ -30,8 +29,6 @@ import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.ModelMap;
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
import org.springframework.web.testfixture.server.MockServerWebExchange;
@ -48,7 +45,7 @@ class HttpMessageWriterViewTests {
private HttpMessageWriterView view = new HttpMessageWriterView(new Jackson2JsonEncoder());
private final ModelMap model = new ExtendedModelMap();
private final Map<String, Object> model = new HashMap<>();
private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
@ -63,18 +60,18 @@ class HttpMessageWriterViewTests {
@Test
void singleMatch() throws Exception {
this.view.setModelKeys(Collections.singleton("foo2"));
this.model.addAttribute("foo1", Collections.singleton("bar1"));
this.model.addAttribute("foo2", Collections.singleton("bar2"));
this.model.addAttribute("foo3", Collections.singleton("bar3"));
this.view.setModelKeys(Set.of("foo2"));
this.model.put("foo1", Set.of("bar1"));
this.model.put("foo2", Set.of("bar2"));
this.model.put("foo3", Set.of("bar3"));
assertThat(doRender()).isEqualTo("[\"bar2\"]");
}
@Test
void noMatch() throws Exception {
this.view.setModelKeys(Collections.singleton("foo2"));
this.model.addAttribute("foo1", "bar1");
this.view.setModelKeys(Set.of("foo2"));
this.model.put("foo1", "bar1");
assertThat(doRender()).isEmpty();
}
@ -82,18 +79,18 @@ class HttpMessageWriterViewTests {
@Test
void noMatchBecauseNotSupported() throws Exception {
this.view = new HttpMessageWriterView(new Jaxb2XmlEncoder());
this.view.setModelKeys(new HashSet<>(Collections.singletonList("foo1")));
this.model.addAttribute("foo1", "bar1");
this.view.setModelKeys(Set.of("foo1"));
this.model.put("foo1", "bar1");
assertThat(doRender()).isEmpty();
}
@Test
void multipleMatches() throws Exception {
this.view.setModelKeys(new HashSet<>(Arrays.asList("foo1", "foo2")));
this.model.addAttribute("foo1", Collections.singleton("bar1"));
this.model.addAttribute("foo2", Collections.singleton("bar2"));
this.model.addAttribute("foo3", Collections.singleton("bar3"));
this.view.setModelKeys(Set.of("foo1", "foo2"));
this.model.put("foo1", Set.of("bar1"));
this.model.put("foo2", Set.of("bar2"));
this.model.put("foo3", Set.of("bar3"));
assertThat(doRender()).isEqualTo("{\"foo1\":[\"bar1\"],\"foo2\":[\"bar2\"]}");
}
@ -101,13 +98,13 @@ class HttpMessageWriterViewTests {
@Test
void multipleMatchesNotSupported() throws Exception {
this.view = new HttpMessageWriterView(CharSequenceEncoder.allMimeTypes());
this.view.setModelKeys(new HashSet<>(Arrays.asList("foo1", "foo2")));
this.model.addAttribute("foo1", "bar1");
this.model.addAttribute("foo2", "bar2");
this.view.setModelKeys(Set.of("foo1", "foo2"));
this.model.put("foo1", "bar1");
this.model.put("foo2", "bar2");
assertThatIllegalStateException().isThrownBy(
this::doRender)
.withMessageContaining("Map rendering is not supported");
assertThatIllegalStateException()
.isThrownBy(this::doRender)
.withMessageContaining("Map rendering is not supported");
}
@Test
@ -115,8 +112,8 @@ class HttpMessageWriterViewTests {
Map<String, String> pojoData = new LinkedHashMap<>();
pojoData.put("foo", "f");
pojoData.put("bar", "b");
this.model.addAttribute("pojoData", pojoData);
this.view.setModelKeys(Collections.singleton("pojoData"));
this.model.put("pojoData", pojoData);
this.view.setModelKeys(Set.of("pojoData"));
this.view.render(this.model, MediaType.APPLICATION_JSON, exchange).block(Duration.ZERO);

View File

@ -37,8 +37,6 @@ import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.ModelMap;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.result.view.BindStatus;
@ -322,7 +320,7 @@ class FreeMarkerMacroTests {
names.put("Fred", "Fred Bloggs");
names.put("Rob&Harrop", "Rob Harrop");
ModelMap model = new ExtendedModelMap();
Map<String, Object> model = new HashMap<>();
DummyMacroRequestContext rc = new DummyMacroRequestContext(this.exchange, model,
this.applicationContext);
rc.setMessageMap(msgMap);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ package org.springframework.web.reactive.result.view.freemarker;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Locale;
import java.util.Map;
import freemarker.template.Configuration;
import org.junit.jupiter.api.BeforeEach;
@ -28,8 +29,6 @@ import reactor.test.StepVerifier;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.ModelMap;
import org.springframework.web.reactive.result.view.ZeroDemandResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
@ -125,8 +124,7 @@ class FreeMarkerViewTests {
freeMarkerView.setConfiguration(this.freeMarkerConfig);
freeMarkerView.setUrl("test.ftl");
ModelMap model = new ExtendedModelMap();
model.addAttribute("hello", "hi FreeMarker");
Map<String, Object> model = Map.of("hello", "hi FreeMarker");
freeMarkerView.render(model, null, this.exchange).block(Duration.ofMillis(5000));
StepVerifier.create(this.exchange.getResponse().getBody())
@ -148,8 +146,7 @@ class FreeMarkerViewTests {
freeMarkerView.setConfiguration(this.freeMarkerConfig);
freeMarkerView.setUrl("test.ftl");
ModelMap model = new ExtendedModelMap();
model.addAttribute("hello", "hi FreeMarker");
Map<String, Object> model = Map.of("hello", "hi FreeMarker");
freeMarkerView.render(model, null, exchange).subscribe();
response.cancelWrite();