Skip to content

Commit 790d515

Browse files
committed
HandlerMethod exposes interface parameter annotations as well
The HandlerMethodParameter arrangement uses an approach similar to ModelAttributeMethodProcessor's FieldAwareConstructorParameter, merging the local parameter annotations with interface-declared annotations. Issue: SPR-11055
1 parent 28f7b26 commit 790d515

File tree

2 files changed

+191
-2
lines changed

2 files changed

+191
-2
lines changed

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818

1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.Method;
21+
import java.util.ArrayList;
22+
import java.util.Arrays;
23+
import java.util.List;
2124

2225
import org.apache.commons.logging.Log;
2326
import org.apache.commons.logging.LogFactory;
@@ -353,6 +356,9 @@ public String toString() {
353356
*/
354357
protected class HandlerMethodParameter extends SynthesizingMethodParameter {
355358

359+
@Nullable
360+
private volatile Annotation[] combinedAnnotations;
361+
356362
public HandlerMethodParameter(int index) {
357363
super(HandlerMethod.this.bridgedMethod, index);
358364
}
@@ -376,6 +382,42 @@ public <T extends Annotation> boolean hasMethodAnnotation(Class<T> annotationTyp
376382
return HandlerMethod.this.hasMethodAnnotation(annotationType);
377383
}
378384

385+
@Override
386+
public Annotation[] getParameterAnnotations() {
387+
Annotation[] anns = this.combinedAnnotations;
388+
if (anns == null) {
389+
anns = super.getParameterAnnotations();
390+
Class<?>[] ifcs = getDeclaringClass().getInterfaces();
391+
for (Class<?> ifc : ifcs) {
392+
try {
393+
Method method = ifc.getMethod(getExecutable().getName(), getExecutable().getParameterTypes());
394+
Annotation[] paramAnns = method.getParameterAnnotations()[getParameterIndex()];
395+
if (paramAnns.length > 0) {
396+
List<Annotation> merged = new ArrayList<>(anns.length + paramAnns.length);
397+
merged.addAll(Arrays.asList(anns));
398+
for (Annotation fieldAnn : paramAnns) {
399+
boolean existingType = false;
400+
for (Annotation ann : anns) {
401+
if (ann.annotationType() == fieldAnn.annotationType()) {
402+
existingType = true;
403+
break;
404+
}
405+
}
406+
if (!existingType) {
407+
merged.add(fieldAnn);
408+
}
409+
}
410+
anns = merged.toArray(new Annotation[0]);
411+
}
412+
}
413+
catch (NoSuchMethodException ex) {
414+
}
415+
}
416+
this.combinedAnnotations = anns;
417+
}
418+
return anns;
419+
}
420+
379421
@Override
380422
public HandlerMethodParameter clone() {
381423
return new HandlerMethodParameter(this);

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java

Lines changed: 149 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -221,6 +221,87 @@ public void handle() throws Exception {
221221
assertEquals(new URI("http://localhost/contextPath/main/path"), model.get("url"));
222222
}
223223

224+
@Test
225+
public void handleInInterface() throws Exception {
226+
Class<?>[] parameterTypes = new Class<?>[] {int.class, String.class, String.class, String.class, Map.class,
227+
Date.class, Map.class, String.class, String.class, TestBean.class, Errors.class, TestBean.class,
228+
Color.class, HttpServletRequest.class, HttpServletResponse.class, TestBean.class, TestBean.class,
229+
User.class, OtherUser.class, Model.class, UriComponentsBuilder.class};
230+
231+
String datePattern = "yyyy.MM.dd";
232+
String formattedDate = "2011.03.16";
233+
Date date = new GregorianCalendar(2011, Calendar.MARCH, 16).getTime();
234+
TestBean sessionAttribute = new TestBean();
235+
TestBean requestAttribute = new TestBean();
236+
237+
request.addHeader("Content-Type", "text/plain; charset=utf-8");
238+
request.addHeader("header", "headerValue");
239+
request.addHeader("anotherHeader", "anotherHeaderValue");
240+
request.addParameter("datePattern", datePattern);
241+
request.addParameter("dateParam", formattedDate);
242+
request.addParameter("paramByConvention", "paramByConventionValue");
243+
request.addParameter("age", "25");
244+
request.setCookies(new Cookie("cookie", "99"));
245+
request.setContent("Hello World".getBytes("UTF-8"));
246+
request.setUserPrincipal(new User());
247+
request.setContextPath("/contextPath");
248+
request.setServletPath("/main");
249+
System.setProperty("systemHeader", "systemHeaderValue");
250+
Map<String, String> uriTemplateVars = new HashMap<>();
251+
uriTemplateVars.put("pathvar", "pathvarValue");
252+
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars);
253+
request.getSession().setAttribute("sessionAttribute", sessionAttribute);
254+
request.setAttribute("requestAttribute", requestAttribute);
255+
256+
HandlerMethod handlerMethod = handlerMethod("handleInInterface", parameterTypes);
257+
ModelAndView mav = handlerAdapter.handle(request, response, handlerMethod);
258+
ModelMap model = mav.getModelMap();
259+
260+
assertEquals("viewName", mav.getViewName());
261+
assertEquals(99, model.get("cookie"));
262+
assertEquals("pathvarValue", model.get("pathvar"));
263+
assertEquals("headerValue", model.get("header"));
264+
assertEquals(date, model.get("dateParam"));
265+
266+
Map<?, ?> map = (Map<?, ?>) model.get("headerMap");
267+
assertEquals("headerValue", map.get("header"));
268+
assertEquals("anotherHeaderValue", map.get("anotherHeader"));
269+
assertEquals("systemHeaderValue", model.get("systemHeader"));
270+
271+
map = (Map<?, ?>) model.get("paramMap");
272+
assertEquals(formattedDate, map.get("dateParam"));
273+
assertEquals("paramByConventionValue", map.get("paramByConvention"));
274+
275+
assertEquals("/contextPath", model.get("value"));
276+
277+
TestBean modelAttr = (TestBean) model.get("modelAttr");
278+
assertEquals(25, modelAttr.getAge());
279+
assertEquals("Set by model method [modelAttr]", modelAttr.getName());
280+
assertSame(modelAttr, request.getSession().getAttribute("modelAttr"));
281+
282+
BindingResult bindingResult = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + "modelAttr");
283+
assertSame(modelAttr, bindingResult.getTarget());
284+
assertEquals(1, bindingResult.getErrorCount());
285+
286+
String conventionAttrName = "testBean";
287+
TestBean modelAttrByConvention = (TestBean) model.get(conventionAttrName);
288+
assertEquals(25, modelAttrByConvention.getAge());
289+
assertEquals("Set by model method [modelAttrByConvention]", modelAttrByConvention.getName());
290+
assertSame(modelAttrByConvention, request.getSession().getAttribute(conventionAttrName));
291+
292+
bindingResult = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + conventionAttrName);
293+
assertSame(modelAttrByConvention, bindingResult.getTarget());
294+
295+
assertTrue(model.get("customArg") instanceof Color);
296+
assertEquals(User.class, model.get("user").getClass());
297+
assertEquals(OtherUser.class, model.get("otherUser").getClass());
298+
299+
assertSame(sessionAttribute, model.get("sessionAttribute"));
300+
assertSame(requestAttribute, model.get("requestAttribute"));
301+
302+
assertEquals(new URI("http://localhost/contextPath/main/path"), model.get("url"));
303+
}
304+
224305
@Test
225306
public void handleRequestBody() throws Exception {
226307
Class<?>[] parameterTypes = new Class<?>[] {byte[].class};
@@ -327,9 +408,36 @@ private HandlerMethod handlerMethod(String methodName, Class<?>... paramTypes) t
327408
}
328409

329410

411+
private interface HandlerIfc {
412+
413+
String handleInInterface(
414+
@CookieValue("cookie") int cookie,
415+
@PathVariable("pathvar") String pathvar,
416+
@RequestHeader("header") String header,
417+
@RequestHeader(defaultValue = "#{systemProperties.systemHeader}") String systemHeader,
418+
@RequestHeader Map<String, Object> headerMap,
419+
@RequestParam("dateParam") Date dateParam,
420+
@RequestParam Map<String, Object> paramMap,
421+
String paramByConvention,
422+
@Value("#{request.contextPath}") String value,
423+
@ModelAttribute("modelAttr") @Valid TestBean modelAttr,
424+
Errors errors,
425+
TestBean modelAttrByConvention,
426+
Color customArg,
427+
HttpServletRequest request,
428+
HttpServletResponse response,
429+
@SessionAttribute TestBean sessionAttribute,
430+
@RequestAttribute TestBean requestAttribute,
431+
User user,
432+
@ModelAttribute OtherUser otherUser,
433+
Model model,
434+
UriComponentsBuilder builder);
435+
}
436+
437+
330438
@SuppressWarnings("unused")
331439
@SessionAttributes(types = TestBean.class)
332-
private static class Handler {
440+
private static class Handler implements HandlerIfc {
333441

334442
@InitBinder("dateParam")
335443
public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String datePattern) {
@@ -388,6 +496,45 @@ public String handle(
388496
return "viewName";
389497
}
390498

499+
@Override
500+
public String handleInInterface(
501+
int cookie,
502+
String pathvar,
503+
String header,
504+
String systemHeader,
505+
Map<String, Object> headerMap,
506+
Date dateParam,
507+
Map<String, Object> paramMap,
508+
String paramByConvention,
509+
String value,
510+
TestBean modelAttr,
511+
Errors errors,
512+
TestBean modelAttrByConvention,
513+
Color customArg,
514+
HttpServletRequest request,
515+
HttpServletResponse response,
516+
TestBean sessionAttribute,
517+
TestBean requestAttribute,
518+
User user,
519+
OtherUser otherUser,
520+
Model model,
521+
UriComponentsBuilder builder) {
522+
523+
model.addAttribute("cookie", cookie).addAttribute("pathvar", pathvar).addAttribute("header", header)
524+
.addAttribute("systemHeader", systemHeader).addAttribute("headerMap", headerMap)
525+
.addAttribute("dateParam", dateParam).addAttribute("paramMap", paramMap)
526+
.addAttribute("paramByConvention", paramByConvention).addAttribute("value", value)
527+
.addAttribute("customArg", customArg).addAttribute(user)
528+
.addAttribute("sessionAttribute", sessionAttribute)
529+
.addAttribute("requestAttribute", requestAttribute)
530+
.addAttribute("url", builder.path("/path").build().toUri());
531+
532+
assertNotNull(request);
533+
assertNotNull(response);
534+
535+
return "viewName";
536+
}
537+
391538
@ResponseStatus(HttpStatus.ACCEPTED)
392539
@ResponseBody
393540
public String handleRequestBody(@RequestBody byte[] bytes) throws Exception {

0 commit comments

Comments
 (0)