Skip to content

Commit 8b11ee9

Browse files
committed
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
1 parent 1cf5264 commit 8b11ee9

File tree

8 files changed

+45
-53
lines changed

8 files changed

+45
-53
lines changed

framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/arguments.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ and others) and is equivalent to `required=false`.
7777
| For access to a part in a `multipart/form-data` request. Supports reactive types.
7878
See xref:web/webflux/controller/ann-methods/multipart-forms.adoc[Multipart Content] and xref:web/webflux/reactive-spring.adoc#webflux-multipart[Multipart Data].
7979

80-
| `java.util.Map`, `org.springframework.ui.Model`, and `org.springframework.ui.ModelMap`.
80+
| `java.util.Map` or `org.springframework.ui.Model`
8181
| For access to the model that is used in HTML controllers and is exposed to templates as
8282
part of view rendering.
8383

spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -46,7 +46,7 @@ public ConcurrentModel() {
4646
}
4747

4848
/**
49-
* Construct a new {@code ModelMap} containing the supplied attribute
49+
* Construct a new {@code ConcurrentModel} containing the supplied attribute
5050
* under the supplied name.
5151
* @see #addAttribute(String, Object)
5252
*/
@@ -55,8 +55,8 @@ public ConcurrentModel(String attributeName, Object attributeValue) {
5555
}
5656

5757
/**
58-
* Construct a new {@code ModelMap} containing the supplied attribute.
59-
* Uses attribute name generation to generate the key for the supplied model
58+
* Construct a new {@code ConcurrentModel} containing the supplied attribute.
59+
* <p>Uses attribute name generation to generate the key for the supplied model
6060
* object.
6161
* @see #addAttribute(Object)
6262
*/

spring-context/src/main/java/org/springframework/ui/package-info.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Generic support for UI layer concepts.
3-
* Provides a generic ModelMap for model holding.
3+
* <p>Provides generic {@code Model} and {@code ModelMap} holders for model attributes.
44
*/
55
@NonNullApi
66
@NonNullFields

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributesHandlerTests.java

+8-7
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818

1919

2020
import java.util.HashSet;
21+
import java.util.Map;
2122

2223
import org.junit.jupiter.api.Test;
2324

2425
import org.springframework.beans.testfixture.beans.TestBean;
25-
import org.springframework.ui.ModelMap;
2626
import org.springframework.web.bind.annotation.SessionAttributes;
2727
import org.springframework.web.server.WebSession;
2828
import org.springframework.web.testfixture.server.MockWebSession;
@@ -31,7 +31,8 @@
3131
import static org.assertj.core.api.Assertions.assertThat;
3232

3333
/**
34-
* Test fixture with {@link SessionAttributesHandler}.
34+
* Tests for {@link SessionAttributesHandler}.
35+
*
3536
* @author Rossen Stoyanchev
3637
*/
3738
class SessionAttributesHandlerTests {
@@ -86,11 +87,11 @@ void cleanupAttributes() {
8687

8788
@Test
8889
void storeAttributes() {
89-
90-
ModelMap model = new ModelMap();
91-
model.put("attr1", "value1");
92-
model.put("attr2", "value2");
93-
model.put("attr3", new TestBean());
90+
Map<String, Object> model = Map.of(
91+
"attr1", "value1",
92+
"attr2", "value2",
93+
"attr3", new TestBean()
94+
);
9495

9596
WebSession session = new MockWebSession();
9697
sessionAttributesHandler.storeAttributes(session, model);

spring-webflux/src/test/java/org/springframework/web/reactive/result/view/DummyMacroRequestContext.java

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,7 +20,6 @@
2020
import java.util.Map;
2121

2222
import org.springframework.context.support.GenericApplicationContext;
23-
import org.springframework.ui.ModelMap;
2423
import org.springframework.web.server.ServerWebExchange;
2524
import org.springframework.web.util.UriComponents;
2625
import org.springframework.web.util.UriComponentsBuilder;
@@ -38,15 +37,15 @@ public class DummyMacroRequestContext {
3837

3938
private final ServerWebExchange exchange;
4039

41-
private final ModelMap model;
40+
private final Map<String, Object> model;
4241

4342
private final GenericApplicationContext context;
4443

4544
private Map<String, String> messageMap;
4645

4746
private String contextPath;
4847

49-
public DummyMacroRequestContext(ServerWebExchange exchange, ModelMap model, GenericApplicationContext context) {
48+
public DummyMacroRequestContext(ServerWebExchange exchange, Map<String, Object> model, GenericApplicationContext context) {
5049
this.exchange = exchange;
5150
this.model = model;
5251
this.context = context;

spring-webflux/src/test/java/org/springframework/web/reactive/result/view/HttpMessageWriterViewTests.java

+23-26
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@
1717
package org.springframework.web.reactive.result.view;
1818

1919
import java.time.Duration;
20-
import java.util.Arrays;
21-
import java.util.Collections;
22-
import java.util.HashSet;
20+
import java.util.HashMap;
2321
import java.util.LinkedHashMap;
2422
import java.util.Map;
23+
import java.util.Set;
2524

2625
import org.junit.jupiter.api.Test;
2726
import reactor.test.StepVerifier;
@@ -30,8 +29,6 @@
3029
import org.springframework.http.MediaType;
3130
import org.springframework.http.codec.json.Jackson2JsonEncoder;
3231
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
33-
import org.springframework.ui.ExtendedModelMap;
34-
import org.springframework.ui.ModelMap;
3532
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
3633
import org.springframework.web.testfixture.server.MockServerWebExchange;
3734

@@ -48,7 +45,7 @@ class HttpMessageWriterViewTests {
4845

4946
private HttpMessageWriterView view = new HttpMessageWriterView(new Jackson2JsonEncoder());
5047

51-
private final ModelMap model = new ExtendedModelMap();
48+
private final Map<String, Object> model = new HashMap<>();
5249

5350
private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
5451

@@ -63,60 +60,60 @@ void supportedMediaTypes() {
6360

6461
@Test
6562
void singleMatch() throws Exception {
66-
this.view.setModelKeys(Collections.singleton("foo2"));
67-
this.model.addAttribute("foo1", Collections.singleton("bar1"));
68-
this.model.addAttribute("foo2", Collections.singleton("bar2"));
69-
this.model.addAttribute("foo3", Collections.singleton("bar3"));
63+
this.view.setModelKeys(Set.of("foo2"));
64+
this.model.put("foo1", Set.of("bar1"));
65+
this.model.put("foo2", Set.of("bar2"));
66+
this.model.put("foo3", Set.of("bar3"));
7067

7168
assertThat(doRender()).isEqualTo("[\"bar2\"]");
7269
}
7370

7471
@Test
7572
void noMatch() throws Exception {
76-
this.view.setModelKeys(Collections.singleton("foo2"));
77-
this.model.addAttribute("foo1", "bar1");
73+
this.view.setModelKeys(Set.of("foo2"));
74+
this.model.put("foo1", "bar1");
7875

7976
assertThat(doRender()).isEmpty();
8077
}
8178

8279
@Test
8380
void noMatchBecauseNotSupported() throws Exception {
8481
this.view = new HttpMessageWriterView(new Jaxb2XmlEncoder());
85-
this.view.setModelKeys(new HashSet<>(Collections.singletonList("foo1")));
86-
this.model.addAttribute("foo1", "bar1");
82+
this.view.setModelKeys(Set.of("foo1"));
83+
this.model.put("foo1", "bar1");
8784

8885
assertThat(doRender()).isEmpty();
8986
}
9087

9188
@Test
9289
void multipleMatches() throws Exception {
93-
this.view.setModelKeys(new HashSet<>(Arrays.asList("foo1", "foo2")));
94-
this.model.addAttribute("foo1", Collections.singleton("bar1"));
95-
this.model.addAttribute("foo2", Collections.singleton("bar2"));
96-
this.model.addAttribute("foo3", Collections.singleton("bar3"));
90+
this.view.setModelKeys(Set.of("foo1", "foo2"));
91+
this.model.put("foo1", Set.of("bar1"));
92+
this.model.put("foo2", Set.of("bar2"));
93+
this.model.put("foo3", Set.of("bar3"));
9794

9895
assertThat(doRender()).isEqualTo("{\"foo1\":[\"bar1\"],\"foo2\":[\"bar2\"]}");
9996
}
10097

10198
@Test
10299
void multipleMatchesNotSupported() throws Exception {
103100
this.view = new HttpMessageWriterView(CharSequenceEncoder.allMimeTypes());
104-
this.view.setModelKeys(new HashSet<>(Arrays.asList("foo1", "foo2")));
105-
this.model.addAttribute("foo1", "bar1");
106-
this.model.addAttribute("foo2", "bar2");
101+
this.view.setModelKeys(Set.of("foo1", "foo2"));
102+
this.model.put("foo1", "bar1");
103+
this.model.put("foo2", "bar2");
107104

108-
assertThatIllegalStateException().isThrownBy(
109-
this::doRender)
110-
.withMessageContaining("Map rendering is not supported");
105+
assertThatIllegalStateException()
106+
.isThrownBy(this::doRender)
107+
.withMessageContaining("Map rendering is not supported");
111108
}
112109

113110
@Test
114111
void render() throws Exception {
115112
Map<String, String> pojoData = new LinkedHashMap<>();
116113
pojoData.put("foo", "f");
117114
pojoData.put("bar", "b");
118-
this.model.addAttribute("pojoData", pojoData);
119-
this.view.setModelKeys(Collections.singleton("pojoData"));
115+
this.model.put("pojoData", pojoData);
116+
this.view.setModelKeys(Set.of("pojoData"));
120117

121118
this.view.render(this.model, MediaType.APPLICATION_JSON, exchange).block(Duration.ZERO);
122119

spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerMacroTests.java

+1-3
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@
3737
import org.springframework.context.support.GenericApplicationContext;
3838
import org.springframework.core.io.ClassPathResource;
3939
import org.springframework.http.MediaType;
40-
import org.springframework.ui.ExtendedModelMap;
41-
import org.springframework.ui.ModelMap;
4240
import org.springframework.util.FileCopyUtils;
4341
import org.springframework.util.StringUtils;
4442
import org.springframework.web.reactive.result.view.BindStatus;
@@ -322,7 +320,7 @@ private Mono<List<String>> getMacroOutput(String name) throws Exception {
322320
names.put("Fred", "Fred Bloggs");
323321
names.put("Rob&Harrop", "Rob Harrop");
324322

325-
ModelMap model = new ExtendedModelMap();
323+
Map<String, Object> model = new HashMap<>();
326324
DummyMacroRequestContext rc = new DummyMacroRequestContext(this.exchange, model,
327325
this.applicationContext);
328326
rc.setMessageMap(msgMap);

spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.nio.charset.StandardCharsets;
2020
import java.time.Duration;
2121
import java.util.Locale;
22+
import java.util.Map;
2223

2324
import freemarker.template.Configuration;
2425
import org.junit.jupiter.api.BeforeEach;
@@ -28,8 +29,6 @@
2829
import org.springframework.context.ApplicationContextException;
2930
import org.springframework.context.support.GenericApplicationContext;
3031
import org.springframework.http.codec.ServerCodecConfigurer;
31-
import org.springframework.ui.ExtendedModelMap;
32-
import org.springframework.ui.ModelMap;
3332
import org.springframework.web.reactive.result.view.ZeroDemandResponse;
3433
import org.springframework.web.server.ServerWebExchange;
3534
import org.springframework.web.server.adapter.DefaultServerWebExchange;
@@ -125,8 +124,7 @@ void render() {
125124
freeMarkerView.setConfiguration(this.freeMarkerConfig);
126125
freeMarkerView.setUrl("test.ftl");
127126

128-
ModelMap model = new ExtendedModelMap();
129-
model.addAttribute("hello", "hi FreeMarker");
127+
Map<String, Object> model = Map.of("hello", "hi FreeMarker");
130128
freeMarkerView.render(model, null, this.exchange).block(Duration.ofMillis(5000));
131129

132130
StepVerifier.create(this.exchange.getResponse().getBody())
@@ -148,8 +146,7 @@ void subscribeWithoutDemand() {
148146
freeMarkerView.setConfiguration(this.freeMarkerConfig);
149147
freeMarkerView.setUrl("test.ftl");
150148

151-
ModelMap model = new ExtendedModelMap();
152-
model.addAttribute("hello", "hi FreeMarker");
149+
Map<String, Object> model = Map.of("hello", "hi FreeMarker");
153150
freeMarkerView.render(model, null, exchange).subscribe();
154151

155152
response.cancelWrite();

0 commit comments

Comments
 (0)