Skip to content

Commit e2bfbdc

Browse files
committed
Support attribute overrides with @ResponseStatus
This commit introduces support for attribute overrides for @ResponseStatus when @ResponseStatus is used as a meta-annotation on a custom composed annotation. Specifically, this commit migrates all code that looks up @ResponseStatus from using AnnotationUtils.findAnnotation() to using AnnotatedElementUtils.findMergedAnnotation(). Issue: SPR-13441
1 parent 4a49ce9 commit e2bfbdc

File tree

8 files changed

+164
-65
lines changed

8 files changed

+164
-65
lines changed

spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/StatusAssertionTests.java

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@
1616

1717
package org.springframework.test.web.servlet.samples.standalone.resultmatchers;
1818

19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
1922
import org.junit.Test;
2023

24+
import org.springframework.core.annotation.AliasFor;
2125
import org.springframework.http.HttpStatus;
2226
import org.springframework.stereotype.Controller;
2327
import org.springframework.test.web.servlet.MockMvc;
@@ -26,6 +30,7 @@
2630
import org.springframework.web.bind.annotation.ResponseStatus;
2731

2832
import static org.hamcrest.Matchers.*;
33+
import static org.springframework.http.HttpStatus.*;
2934
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
3035
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
3136
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
@@ -34,6 +39,7 @@
3439
* Examples of expectations on the status and the status reason found in the response.
3540
*
3641
* @author Rossen Stoyanchev
42+
* @author Sam Brannen
3743
*/
3844
public class StatusAssertionTests {
3945

@@ -42,12 +48,14 @@ public class StatusAssertionTests {
4248
@Test
4349
public void testStatusInt() throws Exception {
4450
this.mockMvc.perform(get("/created")).andExpect(status().is(201));
51+
this.mockMvc.perform(get("/createdWithComposedAnnotation")).andExpect(status().is(201));
4552
this.mockMvc.perform(get("/badRequest")).andExpect(status().is(400));
4653
}
4754

4855
@Test
4956
public void testHttpStatus() throws Exception {
5057
this.mockMvc.perform(get("/created")).andExpect(status().isCreated());
58+
this.mockMvc.perform(get("/createdWithComposedAnnotation")).andExpect(status().isCreated());
5159
this.mockMvc.perform(get("/badRequest")).andExpect(status().isBadRequest());
5260
}
5361

@@ -66,27 +74,43 @@ public void testReasonEqualTo() throws Exception {
6674

6775
@Test
6876
public void testReasonMatcher() throws Exception {
69-
this.mockMvc.perform(get("/badRequest"))
70-
.andExpect(status().reason(endsWith("token")));
77+
this.mockMvc.perform(get("/badRequest")).andExpect(status().reason(endsWith("token")));
7178
}
7279

7380

81+
@RequestMapping
82+
@ResponseStatus
83+
@Retention(RetentionPolicy.RUNTIME)
84+
@interface Get {
85+
86+
@AliasFor(annotation = RequestMapping.class, attribute = "path")
87+
String[] path() default {};
88+
89+
@AliasFor(annotation = ResponseStatus.class, attribute = "code")
90+
HttpStatus status() default INTERNAL_SERVER_ERROR;
91+
}
92+
7493
@Controller
7594
private static class StatusController {
7695

7796
@RequestMapping("/created")
78-
@ResponseStatus(HttpStatus.CREATED)
97+
@ResponseStatus(CREATED)
7998
public @ResponseBody void created(){
8099
}
81100

101+
@Get(path = "/createdWithComposedAnnotation", status = CREATED)
102+
public @ResponseBody void createdWithComposedAnnotation() {
103+
}
104+
82105
@RequestMapping("/badRequest")
83-
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Expired token")
106+
@ResponseStatus(code = BAD_REQUEST, reason = "Expired token")
84107
public @ResponseBody void badRequest(){
85108
}
86109

87110
@RequestMapping("/notImplemented")
88-
@ResponseStatus(HttpStatus.NOT_IMPLEMENTED)
111+
@ResponseStatus(NOT_IMPLEMENTED)
89112
public @ResponseBody void notImplemented(){
90113
}
91114
}
115+
92116
}

spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,25 @@
2525
import org.springframework.beans.factory.BeanFactory;
2626
import org.springframework.core.BridgeMethodResolver;
2727
import org.springframework.core.MethodParameter;
28-
import org.springframework.core.annotation.AnnotationUtils;
28+
import org.springframework.core.annotation.AnnotatedElementUtils;
2929
import org.springframework.core.annotation.SynthesizingMethodParameter;
3030
import org.springframework.util.Assert;
3131
import org.springframework.util.ClassUtils;
3232

3333
/**
3434
* Encapsulates information about a handler method consisting of a
3535
* {@linkplain #getMethod() method} and a {@linkplain #getBean() bean}.
36-
* Provides convenient access to method parameters, method return value, method annotations.
36+
* Provides convenient access to method parameters, the method return value,
37+
* method annotations, etc.
3738
*
3839
* <p>The class may be created with a bean instance or with a bean name (e.g. lazy-init bean,
39-
* prototype bean). Use {@link #createWithResolvedBean()} to obtain a {@link HandlerMethod}
40+
* prototype bean). Use {@link #createWithResolvedBean()} to obtain a {@code HandlerMethod}
4041
* instance with a bean instance resolved through the associated {@link BeanFactory}.
4142
*
4243
* @author Arjen Poutsma
4344
* @author Rossen Stoyanchev
4445
* @author Juergen Hoeller
46+
* @author Sam Brannen
4547
* @since 3.1
4648
*/
4749
public class HandlerMethod {
@@ -98,7 +100,7 @@ public HandlerMethod(Object bean, String methodName, Class<?>... parameterTypes)
98100
/**
99101
* Create an instance from a bean name, a method, and a {@code BeanFactory}.
100102
* The method {@link #createWithResolvedBean()} may be used later to
101-
* re-create the {@code HandlerMethod} with an initialized the bean.
103+
* re-create the {@code HandlerMethod} with an initialized bean.
102104
*/
103105
public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
104106
Assert.hasText(beanName, "Bean name is required");
@@ -222,11 +224,14 @@ public boolean isVoid() {
222224
/**
223225
* Returns a single annotation on the underlying method traversing its super methods
224226
* if no annotation can be found on the given method itself.
227+
* <p>Also supports <em>merged</em> composed annotations with attribute
228+
* overrides as of Spring Framework 4.2.2.
225229
* @param annotationType the type of annotation to introspect the method for.
226230
* @return the annotation, or {@code null} if none found
231+
* @see AnnotatedElementUtils#findMergedAnnotation
227232
*/
228233
public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
229-
return AnnotationUtils.findAnnotation(this.method, annotationType);
234+
return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType);
230235
}
231236

232237
/**

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.springframework.core.DefaultParameterNameDiscoverer;
5757
import org.springframework.core.Ordered;
5858
import org.springframework.core.ParameterNameDiscoverer;
59+
import org.springframework.core.annotation.AnnotatedElementUtils;
5960
import org.springframework.core.annotation.AnnotationUtils;
6061
import org.springframework.http.HttpEntity;
6162
import org.springframework.http.HttpHeaders;
@@ -119,7 +120,7 @@
119120

120121
/**
121122
* Implementation of the {@link org.springframework.web.servlet.HandlerAdapter} interface
122-
* that maps handler methods based on HTTP paths, HTTP methods and request parameters
123+
* that maps handler methods based on HTTP paths, HTTP methods, and request parameters
123124
* expressed through the {@link RequestMapping} annotation.
124125
*
125126
* <p>Supports request parameter binding through the {@link RequestParam} annotation.
@@ -133,6 +134,7 @@
133134
*
134135
* @author Juergen Hoeller
135136
* @author Arjen Poutsma
137+
* @author Sam Brannen
136138
* @since 2.5
137139
* @see #setPathMatcher
138140
* @see #setMethodNameResolver
@@ -911,19 +913,19 @@ else if (Writer.class.isAssignableFrom(parameterType)) {
911913
public ModelAndView getModelAndView(Method handlerMethod, Class<?> handlerType, Object returnValue,
912914
ExtendedModelMap implicitModel, ServletWebRequest webRequest) throws Exception {
913915

914-
ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class);
915-
if (responseStatusAnn != null) {
916-
HttpStatus responseStatus = responseStatusAnn.code();
917-
String reason = responseStatusAnn.reason();
916+
ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(handlerMethod, ResponseStatus.class);
917+
if (responseStatus != null) {
918+
HttpStatus statusCode = responseStatus.code();
919+
String reason = responseStatus.reason();
918920
if (!StringUtils.hasText(reason)) {
919-
webRequest.getResponse().setStatus(responseStatus.value());
921+
webRequest.getResponse().setStatus(statusCode.value());
920922
}
921923
else {
922-
webRequest.getResponse().sendError(responseStatus.value(), reason);
924+
webRequest.getResponse().sendError(statusCode.value(), reason);
923925
}
924926

925927
// to be picked up by the RedirectView
926-
webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, responseStatus);
928+
webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, statusCode);
927929

928930
this.responseArgumentUsed = true;
929931
}

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.springframework.core.ExceptionDepthComparator;
4444
import org.springframework.core.GenericTypeResolver;
4545
import org.springframework.core.MethodParameter;
46+
import org.springframework.core.annotation.AnnotatedElementUtils;
4647
import org.springframework.core.annotation.AnnotationUtils;
4748
import org.springframework.core.annotation.SynthesizingMethodParameter;
4849
import org.springframework.http.HttpInputMessage;
@@ -377,15 +378,15 @@ private Object doInvokeMethod(Method method, Object target, Object[] args) throw
377378
private ModelAndView getModelAndView(Method handlerMethod, Object returnValue, ServletWebRequest webRequest)
378379
throws Exception {
379380

380-
ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class);
381-
if (responseStatusAnn != null) {
382-
HttpStatus responseStatus = responseStatusAnn.code();
383-
String reason = responseStatusAnn.reason();
381+
ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(handlerMethod, ResponseStatus.class);
382+
if (responseStatus != null) {
383+
HttpStatus statusCode = responseStatus.code();
384+
String reason = responseStatus.reason();
384385
if (!StringUtils.hasText(reason)) {
385-
webRequest.getResponse().setStatus(responseStatus.value());
386+
webRequest.getResponse().setStatus(statusCode.value());
386387
}
387388
else {
388-
webRequest.getResponse().sendError(responseStatus.value(), reason);
389+
webRequest.getResponse().sendError(statusCode.value(), reason);
389390
}
390391
}
391392

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import org.springframework.context.MessageSource;
2323
import org.springframework.context.MessageSourceAware;
2424
import org.springframework.context.i18n.LocaleContextHolder;
25-
import org.springframework.core.annotation.AnnotationUtils;
25+
import org.springframework.core.annotation.AnnotatedElementUtils;
2626
import org.springframework.util.StringUtils;
2727
import org.springframework.web.bind.annotation.ResponseStatus;
2828
import org.springframework.web.servlet.ModelAndView;
@@ -38,11 +38,14 @@
3838
* and the MVC Java config and the MVC namespace.
3939
*
4040
* <p>As of 4.2 this resolver also looks recursively for {@code @ResponseStatus}
41-
* present on cause exceptions.
41+
* present on cause exceptions, and as of 4.2.2 this resolver supports
42+
* attribute overrides for {@code @ResponseStatus} in custom composed annotations.
4243
*
4344
* @author Arjen Poutsma
4445
* @author Rossen Stoyanchev
46+
* @author Sam Brannen
4547
* @since 3.0
48+
* @see AnnotatedElementUtils#findMergedAnnotation
4649
*/
4750
public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver implements MessageSourceAware {
4851

@@ -59,7 +62,7 @@ public void setMessageSource(MessageSource messageSource) {
5962
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,
6063
Object handler, Exception ex) {
6164

62-
ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
65+
ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
6366
if (responseStatus != null) {
6467
try {
6568
return resolveResponseStatus(responseStatus, request, response, handler, ex);

0 commit comments

Comments
 (0)