From e62ada898be1180bc13824ad5c3056d42015db04 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 26 Jan 2016 16:36:11 -0500 Subject: [PATCH] Add @RequestAttribute with servlet-based support Issue: SPR-13894 --- .../web/bind/annotation/RequestAttribute.java | 66 +++++++ .../web/bind/annotation/RequestMapping.java | 2 + .../web/bind/annotation/SessionAttribute.java | 1 + .../ExceptionHandlerExceptionResolver.java | 1 + ...equestAttributeMethodArgumentResolver.java | 60 ++++++ .../RequestMappingHandlerAdapter.java | 2 + ...equestAttributesArgumentResolverTests.java | 185 ++++++++++++++++++ ...tAttributeMethodArgumentResolverTests.java | 45 +++++ ...MappingHandlerAdapterIntegrationTests.java | 8 +- ...nAttributeMethodArgumentResolverTests.java | 149 ++------------ src/asciidoc/web-mvc.adoc | 18 ++ src/asciidoc/whats-new.adoc | 1 + 12 files changed, 401 insertions(+), 137 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/bind/annotation/RequestAttribute.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestAttributeMethodArgumentResolver.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractRequestAttributesArgumentResolverTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestAttributeMethodArgumentResolverTests.java diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestAttribute.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestAttribute.java new file mode 100644 index 0000000000..edbf720fef --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestAttribute.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation to bind a method parameter to a request attribute. + * + *

The main motivation is to provide convenient access to request attributes + * from a controller method with an optional/required check and a cast to the + * target method parameter type. + * + * @author Rossen Stoyanchev + * @since 4.3 + * @see RequestMapping + * @see SessionAttribute + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RequestAttribute { + + /** + * Alias for {@link #name}. + */ + @AliasFor("name") + String value() default ""; + + /** + * The name of the request attribute to bind to. + *

The default name is inferred from the method parameter name. + */ + @AliasFor("value") + String name() default ""; + + /** + * Whether the request attribute is required. + *

Defaults to {@code true}, leading to an exception being thrown + * if the attribute is missing. Switch this to {@code false} if you prefer + * a {@code null} or Java 1.8+ {@code java.util.Optional} if the attribute + * doesn't exist. + */ + boolean required() default true; + +} diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index 950daea0db..e4430e9e4f 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -130,6 +130,8 @@ import org.springframework.core.annotation.AliasFor; * to existing, permanent session attributes (e.g. user authentication object) * as opposed to model attributes temporarily stored in the session as part of * a controller workflow via {@link SessionAttributes}. + *

  • {@link RequestAttribute @RequestAttribute} annotated parameters for access + * to request attributes. *
  • {@link org.springframework.http.HttpEntity HttpEntity<?>} parameters * (Servlet-only) for access to the Servlet request HTTP headers and contents. * The request stream will be converted to the entity body using diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttribute.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttribute.java index 9ed52c3d26..71def69d21 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttribute.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttribute.java @@ -42,6 +42,7 @@ import org.springframework.core.annotation.AliasFor; * @since 4.3 * @see RequestMapping * @see SessionAttributes + * @see RequestAttribute */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java index 24b5f767f6..76f9aa084b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java @@ -292,6 +292,7 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce // Annotation-based argument resolution resolvers.add(new SessionAttributeMethodArgumentResolver()); + resolvers.add(new RequestAttributeMethodArgumentResolver()); // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestAttributeMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestAttributeMethodArgumentResolver.java new file mode 100644 index 0000000000..3399068ea2 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestAttributeMethodArgumentResolver.java @@ -0,0 +1,60 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.mvc.method.annotation; + +import javax.servlet.ServletException; + +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver; + +/** + * Resolves method arguments annotated with an @{@link RequestAttribute}. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public class RequestAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RequestAttribute.class); + } + + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + RequestAttribute annot = parameter.getParameterAnnotation(RequestAttribute.class); + return new NamedValueInfo(annot.name(), annot.required(), ValueConstants.DEFAULT_NONE); + } + + @Override + protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request){ + return request.getAttribute(name, RequestAttributes.SCOPE_REQUEST); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException { + throw new ServletRequestBindingException("Missing request attribute '" + name + + "' of type " + parameter.getNestedParameterType().getSimpleName()); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 597ef15eda..74d579e5d1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -601,6 +601,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new SessionAttributeMethodArgumentResolver()); + resolvers.add(new RequestAttributeMethodArgumentResolver()); // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); @@ -641,6 +642,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter resolvers.add(new MatrixVariableMapMethodArgumentResolver()); resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new SessionAttributeMethodArgumentResolver()); + resolvers.add(new RequestAttributeMethodArgumentResolver()); // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractRequestAttributesArgumentResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractRequestAttributesArgumentResolverTests.java new file mode 100644 index 0000000000..3369883e7f --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractRequestAttributesArgumentResolverTests.java @@ -0,0 +1,185 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.mvc.method.annotation; + +import java.lang.reflect.Method; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.mock.web.test.MockHttpServletResponse; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.SessionAttribute; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.bind.support.WebRequestDataBinder; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Base class for {@code @RequestAttribute} and {@code @SessionAttribute} method + * method argument resolution tests. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public abstract class AbstractRequestAttributesArgumentResolverTests { + + private ServletWebRequest webRequest; + + private HandlerMethodArgumentResolver resolver; + + private Method handleMethod; + + + @Before + public void setUp() throws Exception { + HttpServletRequest request = new MockHttpServletRequest(); + HttpServletResponse response = new MockHttpServletResponse(); + this.webRequest = new ServletWebRequest(request, response); + this.resolver = createResolver(); + this.handleMethod = AbstractRequestAttributesArgumentResolverTests.class + .getDeclaredMethod(getHandleMethodName(), Foo.class, Foo.class, Foo.class, Optional.class); + } + + + protected abstract HandlerMethodArgumentResolver createResolver(); + + protected abstract String getHandleMethodName(); + + protected abstract int getScope(); + + + @Test + public void supportsParameter() throws Exception { + assertTrue(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 0))); + assertFalse(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 4))); + } + + @Test + public void resolve() throws Exception { + MethodParameter param = initMethodParameter(0); + try { + testResolveArgument(param); + fail("Should be required by default"); + } + catch (ServletRequestBindingException ex) { + assertTrue(ex.getMessage().startsWith("Missing ")); + } + + Foo foo = new Foo(); + this.webRequest.setAttribute("foo", foo, getScope()); + assertSame(foo, testResolveArgument(param)); + } + + @Test + public void resolveWithName() throws Exception { + MethodParameter param = initMethodParameter(1); + Foo foo = new Foo(); + this.webRequest.setAttribute("specialFoo", foo, getScope()); + assertSame(foo, testResolveArgument(param)); + } + + @Test + public void resolveNotRequired() throws Exception { + MethodParameter param = initMethodParameter(2); + assertNull(testResolveArgument(param)); + + Foo foo = new Foo(); + this.webRequest.setAttribute("foo", foo, getScope()); + assertSame(foo, testResolveArgument(param)); + } + + @Test + public void resolveOptional() throws Exception { + WebDataBinder dataBinder = new WebRequestDataBinder(null); + dataBinder.setConversionService(new DefaultConversionService()); + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(this.webRequest, null, "foo")).willReturn(dataBinder); + + MethodParameter param = initMethodParameter(3); + Object actual = testResolveArgument(param, factory); + assertNotNull(actual); + assertEquals(Optional.class, actual.getClass()); + assertFalse(((Optional) actual).isPresent()); + + Foo foo = new Foo(); + this.webRequest.setAttribute("foo", foo, getScope()); + + actual = testResolveArgument(param, factory); + assertNotNull(actual); + assertEquals(Optional.class, actual.getClass()); + assertTrue(((Optional) actual).isPresent()); + assertSame(foo, ((Optional) actual).get()); + } + + private Object testResolveArgument(MethodParameter param) throws Exception { + return testResolveArgument(param, null); + } + + private Object testResolveArgument(MethodParameter param, WebDataBinderFactory factory) throws Exception { + ModelAndViewContainer mavContainer = new ModelAndViewContainer(); + return this.resolver.resolveArgument(param, mavContainer, this.webRequest, factory); + } + + private MethodParameter initMethodParameter(int parameterIndex) { + MethodParameter param = new SynthesizingMethodParameter(this.handleMethod, parameterIndex); + param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + GenericTypeResolver.resolveParameterType(param, this.resolver.getClass()); + return param; + } + + + @SuppressWarnings("unused") + private void handleWithRequestAttribute( + @RequestAttribute Foo foo, + @RequestAttribute("specialFoo") Foo namedFoo, + @RequestAttribute(name="foo", required = false) Foo notRequiredFoo, + @RequestAttribute(name="foo") Optional optionalFoo) { + } + + @SuppressWarnings("unused") + private void handleWithSessionAttribute( + @SessionAttribute Foo foo, + @SessionAttribute("specialFoo") Foo namedFoo, + @SessionAttribute(name="foo", required = false) Foo notRequiredFoo, + @SessionAttribute(name="foo") Optional optionalFoo) { + } + + private static class Foo { + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestAttributeMethodArgumentResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestAttributeMethodArgumentResolverTests.java new file mode 100644 index 0000000000..90f875b26b --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestAttributeMethodArgumentResolverTests.java @@ -0,0 +1,45 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.mvc.method.annotation; + +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; + + +/** + * Unit tests for {@link RequestAttributeMethodArgumentResolver}. + * @author Rossen Stoyanchev + */ +public class RequestAttributeMethodArgumentResolverTests + extends AbstractRequestAttributesArgumentResolverTests { + + + @Override + protected HandlerMethodArgumentResolver createResolver() { + return new RequestAttributeMethodArgumentResolver(); + } + + @Override + protected String getHandleMethodName() { + return "handleWithRequestAttribute"; + } + + @Override + protected int getScope() { + return RequestAttributes.SCOPE_REQUEST; + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java index 0194af64f2..b9f2e8489e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java @@ -62,6 +62,7 @@ import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; @@ -149,13 +150,14 @@ public class RequestMappingHandlerAdapterIntegrationTests { public void handle() throws Exception { Class[] parameterTypes = new Class[] { int.class, String.class, String.class, String.class, Map.class, Date.class, Map.class, String.class, String.class, TestBean.class, Errors.class, TestBean.class, - Color.class, HttpServletRequest.class, HttpServletResponse.class, TestBean.class, + Color.class, HttpServletRequest.class, HttpServletResponse.class, TestBean.class, TestBean.class, User.class, OtherUser.class, Model.class, UriComponentsBuilder.class }; String datePattern = "yyyy.MM.dd"; String formattedDate = "2011.03.16"; Date date = new GregorianCalendar(2011, Calendar.MARCH, 16).getTime(); TestBean sessionAttribute = new TestBean(); + TestBean requestAttribute = new TestBean(); request.addHeader("Content-Type", "text/plain; charset=utf-8"); request.addHeader("header", "headerValue"); @@ -174,6 +176,7 @@ public class RequestMappingHandlerAdapterIntegrationTests { uriTemplateVars.put("pathvar", "pathvarValue"); request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); request.getSession().setAttribute("sessionAttribute", sessionAttribute); + request.setAttribute("requestAttribute", requestAttribute); HandlerMethod handlerMethod = handlerMethod("handle", parameterTypes); ModelAndView mav = handlerAdapter.handle(request, response, handlerMethod); @@ -219,6 +222,7 @@ public class RequestMappingHandlerAdapterIntegrationTests { assertEquals(OtherUser.class, model.get("otherUser").getClass()); assertSame(sessionAttribute, model.get("sessionAttribute")); + assertSame(requestAttribute, model.get("requestAttribute")); assertEquals(new URI("http://localhost/contextPath/main/path"), model.get("url")); } @@ -369,6 +373,7 @@ public class RequestMappingHandlerAdapterIntegrationTests { HttpServletRequest request, HttpServletResponse response, @SessionAttribute TestBean sessionAttribute, + @RequestAttribute TestBean requestAttribute, User user, @ModelAttribute OtherUser otherUser, Model model, @@ -380,6 +385,7 @@ public class RequestMappingHandlerAdapterIntegrationTests { .addAttribute("paramByConvention", paramByConvention).addAttribute("value", value) .addAttribute("customArg", customArg).addAttribute(user) .addAttribute("sessionAttribute", sessionAttribute) + .addAttribute("requestAttribute", requestAttribute) .addAttribute("url", builder.path("/path").build().toUri()); assertNotNull(request); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/SessionAttributeMethodArgumentResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/SessionAttributeMethodArgumentResolverTests.java index f59e914ccc..5a4f76f9ab 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/SessionAttributeMethodArgumentResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/SessionAttributeMethodArgumentResolverTests.java @@ -15,154 +15,31 @@ */ package org.springframework.web.servlet.mvc.method.annotation; -import java.lang.reflect.Method; -import java.util.Optional; - -import org.junit.Before; -import org.junit.Test; - -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.GenericTypeResolver; -import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.SynthesizingMethodParameter; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.mock.web.test.MockHttpServletRequest; -import org.springframework.mock.web.test.MockHttpServletResponse; -import org.springframework.web.bind.ServletRequestBindingException; -import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.bind.annotation.SessionAttribute; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.bind.support.WebRequestDataBinder; -import org.springframework.web.context.request.ServletWebRequest; -import org.springframework.web.method.support.ModelAndViewContainer; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; /** * Unit tests for {@link SessionAttributeMethodArgumentResolver}. * @author Rossen Stoyanchev */ -public class SessionAttributeMethodArgumentResolverTests { - - private ServletWebRequest webRequest; - - private MockHttpServletRequest servletRequest; - - private SessionAttributeMethodArgumentResolver resolver; - - private Method handleMethod; - +public class SessionAttributeMethodArgumentResolverTests + extends AbstractRequestAttributesArgumentResolverTests { - @Before - public void setUp() throws Exception { - this.servletRequest = new MockHttpServletRequest(); - this.webRequest = new ServletWebRequest(this.servletRequest, new MockHttpServletResponse()); - this.resolver = new SessionAttributeMethodArgumentResolver(); - this.handleMethod = getClass().getDeclaredMethod("handle", Foo.class, Foo.class, - Foo.class, Optional.class, Foo.class); - } - - - @Test - public void supportsParameter() throws Exception { - assertTrue(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 0))); - assertFalse(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 4))); - } - @Test - public void resolve() throws Exception { - MethodParameter param = initMethodParameter(0); - try { - testResolveArgument(param); - fail("Should be required by default"); - } - catch (ServletRequestBindingException ex) { - assertTrue(ex.getMessage().startsWith("Missing session attribute")); - } - - Foo foo = new Foo(); - this.servletRequest.getSession().setAttribute("foo", foo); - assertSame(foo, testResolveArgument(param)); - } - - @Test - public void resolveWithName() throws Exception { - MethodParameter param = initMethodParameter(1); - Foo foo = new Foo(); - this.servletRequest.getSession().setAttribute("specialFoo", foo); - assertSame(foo, testResolveArgument(param)); + @Override + protected HandlerMethodArgumentResolver createResolver() { + return new SessionAttributeMethodArgumentResolver(); } - @Test - public void resolveNotRequired() throws Exception { - MethodParameter param = initMethodParameter(2); - assertNull(testResolveArgument(param)); - - Foo foo = new Foo(); - this.servletRequest.getSession().setAttribute("foo", foo); - assertSame(foo, testResolveArgument(param)); - } - - @Test - public void resolveOptional() throws Exception { - WebDataBinder dataBinder = new WebRequestDataBinder(null); - dataBinder.setConversionService(new DefaultConversionService()); - WebDataBinderFactory factory = mock(WebDataBinderFactory.class); - given(factory.createBinder(this.webRequest, null, "foo")).willReturn(dataBinder); - - MethodParameter param = initMethodParameter(3); - Object actual = testResolveArgument(param, factory); - assertNotNull(actual); - assertEquals(Optional.class, actual.getClass()); - assertFalse(((Optional) actual).isPresent()); - - Foo foo = new Foo(); - this.servletRequest.getSession().setAttribute("foo", foo); - - actual = testResolveArgument(param, factory); - assertNotNull(actual); - assertEquals(Optional.class, actual.getClass()); - assertTrue(((Optional) actual).isPresent()); - assertSame(foo, ((Optional) actual).get()); - } - - - private Object testResolveArgument(MethodParameter param) throws Exception { - return testResolveArgument(param, null); - } - - private Object testResolveArgument(MethodParameter param, WebDataBinderFactory factory) throws Exception { - ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - return this.resolver.resolveArgument(param, mavContainer, this.webRequest, factory); - } - - private MethodParameter initMethodParameter(int parameterIndex) { - MethodParameter param = new SynthesizingMethodParameter(this.handleMethod, parameterIndex); - param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); - GenericTypeResolver.resolveParameterType(param, SessionAttributeMethodArgumentResolver.class); - return param; - } - - - @SuppressWarnings("unused") - private void handle( - @SessionAttribute Foo foo, - @SessionAttribute("specialFoo") Foo namedFoo, - @SessionAttribute(name="foo", required = false) Foo notRequiredFoo, - @SessionAttribute(name="foo") Optional optionalFoo, - Foo notAnnotatedFoo) { + @Override + protected String getHandleMethodName() { + return "handleWithSessionAttribute"; } - private static class Foo { + @Override + protected int getScope() { + return RequestAttributes.SCOPE_SESSION; } } diff --git a/src/asciidoc/web-mvc.adoc b/src/asciidoc/web-mvc.adoc index 6455eb1001..d7fd6b9184 100644 --- a/src/asciidoc/web-mvc.adoc +++ b/src/asciidoc/web-mvc.adoc @@ -1254,6 +1254,7 @@ multiple requests are allowed to access a session concurrently. session attributes (e.g. user authentication object) as opposed to model attributes temporarily stored in the session as part of a controller workflow via `@SessionAttributes`. +* `@RequestAttribute` annotated parameters for access to request attributes. * `HttpEntity` parameters for access to the Servlet request HTTP headers and contents. The request stream will be converted to the entity body using ++HttpMessageConverter++s. See <>. @@ -1794,6 +1795,23 @@ workflow consider using `SessionAttributes` as described in <>. +[[mvc-ann-requestattrib]] +==== Using @RequestAttribute to access request attributes + +Similar to `@SessionAttribute` the `@RequestAttribute` annotation can be used to +access pre-existing request attributes created by a filter or interceptor: + +[source,java,indent=0] +[subs="verbatim,quotes"] +---- + @RequestMapping("/") + public String handle(**@RequestAttribute** Client client) { + // ... + } +---- + + + [[mvc-ann-form-urlencoded-data]] ==== Working with "application/x-www-form-urlencoded" data diff --git a/src/asciidoc/whats-new.adoc b/src/asciidoc/whats-new.adoc index ed4c96537f..60fd8a0c80 100644 --- a/src/asciidoc/whats-new.adoc +++ b/src/asciidoc/whats-new.adoc @@ -665,6 +665,7 @@ Spring 4.3 also improves the caching abstraction as follows: * New `@RestControllerAdvice` annotation with combined `@ControllerAdvice` with `@ResponseBody` semantics. * `@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 <>). * `AsyncRestTemplate` supports request interception. === WebSocket Messaging Improvements -- GitLab