Skip to content

Commit e62ada8

Browse files
committed
Add @RequestAttribute with servlet-based support
Issue: SPR-13894
1 parent 698f923 commit e62ada8

File tree

12 files changed

+401
-137
lines changed

12 files changed

+401
-137
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2002-2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.bind.annotation;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.core.annotation.AliasFor;
26+
27+
/**
28+
* Annotation to bind a method parameter to a request attribute.
29+
*
30+
* <p>The main motivation is to provide convenient access to request attributes
31+
* from a controller method with an optional/required check and a cast to the
32+
* target method parameter type.
33+
*
34+
* @author Rossen Stoyanchev
35+
* @since 4.3
36+
* @see RequestMapping
37+
* @see SessionAttribute
38+
*/
39+
@Target(ElementType.PARAMETER)
40+
@Retention(RetentionPolicy.RUNTIME)
41+
@Documented
42+
public @interface RequestAttribute {
43+
44+
/**
45+
* Alias for {@link #name}.
46+
*/
47+
@AliasFor("name")
48+
String value() default "";
49+
50+
/**
51+
* The name of the request attribute to bind to.
52+
* <p>The default name is inferred from the method parameter name.
53+
*/
54+
@AliasFor("value")
55+
String name() default "";
56+
57+
/**
58+
* Whether the request attribute is required.
59+
* <p>Defaults to {@code true}, leading to an exception being thrown
60+
* if the attribute is missing. Switch this to {@code false} if you prefer
61+
* a {@code null} or Java 1.8+ {@code java.util.Optional} if the attribute
62+
* doesn't exist.
63+
*/
64+
boolean required() default true;
65+
66+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@
130130
* to existing, permanent session attributes (e.g. user authentication object)
131131
* as opposed to model attributes temporarily stored in the session as part of
132132
* a controller workflow via {@link SessionAttributes}.
133+
* <li>{@link RequestAttribute @RequestAttribute} annotated parameters for access
134+
* to request attributes.
133135
* <li>{@link org.springframework.http.HttpEntity HttpEntity&lt;?&gt;} parameters
134136
* (Servlet-only) for access to the Servlet request HTTP headers and contents.
135137
* The request stream will be converted to the entity body using

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
* @since 4.3
4343
* @see RequestMapping
4444
* @see SessionAttributes
45+
* @see RequestAttribute
4546
*/
4647
@Target(ElementType.PARAMETER)
4748
@Retention(RetentionPolicy.RUNTIME)

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
292292

293293
// Annotation-based argument resolution
294294
resolvers.add(new SessionAttributeMethodArgumentResolver());
295+
resolvers.add(new RequestAttributeMethodArgumentResolver());
295296

296297
// Type-based argument resolution
297298
resolvers.add(new ServletRequestMethodArgumentResolver());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2002-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.web.servlet.mvc.method.annotation;
17+
18+
import javax.servlet.ServletException;
19+
20+
import org.springframework.core.MethodParameter;
21+
import org.springframework.web.bind.ServletRequestBindingException;
22+
import org.springframework.web.bind.annotation.RequestAttribute;
23+
import org.springframework.web.bind.annotation.ValueConstants;
24+
import org.springframework.web.context.request.NativeWebRequest;
25+
import org.springframework.web.context.request.RequestAttributes;
26+
import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver;
27+
28+
/**
29+
* Resolves method arguments annotated with an @{@link RequestAttribute}.
30+
*
31+
* @author Rossen Stoyanchev
32+
* @since 4.3
33+
*/
34+
public class RequestAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
35+
36+
37+
@Override
38+
public boolean supportsParameter(MethodParameter parameter) {
39+
return parameter.hasParameterAnnotation(RequestAttribute.class);
40+
}
41+
42+
43+
@Override
44+
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
45+
RequestAttribute annot = parameter.getParameterAnnotation(RequestAttribute.class);
46+
return new NamedValueInfo(annot.name(), annot.required(), ValueConstants.DEFAULT_NONE);
47+
}
48+
49+
@Override
50+
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request){
51+
return request.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
52+
}
53+
54+
@Override
55+
protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException {
56+
throw new ServletRequestBindingException("Missing request attribute '" + name +
57+
"' of type " + parameter.getNestedParameterType().getSimpleName());
58+
}
59+
60+
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,7 @@ private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
601601
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
602602
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
603603
resolvers.add(new SessionAttributeMethodArgumentResolver());
604+
resolvers.add(new RequestAttributeMethodArgumentResolver());
604605

605606
// Type-based argument resolution
606607
resolvers.add(new ServletRequestMethodArgumentResolver());
@@ -641,6 +642,7 @@ private List<HandlerMethodArgumentResolver> getDefaultInitBinderArgumentResolver
641642
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
642643
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
643644
resolvers.add(new SessionAttributeMethodArgumentResolver());
645+
resolvers.add(new RequestAttributeMethodArgumentResolver());
644646

645647
// Type-based argument resolution
646648
resolvers.add(new ServletRequestMethodArgumentResolver());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* Copyright 2002-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.web.servlet.mvc.method.annotation;
17+
18+
import java.lang.reflect.Method;
19+
import java.util.Optional;
20+
import javax.servlet.http.HttpServletRequest;
21+
import javax.servlet.http.HttpServletResponse;
22+
23+
import org.junit.Before;
24+
import org.junit.Test;
25+
26+
import org.springframework.core.DefaultParameterNameDiscoverer;
27+
import org.springframework.core.GenericTypeResolver;
28+
import org.springframework.core.MethodParameter;
29+
import org.springframework.core.annotation.SynthesizingMethodParameter;
30+
import org.springframework.core.convert.support.DefaultConversionService;
31+
import org.springframework.mock.web.test.MockHttpServletRequest;
32+
import org.springframework.mock.web.test.MockHttpServletResponse;
33+
import org.springframework.web.bind.ServletRequestBindingException;
34+
import org.springframework.web.bind.WebDataBinder;
35+
import org.springframework.web.bind.annotation.RequestAttribute;
36+
import org.springframework.web.bind.annotation.SessionAttribute;
37+
import org.springframework.web.bind.support.WebDataBinderFactory;
38+
import org.springframework.web.bind.support.WebRequestDataBinder;
39+
import org.springframework.web.context.request.ServletWebRequest;
40+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
41+
import org.springframework.web.method.support.ModelAndViewContainer;
42+
43+
import static org.junit.Assert.assertEquals;
44+
import static org.junit.Assert.assertFalse;
45+
import static org.junit.Assert.assertNotNull;
46+
import static org.junit.Assert.assertNull;
47+
import static org.junit.Assert.assertSame;
48+
import static org.junit.Assert.assertTrue;
49+
import static org.junit.Assert.fail;
50+
import static org.mockito.BDDMockito.given;
51+
import static org.mockito.Mockito.mock;
52+
53+
/**
54+
* Base class for {@code @RequestAttribute} and {@code @SessionAttribute} method
55+
* method argument resolution tests.
56+
*
57+
* @author Rossen Stoyanchev
58+
* @since 4.3
59+
*/
60+
public abstract class AbstractRequestAttributesArgumentResolverTests {
61+
62+
private ServletWebRequest webRequest;
63+
64+
private HandlerMethodArgumentResolver resolver;
65+
66+
private Method handleMethod;
67+
68+
69+
@Before
70+
public void setUp() throws Exception {
71+
HttpServletRequest request = new MockHttpServletRequest();
72+
HttpServletResponse response = new MockHttpServletResponse();
73+
this.webRequest = new ServletWebRequest(request, response);
74+
this.resolver = createResolver();
75+
this.handleMethod = AbstractRequestAttributesArgumentResolverTests.class
76+
.getDeclaredMethod(getHandleMethodName(), Foo.class, Foo.class, Foo.class, Optional.class);
77+
}
78+
79+
80+
protected abstract HandlerMethodArgumentResolver createResolver();
81+
82+
protected abstract String getHandleMethodName();
83+
84+
protected abstract int getScope();
85+
86+
87+
@Test
88+
public void supportsParameter() throws Exception {
89+
assertTrue(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 0)));
90+
assertFalse(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 4)));
91+
}
92+
93+
@Test
94+
public void resolve() throws Exception {
95+
MethodParameter param = initMethodParameter(0);
96+
try {
97+
testResolveArgument(param);
98+
fail("Should be required by default");
99+
}
100+
catch (ServletRequestBindingException ex) {
101+
assertTrue(ex.getMessage().startsWith("Missing "));
102+
}
103+
104+
Foo foo = new Foo();
105+
this.webRequest.setAttribute("foo", foo, getScope());
106+
assertSame(foo, testResolveArgument(param));
107+
}
108+
109+
@Test
110+
public void resolveWithName() throws Exception {
111+
MethodParameter param = initMethodParameter(1);
112+
Foo foo = new Foo();
113+
this.webRequest.setAttribute("specialFoo", foo, getScope());
114+
assertSame(foo, testResolveArgument(param));
115+
}
116+
117+
@Test
118+
public void resolveNotRequired() throws Exception {
119+
MethodParameter param = initMethodParameter(2);
120+
assertNull(testResolveArgument(param));
121+
122+
Foo foo = new Foo();
123+
this.webRequest.setAttribute("foo", foo, getScope());
124+
assertSame(foo, testResolveArgument(param));
125+
}
126+
127+
@Test
128+
public void resolveOptional() throws Exception {
129+
WebDataBinder dataBinder = new WebRequestDataBinder(null);
130+
dataBinder.setConversionService(new DefaultConversionService());
131+
WebDataBinderFactory factory = mock(WebDataBinderFactory.class);
132+
given(factory.createBinder(this.webRequest, null, "foo")).willReturn(dataBinder);
133+
134+
MethodParameter param = initMethodParameter(3);
135+
Object actual = testResolveArgument(param, factory);
136+
assertNotNull(actual);
137+
assertEquals(Optional.class, actual.getClass());
138+
assertFalse(((Optional) actual).isPresent());
139+
140+
Foo foo = new Foo();
141+
this.webRequest.setAttribute("foo", foo, getScope());
142+
143+
actual = testResolveArgument(param, factory);
144+
assertNotNull(actual);
145+
assertEquals(Optional.class, actual.getClass());
146+
assertTrue(((Optional) actual).isPresent());
147+
assertSame(foo, ((Optional) actual).get());
148+
}
149+
150+
private Object testResolveArgument(MethodParameter param) throws Exception {
151+
return testResolveArgument(param, null);
152+
}
153+
154+
private Object testResolveArgument(MethodParameter param, WebDataBinderFactory factory) throws Exception {
155+
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
156+
return this.resolver.resolveArgument(param, mavContainer, this.webRequest, factory);
157+
}
158+
159+
private MethodParameter initMethodParameter(int parameterIndex) {
160+
MethodParameter param = new SynthesizingMethodParameter(this.handleMethod, parameterIndex);
161+
param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
162+
GenericTypeResolver.resolveParameterType(param, this.resolver.getClass());
163+
return param;
164+
}
165+
166+
167+
@SuppressWarnings("unused")
168+
private void handleWithRequestAttribute(
169+
@RequestAttribute Foo foo,
170+
@RequestAttribute("specialFoo") Foo namedFoo,
171+
@RequestAttribute(name="foo", required = false) Foo notRequiredFoo,
172+
@RequestAttribute(name="foo") Optional<Foo> optionalFoo) {
173+
}
174+
175+
@SuppressWarnings("unused")
176+
private void handleWithSessionAttribute(
177+
@SessionAttribute Foo foo,
178+
@SessionAttribute("specialFoo") Foo namedFoo,
179+
@SessionAttribute(name="foo", required = false) Foo notRequiredFoo,
180+
@SessionAttribute(name="foo") Optional<Foo> optionalFoo) {
181+
}
182+
183+
private static class Foo {
184+
}
185+
}

0 commit comments

Comments
 (0)