From 2e7470b27f0eaae042334cd86f212cd958676be0 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 26 Jan 2016 21:12:21 -0500 Subject: [PATCH] Allow binding=false on @ModelAttribute Issue: SPR-13402 --- .../web/bind/annotation/ModelAttribute.java | 24 ++++++++++- .../ModelAttributeMethodProcessor.java | 13 +++++- .../web/method/annotation/ModelFactory.java | 11 +++-- .../method/support/ModelAndViewContainer.java | 22 ++++++++++ .../ModelAttributeMethodProcessorTests.java | 43 ++++++++++++++++++- .../method/annotation/ModelFactoryTests.java | 34 ++++++++++++++- src/asciidoc/web-mvc.adoc | 26 +++++++++++ src/asciidoc/whats-new.adoc | 1 + 8 files changed, 164 insertions(+), 10 deletions(-) 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