diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java
index 39f3141338f..74ce92a36f4 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -22,6 +22,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import org.springframework.core.annotation.AliasFor;
import org.springframework.ui.Model;
/**
@@ -49,6 +50,7 @@ import org.springframework.ui.Model;
* access to a {@link Model} argument.
*
* @author Juergen Hoeller
+ * @author Rossen Stoyanchev
* @since 2.5
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@@ -56,6 +58,12 @@ import org.springframework.ui.Model;
@Documented
public @interface ModelAttribute {
+ /**
+ * Alias for {@link #name}.
+ */
+ @AliasFor("name")
+ String value() default "";
+
/**
* The name of the model attribute to bind to.
*
The default model attribute name is inferred from the declared
@@ -63,7 +71,19 @@ public @interface ModelAttribute {
* based on the non-qualified class name:
* e.g. "orderAddress" for class "mypackage.OrderAddress",
* or "orderAddressList" for "List<mypackage.OrderAddress>".
+ * @since 4.3
*/
- String value() default "";
+ @AliasFor("value")
+ String name() default "";
+
+ /**
+ * Allows declaring data binding disabled directly on an
+ * {@code @ModelAttribute} method parameter or on the attribute returned from
+ * an {@code @ModelAttribute} method, both of which would prevent data
+ * binding for that attribute.
+ *
By default this is set to "true" in which case data binding applies.
+ * Set this to "false" to disable data binding.
+ */
+ boolean binding() default true;
}
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java
index da1f69ffc0e..d7b9e43f6fe 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -101,9 +101,18 @@ public class ModelAttributeMethodProcessor
Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) :
createAttribute(name, parameter, binderFactory, webRequest));
+ if (!mavContainer.isBindingDisabled(name)) {
+ ModelAttribute annotation = parameter.getParameterAnnotation(ModelAttribute.class);
+ if (annotation != null && !annotation.binding()) {
+ mavContainer.setBindingDisabled(name);
+ }
+ }
+
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
- bindRequestParameters(binder, webRequest);
+ if (!mavContainer.isBindingDisabled(name)) {
+ bindRequestParameters(binder, webRequest);
+ }
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java
index 3ef5012ebbb..475280165c4 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java
@@ -132,9 +132,11 @@ public final class ModelFactory {
while (!this.modelMethods.isEmpty()) {
InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod();
- ModelAttribute annot = modelMethod.getMethodAnnotation(ModelAttribute.class);
- String modelName = annot.value();
- if (container.containsAttribute(modelName)) {
+ ModelAttribute annotation = modelMethod.getMethodAnnotation(ModelAttribute.class);
+ if (container.containsAttribute(annotation.name())) {
+ if (!annotation.binding()) {
+ container.setBindingDisabled(annotation.name());
+ }
continue;
}
@@ -142,6 +144,9 @@ public final class ModelFactory {
if (!modelMethod.isVoid()){
String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType());
+ if (!annotation.binding()) {
+ container.setBindingDisabled(returnValueName);
+ }
if (!container.containsAttribute(returnValueName)) {
container.addAttribute(returnValueName, returnValue);
}
diff --git a/spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java b/spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java
index 08a8d588806..3c563356723 100644
--- a/spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java
+++ b/spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java
@@ -16,7 +16,9 @@
package org.springframework.web.method.support;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
@@ -55,6 +57,9 @@ public class ModelAndViewContainer {
private boolean redirectModelScenario = false;
+ /* Names of attributes with binding disabled */
+ private final Set bindingDisabledAttributes = new HashSet(4);
+
private HttpStatus status;
private final SessionStatus sessionStatus = new SimpleSessionStatus();
@@ -133,6 +138,23 @@ public class ModelAndViewContainer {
}
}
+ /**
+ * Register an attribute for which data binding should not occur, for example
+ * corresponding to an {@code @ModelAttribute(binding=false)} declaration.
+ * @param attributeName the name of the attribute
+ * @since 4.3
+ */
+ public void setBindingDisabled(String attributeName) {
+ this.bindingDisabledAttributes.add(attributeName);
+ }
+
+ /**
+ * Whether binding is disabled for the given model attribute.
+ */
+ public boolean isBindingDisabled(String name) {
+ return this.bindingDisabledAttributes.contains(name);
+ }
+
/**
* Whether to use the default model or the redirect model.
*/
diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java
index 1c9871bf3db..2010ba9dd92 100644
--- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java
+++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java
@@ -62,6 +62,7 @@ public class ModelAttributeMethodProcessorTests {
private MethodParameter paramErrors;
private MethodParameter paramInt;
private MethodParameter paramModelAttr;
+ private MethodParameter paramBindingDisabledAttr;
private MethodParameter paramNonSimpleType;
private MethodParameter returnParamNamedModelAttr;
@@ -75,13 +76,15 @@ public class ModelAttributeMethodProcessorTests {
this.processor = new ModelAttributeMethodProcessor(false);
Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute",
- TestBean.class, Errors.class, int.class, TestBean.class, TestBean.class);
+ TestBean.class, Errors.class, int.class, TestBean.class,
+ TestBean.class, TestBean.class);
this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0);
this.paramErrors = new SynthesizingMethodParameter(method, 1);
this.paramInt = new SynthesizingMethodParameter(method, 2);
this.paramModelAttr = new SynthesizingMethodParameter(method, 3);
- this.paramNonSimpleType = new SynthesizingMethodParameter(method, 4);
+ this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4);
+ this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5);
method = getClass().getDeclaredMethod("annotatedReturnValue");
this.returnParamNamedModelAttr = new MethodParameter(method, -1);
@@ -167,6 +170,41 @@ public class ModelAttributeMethodProcessorTests {
assertTrue(dataBinder.isValidateInvoked());
}
+ @Test
+ public void resolveArgumentBindingDisabledPreviously() throws Exception {
+ String name = "attrName";
+ Object target = new TestBean();
+ this.container.addAttribute(name, target);
+
+ // Declare binding disabled (e.g. via @ModelAttribute method)
+ this.container.setBindingDisabled(name);
+
+ StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
+ WebDataBinderFactory factory = mock(WebDataBinderFactory.class);
+ given(factory.createBinder(this.request, target, name)).willReturn(dataBinder);
+
+ this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory);
+
+ assertFalse(dataBinder.isBindInvoked());
+ assertTrue(dataBinder.isValidateInvoked());
+ }
+
+ @Test
+ public void resolveArgumentBindingDisabled() throws Exception {
+ String name = "noBindAttr";
+ Object target = new TestBean();
+ this.container.addAttribute(name, target);
+
+ StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
+ WebDataBinderFactory factory = mock(WebDataBinderFactory.class);
+ given(factory.createBinder(this.request, target, name)).willReturn(dataBinder);
+
+ this.processor.resolveArgument(this.paramBindingDisabledAttr, this.container, this.request, factory);
+
+ assertFalse(dataBinder.isBindInvoked());
+ assertTrue(dataBinder.isValidateInvoked());
+ }
+
@Test(expected = BindException.class)
public void resolveArgumentBindException() throws Exception {
String name = "testBean";
@@ -281,6 +319,7 @@ public class ModelAttributeMethodProcessorTests {
Errors errors,
int intArg,
@ModelAttribute TestBean defaultNameAttr,
+ @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr,
TestBean notAnnotatedAttr) {
}
}
diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java
index 54a1f2271ab..24c618255a4 100644
--- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java
+++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java
@@ -115,6 +115,30 @@ public class ModelFactoryTests {
assertNull(this.mavContainer.getModel().get("name"));
}
+ @Test
+ public void modelAttributeWithBindingDisabled() throws Exception {
+ ModelFactory modelFactory = createModelFactory("modelAttrWithBindingDisabled");
+ HandlerMethod handlerMethod = createHandlerMethod("handle");
+ modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
+
+ assertTrue(this.mavContainer.containsAttribute("foo"));
+ assertTrue(this.mavContainer.isBindingDisabled("foo"));
+ }
+
+ @Test
+ public void modelAttributeFromSessionWithBindingDisabled() throws Exception {
+ Foo foo = new Foo();
+ this.attributeStore.storeAttribute(this.webRequest, "foo", foo);
+
+ ModelFactory modelFactory = createModelFactory("modelAttrWithBindingDisabled");
+ HandlerMethod handlerMethod = createHandlerMethod("handle");
+ modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
+
+ assertTrue(this.mavContainer.containsAttribute("foo"));
+ assertSame(foo, this.mavContainer.getModel().get("foo"));
+ assertTrue(this.mavContainer.isBindingDisabled("foo"));
+ }
+
@Test
public void sessionAttribute() throws Exception {
this.attributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue");
@@ -250,7 +274,7 @@ public class ModelFactoryTests {
}
- @SessionAttributes("sessionAttr") @SuppressWarnings("unused")
+ @SessionAttributes({"sessionAttr", "foo"}) @SuppressWarnings("unused")
private static class TestController {
@ModelAttribute
@@ -273,6 +297,11 @@ public class ModelFactoryTests {
return null;
}
+ @ModelAttribute(name="foo", binding=false)
+ public Foo modelAttrWithBindingDisabled() {
+ return new Foo();
+ }
+
public void handle() {
}
@@ -280,4 +309,7 @@ public class ModelFactoryTests {
}
}
+ private static class Foo {
+ }
+
}
diff --git a/src/asciidoc/web-mvc.adoc b/src/asciidoc/web-mvc.adoc
index d7fd6b91840..cdc952f65fc 100644
--- a/src/asciidoc/web-mvc.adoc
+++ b/src/asciidoc/web-mvc.adoc
@@ -1704,6 +1704,31 @@ With a `BindingResult` you can check if errors were found in which case it's com
render the same form where the errors can be shown with the help of Spring's ``
form tag.
+Note that in some cases it may be useful to gain access to an attribute in the
+model without data binding. For such cases you may inject the `Model` into the
+controller or alternatively use the `binding` flag on the annotation:
+
+[source,java,indent=0]
+[subs="verbatim,quotes"]
+----
+@ModelAttribute
+public AccountForm setUpForm() {
+ return new AccountForm();
+}
+
+@ModelAttribute
+public Account findAccount(@PathVariable String accountId) {
+ return accountRepository.findOne(accountId);
+}
+
+@RequestMapping(path="update", method=POST)
+public String update(@Valid AccountUpdateForm form, BindingResult result,
+ **@ModelAttribute(binding=false)** Account account) {
+
+ // ...
+}
+----
+
In addition to data binding you can also invoke validation using your own custom
validator passing the same `BindingResult` that was used to record data binding errors.
That allows for data binding and validation errors to be accumulated in one place and
@@ -1747,6 +1772,7 @@ See <> and <> for details on how to confi
use validation.
+
[[mvc-ann-sessionattrib]]
==== Using @SessionAttributes to store model attributes in the HTTP session between requests
diff --git a/src/asciidoc/whats-new.adoc b/src/asciidoc/whats-new.adoc
index 60fd8a0c805..069b7efc581 100644
--- a/src/asciidoc/whats-new.adoc
+++ b/src/asciidoc/whats-new.adoc
@@ -666,6 +666,7 @@ Spring 4.3 also improves the caching abstraction as follows:
* `@ResponseStatus` supported on the class level and inherited on all methods.
* New `@SessionAttribute` annotation for access to session attributes (see <>).
* New `@RequestAttribute` annotation for access to session attributes (see <>).
+* `@ModelAttribute` allows preventing data binding via `binding=false` attribute (see <>).
* `AsyncRestTemplate` supports request interception.
=== WebSocket Messaging Improvements