From 790d515f8c49d9f80f41a2f0ca00535fa930ccc2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 18 Jul 2018 17:13:55 +0200 Subject: [PATCH] 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 --- .../web/method/HandlerMethod.java | 42 +++++ ...MappingHandlerAdapterIntegrationTests.java | 151 +++++++++++++++++- 2 files changed, 191 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java index 65c3b7ef57..f4e911739c 100644 --- a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java @@ -18,6 +18,9 @@ package org.springframework.web.method; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -353,6 +356,9 @@ public class HandlerMethod { */ protected class HandlerMethodParameter extends SynthesizingMethodParameter { + @Nullable + private volatile Annotation[] combinedAnnotations; + public HandlerMethodParameter(int index) { super(HandlerMethod.this.bridgedMethod, index); } @@ -376,6 +382,42 @@ public class HandlerMethod { return HandlerMethod.this.hasMethodAnnotation(annotationType); } + @Override + public Annotation[] getParameterAnnotations() { + Annotation[] anns = this.combinedAnnotations; + if (anns == null) { + anns = super.getParameterAnnotations(); + Class[] ifcs = getDeclaringClass().getInterfaces(); + for (Class ifc : ifcs) { + try { + Method method = ifc.getMethod(getExecutable().getName(), getExecutable().getParameterTypes()); + Annotation[] paramAnns = method.getParameterAnnotations()[getParameterIndex()]; + if (paramAnns.length > 0) { + List merged = new ArrayList<>(anns.length + paramAnns.length); + merged.addAll(Arrays.asList(anns)); + for (Annotation fieldAnn : paramAnns) { + boolean existingType = false; + for (Annotation ann : anns) { + if (ann.annotationType() == fieldAnn.annotationType()) { + existingType = true; + break; + } + } + if (!existingType) { + merged.add(fieldAnn); + } + } + anns = merged.toArray(new Annotation[0]); + } + } + catch (NoSuchMethodException ex) { + } + } + this.combinedAnnotations = anns; + } + return anns; + } + @Override public HandlerMethodParameter clone() { return new HandlerMethodParameter(this); 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 05503093d9..3a278e231f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 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. @@ -221,6 +221,87 @@ public class RequestMappingHandlerAdapterIntegrationTests { assertEquals(new URI("http://localhost/contextPath/main/path"), model.get("url")); } + @Test + public void handleInInterface() 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, 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"); + request.addHeader("anotherHeader", "anotherHeaderValue"); + request.addParameter("datePattern", datePattern); + request.addParameter("dateParam", formattedDate); + request.addParameter("paramByConvention", "paramByConventionValue"); + request.addParameter("age", "25"); + request.setCookies(new Cookie("cookie", "99")); + request.setContent("Hello World".getBytes("UTF-8")); + request.setUserPrincipal(new User()); + request.setContextPath("/contextPath"); + request.setServletPath("/main"); + System.setProperty("systemHeader", "systemHeaderValue"); + Map uriTemplateVars = new HashMap<>(); + uriTemplateVars.put("pathvar", "pathvarValue"); + request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); + request.getSession().setAttribute("sessionAttribute", sessionAttribute); + request.setAttribute("requestAttribute", requestAttribute); + + HandlerMethod handlerMethod = handlerMethod("handleInInterface", parameterTypes); + ModelAndView mav = handlerAdapter.handle(request, response, handlerMethod); + ModelMap model = mav.getModelMap(); + + assertEquals("viewName", mav.getViewName()); + assertEquals(99, model.get("cookie")); + assertEquals("pathvarValue", model.get("pathvar")); + assertEquals("headerValue", model.get("header")); + assertEquals(date, model.get("dateParam")); + + Map map = (Map) model.get("headerMap"); + assertEquals("headerValue", map.get("header")); + assertEquals("anotherHeaderValue", map.get("anotherHeader")); + assertEquals("systemHeaderValue", model.get("systemHeader")); + + map = (Map) model.get("paramMap"); + assertEquals(formattedDate, map.get("dateParam")); + assertEquals("paramByConventionValue", map.get("paramByConvention")); + + assertEquals("/contextPath", model.get("value")); + + TestBean modelAttr = (TestBean) model.get("modelAttr"); + assertEquals(25, modelAttr.getAge()); + assertEquals("Set by model method [modelAttr]", modelAttr.getName()); + assertSame(modelAttr, request.getSession().getAttribute("modelAttr")); + + BindingResult bindingResult = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + "modelAttr"); + assertSame(modelAttr, bindingResult.getTarget()); + assertEquals(1, bindingResult.getErrorCount()); + + String conventionAttrName = "testBean"; + TestBean modelAttrByConvention = (TestBean) model.get(conventionAttrName); + assertEquals(25, modelAttrByConvention.getAge()); + assertEquals("Set by model method [modelAttrByConvention]", modelAttrByConvention.getName()); + assertSame(modelAttrByConvention, request.getSession().getAttribute(conventionAttrName)); + + bindingResult = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + conventionAttrName); + assertSame(modelAttrByConvention, bindingResult.getTarget()); + + assertTrue(model.get("customArg") instanceof Color); + assertEquals(User.class, model.get("user").getClass()); + 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")); + } + @Test public void handleRequestBody() throws Exception { Class[] parameterTypes = new Class[] {byte[].class}; @@ -327,9 +408,36 @@ public class RequestMappingHandlerAdapterIntegrationTests { } + private interface HandlerIfc { + + String handleInInterface( + @CookieValue("cookie") int cookie, + @PathVariable("pathvar") String pathvar, + @RequestHeader("header") String header, + @RequestHeader(defaultValue = "#{systemProperties.systemHeader}") String systemHeader, + @RequestHeader Map headerMap, + @RequestParam("dateParam") Date dateParam, + @RequestParam Map paramMap, + String paramByConvention, + @Value("#{request.contextPath}") String value, + @ModelAttribute("modelAttr") @Valid TestBean modelAttr, + Errors errors, + TestBean modelAttrByConvention, + Color customArg, + HttpServletRequest request, + HttpServletResponse response, + @SessionAttribute TestBean sessionAttribute, + @RequestAttribute TestBean requestAttribute, + User user, + @ModelAttribute OtherUser otherUser, + Model model, + UriComponentsBuilder builder); + } + + @SuppressWarnings("unused") @SessionAttributes(types = TestBean.class) - private static class Handler { + private static class Handler implements HandlerIfc { @InitBinder("dateParam") public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String datePattern) { @@ -388,6 +496,45 @@ public class RequestMappingHandlerAdapterIntegrationTests { return "viewName"; } + @Override + public String handleInInterface( + int cookie, + String pathvar, + String header, + String systemHeader, + Map headerMap, + Date dateParam, + Map paramMap, + String paramByConvention, + String value, + TestBean modelAttr, + Errors errors, + TestBean modelAttrByConvention, + Color customArg, + HttpServletRequest request, + HttpServletResponse response, + TestBean sessionAttribute, + TestBean requestAttribute, + User user, + OtherUser otherUser, + Model model, + UriComponentsBuilder builder) { + + model.addAttribute("cookie", cookie).addAttribute("pathvar", pathvar).addAttribute("header", header) + .addAttribute("systemHeader", systemHeader).addAttribute("headerMap", headerMap) + .addAttribute("dateParam", dateParam).addAttribute("paramMap", paramMap) + .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); + assertNotNull(response); + + return "viewName"; + } + @ResponseStatus(HttpStatus.ACCEPTED) @ResponseBody public String handleRequestBody(@RequestBody byte[] bytes) throws Exception {