Skip to content

Commit 2e7470b

Browse files
committed
Allow binding=false on @ModelAttribute
Issue: SPR-13402
1 parent 806e79b commit 2e7470b

File tree

8 files changed

+164
-10
lines changed

8 files changed

+164
-10
lines changed

spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -22,6 +22,7 @@
2222
import java.lang.annotation.RetentionPolicy;
2323
import java.lang.annotation.Target;
2424

25+
import org.springframework.core.annotation.AliasFor;
2526
import org.springframework.ui.Model;
2627

2728
/**
@@ -49,21 +50,40 @@
4950
* access to a {@link Model} argument.
5051
*
5152
* @author Juergen Hoeller
53+
* @author Rossen Stoyanchev
5254
* @since 2.5
5355
*/
5456
@Target({ElementType.PARAMETER, ElementType.METHOD})
5557
@Retention(RetentionPolicy.RUNTIME)
5658
@Documented
5759
public @interface ModelAttribute {
5860

61+
/**
62+
* Alias for {@link #name}.
63+
*/
64+
@AliasFor("name")
65+
String value() default "";
66+
5967
/**
6068
* The name of the model attribute to bind to.
6169
* <p>The default model attribute name is inferred from the declared
6270
* attribute type (i.e. the method parameter type or method return type),
6371
* based on the non-qualified class name:
6472
* e.g. "orderAddress" for class "mypackage.OrderAddress",
6573
* or "orderAddressList" for "List&lt;mypackage.OrderAddress&gt;".
74+
* @since 4.3
6675
*/
67-
String value() default "";
76+
@AliasFor("value")
77+
String name() default "";
78+
79+
/**
80+
* Allows declaring data binding disabled directly on an
81+
* {@code @ModelAttribute} method parameter or on the attribute returned from
82+
* an {@code @ModelAttribute} method, both of which would prevent data
83+
* binding for that attribute.
84+
* <p>By default this is set to "true" in which case data binding applies.
85+
* Set this to "false" to disable data binding.
86+
*/
87+
boolean binding() default true;
6888

6989
}

spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -101,9 +101,18 @@ public final Object resolveArgument(MethodParameter parameter, ModelAndViewConta
101101
Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) :
102102
createAttribute(name, parameter, binderFactory, webRequest));
103103

104+
if (!mavContainer.isBindingDisabled(name)) {
105+
ModelAttribute annotation = parameter.getParameterAnnotation(ModelAttribute.class);
106+
if (annotation != null && !annotation.binding()) {
107+
mavContainer.setBindingDisabled(name);
108+
}
109+
}
110+
104111
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
105112
if (binder.getTarget() != null) {
106-
bindRequestParameters(binder, webRequest);
113+
if (!mavContainer.isBindingDisabled(name)) {
114+
bindRequestParameters(binder, webRequest);
115+
}
107116
validateIfApplicable(binder, parameter);
108117
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
109118
throw new BindException(binder.getBindingResult());

spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,21 @@ private void invokeModelAttributeMethods(NativeWebRequest request,
132132

133133
while (!this.modelMethods.isEmpty()) {
134134
InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod();
135-
ModelAttribute annot = modelMethod.getMethodAnnotation(ModelAttribute.class);
136-
String modelName = annot.value();
137-
if (container.containsAttribute(modelName)) {
135+
ModelAttribute annotation = modelMethod.getMethodAnnotation(ModelAttribute.class);
136+
if (container.containsAttribute(annotation.name())) {
137+
if (!annotation.binding()) {
138+
container.setBindingDisabled(annotation.name());
139+
}
138140
continue;
139141
}
140142

141143
Object returnValue = modelMethod.invokeForRequest(request, container);
142144

143145
if (!modelMethod.isVoid()){
144146
String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType());
147+
if (!annotation.binding()) {
148+
container.setBindingDisabled(returnValueName);
149+
}
145150
if (!container.containsAttribute(returnValueName)) {
146151
container.addAttribute(returnValueName, returnValue);
147152
}

spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package org.springframework.web.method.support;
1818

19+
import java.util.HashSet;
1920
import java.util.Map;
21+
import java.util.Set;
2022

2123
import org.springframework.http.HttpStatus;
2224
import org.springframework.ui.Model;
@@ -55,6 +57,9 @@ public class ModelAndViewContainer {
5557

5658
private boolean redirectModelScenario = false;
5759

60+
/* Names of attributes with binding disabled */
61+
private final Set<String> bindingDisabledAttributes = new HashSet<String>(4);
62+
5863
private HttpStatus status;
5964

6065
private final SessionStatus sessionStatus = new SimpleSessionStatus();
@@ -133,6 +138,23 @@ public ModelMap getModel() {
133138
}
134139
}
135140

141+
/**
142+
* Register an attribute for which data binding should not occur, for example
143+
* corresponding to an {@code @ModelAttribute(binding=false)} declaration.
144+
* @param attributeName the name of the attribute
145+
* @since 4.3
146+
*/
147+
public void setBindingDisabled(String attributeName) {
148+
this.bindingDisabledAttributes.add(attributeName);
149+
}
150+
151+
/**
152+
* Whether binding is disabled for the given model attribute.
153+
*/
154+
public boolean isBindingDisabled(String name) {
155+
return this.bindingDisabledAttributes.contains(name);
156+
}
157+
136158
/**
137159
* Whether to use the default model or the redirect model.
138160
*/

spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public class ModelAttributeMethodProcessorTests {
6262
private MethodParameter paramErrors;
6363
private MethodParameter paramInt;
6464
private MethodParameter paramModelAttr;
65+
private MethodParameter paramBindingDisabledAttr;
6566
private MethodParameter paramNonSimpleType;
6667

6768
private MethodParameter returnParamNamedModelAttr;
@@ -75,13 +76,15 @@ public void setUp() throws Exception {
7576
this.processor = new ModelAttributeMethodProcessor(false);
7677

7778
Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute",
78-
TestBean.class, Errors.class, int.class, TestBean.class, TestBean.class);
79+
TestBean.class, Errors.class, int.class, TestBean.class,
80+
TestBean.class, TestBean.class);
7981

8082
this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0);
8183
this.paramErrors = new SynthesizingMethodParameter(method, 1);
8284
this.paramInt = new SynthesizingMethodParameter(method, 2);
8385
this.paramModelAttr = new SynthesizingMethodParameter(method, 3);
84-
this.paramNonSimpleType = new SynthesizingMethodParameter(method, 4);
86+
this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4);
87+
this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5);
8588

8689
method = getClass().getDeclaredMethod("annotatedReturnValue");
8790
this.returnParamNamedModelAttr = new MethodParameter(method, -1);
@@ -167,6 +170,41 @@ public void resolveArgumentValidation() throws Exception {
167170
assertTrue(dataBinder.isValidateInvoked());
168171
}
169172

173+
@Test
174+
public void resolveArgumentBindingDisabledPreviously() throws Exception {
175+
String name = "attrName";
176+
Object target = new TestBean();
177+
this.container.addAttribute(name, target);
178+
179+
// Declare binding disabled (e.g. via @ModelAttribute method)
180+
this.container.setBindingDisabled(name);
181+
182+
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
183+
WebDataBinderFactory factory = mock(WebDataBinderFactory.class);
184+
given(factory.createBinder(this.request, target, name)).willReturn(dataBinder);
185+
186+
this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory);
187+
188+
assertFalse(dataBinder.isBindInvoked());
189+
assertTrue(dataBinder.isValidateInvoked());
190+
}
191+
192+
@Test
193+
public void resolveArgumentBindingDisabled() throws Exception {
194+
String name = "noBindAttr";
195+
Object target = new TestBean();
196+
this.container.addAttribute(name, target);
197+
198+
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
199+
WebDataBinderFactory factory = mock(WebDataBinderFactory.class);
200+
given(factory.createBinder(this.request, target, name)).willReturn(dataBinder);
201+
202+
this.processor.resolveArgument(this.paramBindingDisabledAttr, this.container, this.request, factory);
203+
204+
assertFalse(dataBinder.isBindInvoked());
205+
assertTrue(dataBinder.isValidateInvoked());
206+
}
207+
170208
@Test(expected = BindException.class)
171209
public void resolveArgumentBindException() throws Exception {
172210
String name = "testBean";
@@ -281,6 +319,7 @@ public void modelAttribute(
281319
Errors errors,
282320
int intArg,
283321
@ModelAttribute TestBean defaultNameAttr,
322+
@ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr,
284323
TestBean notAnnotatedAttr) {
285324
}
286325
}

spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,30 @@ public void modelAttributeMethodWithNullReturnValue() throws Exception {
115115
assertNull(this.mavContainer.getModel().get("name"));
116116
}
117117

118+
@Test
119+
public void modelAttributeWithBindingDisabled() throws Exception {
120+
ModelFactory modelFactory = createModelFactory("modelAttrWithBindingDisabled");
121+
HandlerMethod handlerMethod = createHandlerMethod("handle");
122+
modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
123+
124+
assertTrue(this.mavContainer.containsAttribute("foo"));
125+
assertTrue(this.mavContainer.isBindingDisabled("foo"));
126+
}
127+
128+
@Test
129+
public void modelAttributeFromSessionWithBindingDisabled() throws Exception {
130+
Foo foo = new Foo();
131+
this.attributeStore.storeAttribute(this.webRequest, "foo", foo);
132+
133+
ModelFactory modelFactory = createModelFactory("modelAttrWithBindingDisabled");
134+
HandlerMethod handlerMethod = createHandlerMethod("handle");
135+
modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
136+
137+
assertTrue(this.mavContainer.containsAttribute("foo"));
138+
assertSame(foo, this.mavContainer.getModel().get("foo"));
139+
assertTrue(this.mavContainer.isBindingDisabled("foo"));
140+
}
141+
118142
@Test
119143
public void sessionAttribute() throws Exception {
120144
this.attributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue");
@@ -250,7 +274,7 @@ private InvocableHandlerMethod createHandlerMethod(String methodName, Class<?>..
250274
}
251275

252276

253-
@SessionAttributes("sessionAttr") @SuppressWarnings("unused")
277+
@SessionAttributes({"sessionAttr", "foo"}) @SuppressWarnings("unused")
254278
private static class TestController {
255279

256280
@ModelAttribute
@@ -273,11 +297,19 @@ public Boolean nullModelAttr() {
273297
return null;
274298
}
275299

300+
@ModelAttribute(name="foo", binding=false)
301+
public Foo modelAttrWithBindingDisabled() {
302+
return new Foo();
303+
}
304+
276305
public void handle() {
277306
}
278307

279308
public void handleSessionAttr(@ModelAttribute("sessionAttr") String sessionAttr) {
280309
}
281310
}
282311

312+
private static class Foo {
313+
}
314+
283315
}

src/asciidoc/web-mvc.adoc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,6 +1704,31 @@ With a `BindingResult` you can check if errors were found in which case it's com
17041704
render the same form where the errors can be shown with the help of Spring's `<errors>`
17051705
form tag.
17061706

1707+
Note that in some cases it may be useful to gain access to an attribute in the
1708+
model without data binding. For such cases you may inject the `Model` into the
1709+
controller or alternatively use the `binding` flag on the annotation:
1710+
1711+
[source,java,indent=0]
1712+
[subs="verbatim,quotes"]
1713+
----
1714+
@ModelAttribute
1715+
public AccountForm setUpForm() {
1716+
return new AccountForm();
1717+
}
1718+
1719+
@ModelAttribute
1720+
public Account findAccount(@PathVariable String accountId) {
1721+
return accountRepository.findOne(accountId);
1722+
}
1723+
1724+
@RequestMapping(path="update", method=POST)
1725+
public String update(@Valid AccountUpdateForm form, BindingResult result,
1726+
**@ModelAttribute(binding=false)** Account account) {
1727+
1728+
// ...
1729+
}
1730+
----
1731+
17071732
In addition to data binding you can also invoke validation using your own custom
17081733
validator passing the same `BindingResult` that was used to record data binding errors.
17091734
That allows for data binding and validation errors to be accumulated in one place and
@@ -1747,6 +1772,7 @@ See <<validation-beanvalidation>> and <<validation>> for details on how to confi
17471772
use validation.
17481773

17491774

1775+
17501776
[[mvc-ann-sessionattrib]]
17511777
==== Using @SessionAttributes to store model attributes in the HTTP session between requests
17521778

src/asciidoc/whats-new.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,7 @@ Spring 4.3 also improves the caching abstraction as follows:
666666
* `@ResponseStatus` supported on the class level and inherited on all methods.
667667
* New `@SessionAttribute` annotation for access to session attributes (see <<mvc-ann-sessionattrib-global, example>>).
668668
* New `@RequestAttribute` annotation for access to session attributes (see <<mvc-ann-requestattrib, example>>).
669+
* `@ModelAttribute` allows preventing data binding via `binding=false` attribute (see <<mvc-ann-modelattrib-method-args, reference>>).
669670
* `AsyncRestTemplate` supports request interception.
670671

671672
=== WebSocket Messaging Improvements

0 commit comments

Comments
 (0)