Allow binding=false on @ModelAttribute

Issue: SPR-13402
This commit is contained in:
Rossen Stoyanchev 2016-01-26 21:12:21 -05:00
parent 806e79b14b
commit 2e7470b27f
8 changed files with 164 additions and 10 deletions

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.ui.Model; import org.springframework.ui.Model;
/** /**
@ -49,6 +50,7 @@ import org.springframework.ui.Model;
* access to a {@link Model} argument. * access to a {@link Model} argument.
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Rossen Stoyanchev
* @since 2.5 * @since 2.5
*/ */
@Target({ElementType.PARAMETER, ElementType.METHOD}) @Target({ElementType.PARAMETER, ElementType.METHOD})
@ -56,6 +58,12 @@ import org.springframework.ui.Model;
@Documented @Documented
public @interface ModelAttribute { public @interface ModelAttribute {
/**
* Alias for {@link #name}.
*/
@AliasFor("name")
String value() default "";
/** /**
* The name of the model attribute to bind to. * The name of the model attribute to bind to.
* <p>The default model attribute name is inferred from the declared * <p>The default model attribute name is inferred from the declared
@ -63,7 +71,19 @@ public @interface ModelAttribute {
* based on the non-qualified class name: * based on the non-qualified class name:
* e.g. "orderAddress" for class "mypackage.OrderAddress", * e.g. "orderAddress" for class "mypackage.OrderAddress",
* or "orderAddressList" for "List&lt;mypackage.OrderAddress&gt;". * or "orderAddressList" for "List&lt;mypackage.OrderAddress&gt;".
* @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.
* <p>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;
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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) : Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) :
createAttribute(name, parameter, binderFactory, webRequest)); 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); WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) { if (binder.getTarget() != null) {
bindRequestParameters(binder, webRequest); if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter); validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult()); throw new BindException(binder.getBindingResult());

View File

@ -132,9 +132,11 @@ public final class ModelFactory {
while (!this.modelMethods.isEmpty()) { while (!this.modelMethods.isEmpty()) {
InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod(); InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod();
ModelAttribute annot = modelMethod.getMethodAnnotation(ModelAttribute.class); ModelAttribute annotation = modelMethod.getMethodAnnotation(ModelAttribute.class);
String modelName = annot.value(); if (container.containsAttribute(annotation.name())) {
if (container.containsAttribute(modelName)) { if (!annotation.binding()) {
container.setBindingDisabled(annotation.name());
}
continue; continue;
} }
@ -142,6 +144,9 @@ public final class ModelFactory {
if (!modelMethod.isVoid()){ if (!modelMethod.isVoid()){
String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType()); String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType());
if (!annotation.binding()) {
container.setBindingDisabled(returnValueName);
}
if (!container.containsAttribute(returnValueName)) { if (!container.containsAttribute(returnValueName)) {
container.addAttribute(returnValueName, returnValue); container.addAttribute(returnValueName, returnValue);
} }

View File

@ -16,7 +16,9 @@
package org.springframework.web.method.support; package org.springframework.web.method.support;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.ui.Model; import org.springframework.ui.Model;
@ -55,6 +57,9 @@ public class ModelAndViewContainer {
private boolean redirectModelScenario = false; private boolean redirectModelScenario = false;
/* Names of attributes with binding disabled */
private final Set<String> bindingDisabledAttributes = new HashSet<String>(4);
private HttpStatus status; private HttpStatus status;
private final SessionStatus sessionStatus = new SimpleSessionStatus(); 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. * Whether to use the default model or the redirect model.
*/ */

View File

@ -62,6 +62,7 @@ public class ModelAttributeMethodProcessorTests {
private MethodParameter paramErrors; private MethodParameter paramErrors;
private MethodParameter paramInt; private MethodParameter paramInt;
private MethodParameter paramModelAttr; private MethodParameter paramModelAttr;
private MethodParameter paramBindingDisabledAttr;
private MethodParameter paramNonSimpleType; private MethodParameter paramNonSimpleType;
private MethodParameter returnParamNamedModelAttr; private MethodParameter returnParamNamedModelAttr;
@ -75,13 +76,15 @@ public class ModelAttributeMethodProcessorTests {
this.processor = new ModelAttributeMethodProcessor(false); this.processor = new ModelAttributeMethodProcessor(false);
Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute", 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.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0);
this.paramErrors = new SynthesizingMethodParameter(method, 1); this.paramErrors = new SynthesizingMethodParameter(method, 1);
this.paramInt = new SynthesizingMethodParameter(method, 2); this.paramInt = new SynthesizingMethodParameter(method, 2);
this.paramModelAttr = new SynthesizingMethodParameter(method, 3); 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"); method = getClass().getDeclaredMethod("annotatedReturnValue");
this.returnParamNamedModelAttr = new MethodParameter(method, -1); this.returnParamNamedModelAttr = new MethodParameter(method, -1);
@ -167,6 +170,41 @@ public class ModelAttributeMethodProcessorTests {
assertTrue(dataBinder.isValidateInvoked()); 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) @Test(expected = BindException.class)
public void resolveArgumentBindException() throws Exception { public void resolveArgumentBindException() throws Exception {
String name = "testBean"; String name = "testBean";
@ -281,6 +319,7 @@ public class ModelAttributeMethodProcessorTests {
Errors errors, Errors errors,
int intArg, int intArg,
@ModelAttribute TestBean defaultNameAttr, @ModelAttribute TestBean defaultNameAttr,
@ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr,
TestBean notAnnotatedAttr) { TestBean notAnnotatedAttr) {
} }
} }

View File

@ -115,6 +115,30 @@ public class ModelFactoryTests {
assertNull(this.mavContainer.getModel().get("name")); 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 @Test
public void sessionAttribute() throws Exception { public void sessionAttribute() throws Exception {
this.attributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue"); 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 { private static class TestController {
@ModelAttribute @ModelAttribute
@ -273,6 +297,11 @@ public class ModelFactoryTests {
return null; return null;
} }
@ModelAttribute(name="foo", binding=false)
public Foo modelAttrWithBindingDisabled() {
return new Foo();
}
public void handle() { public void handle() {
} }
@ -280,4 +309,7 @@ public class ModelFactoryTests {
} }
} }
private static class Foo {
}
} }

View File

@ -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 `<errors>` render the same form where the errors can be shown with the help of Spring's `<errors>`
form tag. 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 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. 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 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
use validation. use validation.
[[mvc-ann-sessionattrib]] [[mvc-ann-sessionattrib]]
==== Using @SessionAttributes to store model attributes in the HTTP session between requests ==== Using @SessionAttributes to store model attributes in the HTTP session between requests

View File

@ -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. * `@ResponseStatus` supported on the class level and inherited on all methods.
* New `@SessionAttribute` annotation for access to session attributes (see <<mvc-ann-sessionattrib-global, example>>). * New `@SessionAttribute` annotation for access to session attributes (see <<mvc-ann-sessionattrib-global, example>>).
* New `@RequestAttribute` annotation for access to session attributes (see <<mvc-ann-requestattrib, example>>). * New `@RequestAttribute` annotation for access to session attributes (see <<mvc-ann-requestattrib, example>>).
* `@ModelAttribute` allows preventing data binding via `binding=false` attribute (see <<mvc-ann-modelattrib-method-args, reference>>).
* `AsyncRestTemplate` supports request interception. * `AsyncRestTemplate` supports request interception.
=== WebSocket Messaging Improvements === WebSocket Messaging Improvements