From 6fa09e1783638427c57c9c287de85af5ae61f91f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 4 Jul 2023 20:42:30 +0200 Subject: [PATCH] Extract AnnotatedMethod base class for consistent annotation exposure As a consequence, the spring-messaging HandlerMethod detects interface parameter annotations as well, and the same is available for other HandlerMethod variants. Closes gh-30801 --- .../core/annotation/AnnotatedMethod.java | 349 ++++++++++++++++++ .../messaging/handler/HandlerMethod.java | 235 +----------- .../invocation/InvocableHandlerMethod.java | 2 +- ...mpAnnotationMethodMessageHandlerTests.java | 59 ++- .../web/method/HandlerMethod.java | 324 ++-------------- .../ServletInvocableHandlerMethod.java | 2 +- 6 files changed, 440 insertions(+), 531 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java new file mode 100644 index 0000000000..fba4331b8a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java @@ -0,0 +1,349 @@ +/* + * Copyright 2002-2023 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 + * + * https://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.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * A convenient wrapper for a {@link Method} handle, providing deep annotation + * introspection on methods and method parameters, including the exposure of + * interface-declared parameter annotations from the concrete target method. + * + * @author Juergen Hoeller + * @since 6.1 + * @see #getMethodAnnotation(Class) + * @see #getMethodParameters() + * @see AnnotatedElementUtils + * @see SynthesizingMethodParameter + */ +public class AnnotatedMethod { + + private final Method method; + + private final Method bridgedMethod; + + private final MethodParameter[] parameters; + + @Nullable + private volatile List interfaceParameterAnnotations; + + + /** + * Create an instance that wraps the given {@link Method}. + * @param method the Method handle to wrap + */ + public AnnotatedMethod(Method method) { + Assert.notNull(method, "Method is required"); + this.method = method; + this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); + ReflectionUtils.makeAccessible(this.bridgedMethod); + this.parameters = initMethodParameters(); + } + + /** + * Copy constructor for use in subclasses. + */ + protected AnnotatedMethod(AnnotatedMethod annotatedMethod) { + Assert.notNull(annotatedMethod, "AnnotatedMethod is required"); + this.method = annotatedMethod.method; + this.bridgedMethod = annotatedMethod.bridgedMethod; + this.parameters = annotatedMethod.parameters; + this.interfaceParameterAnnotations = annotatedMethod.interfaceParameterAnnotations; + } + + + /** + * Return the method for this handler method. + */ + public final Method getMethod() { + return this.method; + } + + /** + * If the bean method is a bridge method, this method returns the bridged + * (user-defined) method. Otherwise, it returns the same method as {@link #getMethod()}. + */ + protected final Method getBridgedMethod() { + return this.bridgedMethod; + } + + /** + * Expose the containing class for method parameters. + * @see MethodParameter#getContainingClass() + */ + protected Class getContainingClass() { + return this.method.getDeclaringClass(); + } + + /** + * Return the method parameters for this handler method. + */ + public final MethodParameter[] getMethodParameters() { + return this.parameters; + } + + private MethodParameter[] initMethodParameters() { + int count = this.bridgedMethod.getParameterCount(); + MethodParameter[] result = new MethodParameter[count]; + for (int i = 0; i < count; i++) { + result[i] = new AnnotatedMethodParameter(i); + } + return result; + } + + /** + * Return a MethodParameter for the declared return type. + */ + public MethodParameter getReturnType() { + return new AnnotatedMethodParameter(-1); + } + + /** + * Return a MethodParameter for the actual return value type. + */ + public MethodParameter getReturnValueType(@Nullable Object returnValue) { + return new ReturnValueMethodParameter(returnValue); + } + + /** + * Return {@code true} if the method return type is void, {@code false} otherwise. + */ + public boolean isVoid() { + return Void.TYPE.equals(getReturnType().getParameterType()); + } + + /** + * Return a single annotation on the underlying method traversing its super methods + * if no annotation can be found on the given method itself. + *

Also supports merged composed annotations with attribute + * overrides as of Spring Framework 4.2.2. + * @param annotationType the type of annotation to introspect the method for + * @return the annotation, or {@code null} if none found + * @see AnnotatedElementUtils#findMergedAnnotation + */ + @Nullable + public A getMethodAnnotation(Class annotationType) { + return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType); + } + + /** + * Return whether the parameter is declared with the given annotation type. + * @param annotationType the annotation type to look for + * @since 4.3 + * @see AnnotatedElementUtils#hasAnnotation + */ + public boolean hasMethodAnnotation(Class annotationType) { + return AnnotatedElementUtils.hasAnnotation(this.method, annotationType); + } + + private List getInterfaceParameterAnnotations() { + List parameterAnnotations = this.interfaceParameterAnnotations; + if (parameterAnnotations == null) { + parameterAnnotations = new ArrayList<>(); + for (Class ifc : ClassUtils.getAllInterfacesForClassAsSet(this.method.getDeclaringClass())) { + for (Method candidate : ifc.getMethods()) { + if (isOverrideFor(candidate)) { + parameterAnnotations.add(candidate.getParameterAnnotations()); + } + } + } + this.interfaceParameterAnnotations = parameterAnnotations; + } + return parameterAnnotations; + } + + private boolean isOverrideFor(Method candidate) { + if (!candidate.getName().equals(this.method.getName()) || + candidate.getParameterCount() != this.method.getParameterCount()) { + return false; + } + Class[] paramTypes = this.method.getParameterTypes(); + if (Arrays.equals(candidate.getParameterTypes(), paramTypes)) { + return true; + } + for (int i = 0; i < paramTypes.length; i++) { + if (paramTypes[i] != + ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).resolve()) { + return false; + } + } + return true; + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other != null && getClass() == other.getClass() && + this.method.equals(((AnnotatedMethod) other).method))); + } + + @Override + public int hashCode() { + return (this.method.hashCode()); + } + + @Override + public String toString() { + return this.method.toGenericString(); + } + + + // Support methods for use in subclass variants + + @Nullable + protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) { + if (!ObjectUtils.isEmpty(providedArgs)) { + for (Object providedArg : providedArgs) { + if (parameter.getParameterType().isInstance(providedArg)) { + return providedArg; + } + } + } + return null; + } + + protected static String formatArgumentError(MethodParameter param, String message) { + return "Could not resolve parameter [" + param.getParameterIndex() + "] in " + + param.getExecutable().toGenericString() + (StringUtils.hasText(message) ? ": " + message : ""); + } + + + /** + * A MethodParameter with AnnotatedMethod-specific behavior. + */ + protected class AnnotatedMethodParameter extends SynthesizingMethodParameter { + + @Nullable + private volatile Annotation[] combinedAnnotations; + + public AnnotatedMethodParameter(int index) { + super(AnnotatedMethod.this.getBridgedMethod(), index); + } + + protected AnnotatedMethodParameter(AnnotatedMethodParameter original) { + super(original); + this.combinedAnnotations = original.combinedAnnotations; + } + + @Override + @NonNull + public Method getMethod() { + return AnnotatedMethod.this.getBridgedMethod(); + } + + @Override + public Class getContainingClass() { + return AnnotatedMethod.this.getContainingClass(); + } + + @Override + public T getMethodAnnotation(Class annotationType) { + return AnnotatedMethod.this.getMethodAnnotation(annotationType); + } + + @Override + public boolean hasMethodAnnotation(Class annotationType) { + return AnnotatedMethod.this.hasMethodAnnotation(annotationType); + } + + @Override + public Annotation[] getParameterAnnotations() { + Annotation[] anns = this.combinedAnnotations; + if (anns == null) { + anns = super.getParameterAnnotations(); + int index = getParameterIndex(); + if (index >= 0) { + for (Annotation[][] ifcAnns : getInterfaceParameterAnnotations()) { + if (index < ifcAnns.length) { + Annotation[] paramAnns = ifcAnns[index]; + if (paramAnns.length > 0) { + List merged = new ArrayList<>(anns.length + paramAnns.length); + merged.addAll(Arrays.asList(anns)); + for (Annotation paramAnn : paramAnns) { + boolean existingType = false; + for (Annotation ann : anns) { + if (ann.annotationType() == paramAnn.annotationType()) { + existingType = true; + break; + } + } + if (!existingType) { + merged.add(adaptAnnotation(paramAnn)); + } + } + anns = merged.toArray(new Annotation[0]); + } + } + } + } + this.combinedAnnotations = anns; + } + return anns; + } + + @Override + public AnnotatedMethodParameter clone() { + return new AnnotatedMethodParameter(this); + } + } + + + /** + * A MethodParameter for an AnnotatedMethod return type based on an actual return value. + */ + private class ReturnValueMethodParameter extends AnnotatedMethodParameter { + + @Nullable + private final Class returnValueType; + + public ReturnValueMethodParameter(@Nullable Object returnValue) { + super(-1); + this.returnValueType = (returnValue != null ? returnValue.getClass() : null); + } + + protected ReturnValueMethodParameter(ReturnValueMethodParameter original) { + super(original); + this.returnValueType = original.returnValueType; + } + + @Override + public Class getParameterType() { + return (this.returnValueType != null ? this.returnValueType : super.getParameterType()); + } + + @Override + public ReturnValueMethodParameter clone() { + return new ReturnValueMethodParameter(this); + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java index e1efc7aee1..375d05820f 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -16,7 +16,6 @@ package org.springframework.messaging.handler; -import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -25,16 +24,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanFactory; -import org.springframework.core.BridgeMethodResolver; -import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.annotation.AnnotatedMethod; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; /** * Encapsulates information about a handler method consisting of a @@ -52,7 +45,7 @@ import org.springframework.util.StringUtils; * @author Juergen Hoeller * @since 4.0 */ -public class HandlerMethod { +public class HandlerMethod extends AnnotatedMethod { /** Public for wrapping with fallback logger. */ public static final Log defaultLogger = LogFactory.getLog(HandlerMethod.class); @@ -65,12 +58,6 @@ public class HandlerMethod { private final Class beanType; - private final Method method; - - private final Method bridgedMethod; - - private final MethodParameter[] parameters; - @Nullable private HandlerMethod resolvedFromHandlerMethod; @@ -81,15 +68,10 @@ public class HandlerMethod { * Create an instance from a bean instance and a method. */ public HandlerMethod(Object bean, Method method) { - Assert.notNull(bean, "Bean is required"); - Assert.notNull(method, "Method is required"); + super(method); this.bean = bean; this.beanFactory = null; this.beanType = ClassUtils.getUserClass(bean); - this.method = method; - this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); - ReflectionUtils.makeAccessible(this.bridgedMethod); - this.parameters = initMethodParameters(); } /** @@ -97,15 +79,10 @@ public class HandlerMethod { * @throws NoSuchMethodException when the method cannot be found */ public HandlerMethod(Object bean, String methodName, Class... parameterTypes) throws NoSuchMethodException { - Assert.notNull(bean, "Bean is required"); - Assert.notNull(methodName, "Method name is required"); + super(bean.getClass().getMethod(methodName, parameterTypes)); this.bean = bean; this.beanFactory = null; this.beanType = ClassUtils.getUserClass(bean); - this.method = bean.getClass().getMethod(methodName, parameterTypes); - this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(this.method); - ReflectionUtils.makeAccessible(this.bridgedMethod); - this.parameters = initMethodParameters(); } /** @@ -114,9 +91,7 @@ public class HandlerMethod { * re-create the {@code HandlerMethod} with an initialized bean. */ public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) { - Assert.hasText(beanName, "Bean name is required"); - Assert.notNull(beanFactory, "BeanFactory is required"); - Assert.notNull(method, "Method is required"); + super(method); this.bean = beanName; this.beanFactory = beanFactory; Class beanType = beanFactory.getType(beanName); @@ -124,23 +99,16 @@ public class HandlerMethod { throw new IllegalStateException("Cannot resolve bean type for bean with name '" + beanName + "'"); } this.beanType = ClassUtils.getUserClass(beanType); - this.method = method; - this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); - ReflectionUtils.makeAccessible(this.bridgedMethod); - this.parameters = initMethodParameters(); } /** * Copy constructor for use in subclasses. */ protected HandlerMethod(HandlerMethod handlerMethod) { - Assert.notNull(handlerMethod, "HandlerMethod is required"); + super(handlerMethod); this.bean = handlerMethod.bean; this.beanFactory = handlerMethod.beanFactory; this.beanType = handlerMethod.beanType; - this.method = handlerMethod.method; - this.bridgedMethod = handlerMethod.bridgedMethod; - this.parameters = handlerMethod.parameters; this.resolvedFromHandlerMethod = handlerMethod.resolvedFromHandlerMethod; } @@ -148,28 +116,14 @@ public class HandlerMethod { * Re-create HandlerMethod with the resolved handler. */ private HandlerMethod(HandlerMethod handlerMethod, Object handler) { - Assert.notNull(handlerMethod, "HandlerMethod is required"); - Assert.notNull(handler, "Handler object is required"); + super(handlerMethod); this.bean = handler; this.beanFactory = handlerMethod.beanFactory; this.beanType = handlerMethod.beanType; - this.method = handlerMethod.method; - this.bridgedMethod = handlerMethod.bridgedMethod; - this.parameters = handlerMethod.parameters; this.resolvedFromHandlerMethod = handlerMethod; } - private MethodParameter[] initMethodParameters() { - int count = this.bridgedMethod.getParameterCount(); - MethodParameter[] result = new MethodParameter[count]; - for (int i = 0; i < count; i++) { - result[i] = new HandlerMethodParameter(i); - } - return result; - } - - /** * Set an alternative logger to use than the one based on the class name. * @param logger the logger to use @@ -194,13 +148,6 @@ public class HandlerMethod { return this.bean; } - /** - * Return the method for this handler method. - */ - public Method getMethod() { - return this.method; - } - /** * This method returns the type of the handler for this handler method. *

Note that if the bean type is a CGLIB-generated class, the original @@ -210,64 +157,9 @@ public class HandlerMethod { return this.beanType; } - /** - * If the bean method is a bridge method, this method returns the bridged - * (user-defined) method. Otherwise it returns the same method as {@link #getMethod()}. - */ - protected Method getBridgedMethod() { - return this.bridgedMethod; - } - - /** - * Return the method parameters for this handler method. - */ - public MethodParameter[] getMethodParameters() { - return this.parameters; - } - - /** - * Return the HandlerMethod return type. - */ - public MethodParameter getReturnType() { - return new HandlerMethodParameter(-1); - } - - /** - * Return the actual return value type. - */ - public MethodParameter getReturnValueType(@Nullable Object returnValue) { - return new ReturnValueMethodParameter(returnValue); - } - - /** - * Return {@code true} if the method return type is void, {@code false} otherwise. - */ - public boolean isVoid() { - return Void.TYPE.equals(getReturnType().getParameterType()); - } - - /** - * Return a single annotation on the underlying method traversing its super methods - * if no annotation can be found on the given method itself. - *

Also supports merged composed annotations with attribute - * overrides as of Spring Framework 4.3. - * @param annotationType the type of annotation to introspect the method for - * @return the annotation, or {@code null} if none found - * @see AnnotatedElementUtils#findMergedAnnotation - */ - @Nullable - public A getMethodAnnotation(Class annotationType) { - return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType); - } - - /** - * Return whether the parameter is declared with the given annotation type. - * @param annotationType the annotation type to look for - * @since 4.3 - * @see AnnotatedElementUtils#hasAnnotation - */ - public boolean hasMethodAnnotation(Class annotationType) { - return AnnotatedElementUtils.hasAnnotation(this.method, annotationType); + @Override + protected Class getContainingClass() { + return this.beanType; } /** @@ -297,51 +189,23 @@ public class HandlerMethod { * Return a short representation of this handler method for log message purposes. */ public String getShortLogMessage() { - int args = this.method.getParameterCount(); - return getBeanType().getSimpleName() + "#" + this.method.getName() + "[" + args + " args]"; + return getBeanType().getName() + "#" + getMethod().getName() + + "[" + getMethod().getParameterCount() + " args]"; } @Override public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (!(other instanceof HandlerMethod otherMethod)) { - return false; - } - return (this.bean.equals(otherMethod.bean) && this.method.equals(otherMethod.method)); + return (this == other || (super.equals(other) && this.bean.equals(((HandlerMethod) other).bean))); } @Override public int hashCode() { - return (this.bean.hashCode() * 31 + this.method.hashCode()); - } - - @Override - public String toString() { - return this.method.toGenericString(); + return (this.bean.hashCode() * 31 + super.hashCode()); } - // Support methods for use in "InvocableHandlerMethod" sub-class variants.. - - @Nullable - protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) { - if (!ObjectUtils.isEmpty(providedArgs)) { - for (Object providedArg : providedArgs) { - if (parameter.getParameterType().isInstance(providedArg)) { - return providedArg; - } - } - } - return null; - } - - protected static String formatArgumentError(MethodParameter param, String message) { - return "Could not resolve parameter [" + param.getParameterIndex() + "] in " + - param.getExecutable().toGenericString() + (StringUtils.hasText(message) ? ": " + message : ""); - } + // Support methods for use in subclass variants /** * Assert that the target bean class is an instance of the class where the given @@ -363,82 +227,15 @@ public class HandlerMethod { } protected String formatInvokeError(String text, Object[] args) { - String formattedArgs = IntStream.range(0, args.length) .mapToObj(i -> (args[i] != null ? "[" + i + "] [type=" + args[i].getClass().getName() + "] [value=" + args[i] + "]" : "[" + i + "] [null]")) .collect(Collectors.joining(",\n", " ", " ")); - return text + "\n" + "Endpoint [" + getBeanType().getName() + "]\n" + "Method [" + getBridgedMethod().toGenericString() + "] " + "with argument values:\n" + formattedArgs; } - - /** - * A MethodParameter with HandlerMethod-specific behavior. - */ - protected class HandlerMethodParameter extends SynthesizingMethodParameter { - - public HandlerMethodParameter(int index) { - super(HandlerMethod.this.bridgedMethod, index); - } - - protected HandlerMethodParameter(HandlerMethodParameter original) { - super(original); - } - - @Override - public Class getContainingClass() { - return HandlerMethod.this.getBeanType(); - } - - @Override - public T getMethodAnnotation(Class annotationType) { - return HandlerMethod.this.getMethodAnnotation(annotationType); - } - - @Override - public boolean hasMethodAnnotation(Class annotationType) { - return HandlerMethod.this.hasMethodAnnotation(annotationType); - } - - @Override - public HandlerMethodParameter clone() { - return new HandlerMethodParameter(this); - } - } - - - /** - * A MethodParameter for a HandlerMethod return type based on an actual return value. - */ - private class ReturnValueMethodParameter extends HandlerMethodParameter { - - @Nullable - private final Class returnValueType; - - public ReturnValueMethodParameter(@Nullable Object returnValue) { - super(-1); - this.returnValueType = (returnValue != null ? returnValue.getClass() : null); - } - - protected ReturnValueMethodParameter(ReturnValueMethodParameter original) { - super(original); - this.returnValueType = original.returnValueType; - } - - @Override - public Class getParameterType() { - return (this.returnValueType != null ? this.returnValueType : super.getParameterType()); - } - - @Override - public ReturnValueMethodParameter clone() { - return new ReturnValueMethodParameter(this); - } - } - } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java index 76a7aa4e72..5a3720dc4e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java @@ -197,7 +197,7 @@ public class InvocableHandlerMethod extends HandlerMethod { } - private class AsyncResultMethodParameter extends HandlerMethodParameter { + private class AsyncResultMethodParameter extends AnnotatedMethodParameter { @Nullable private final Object returnValue; diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java index e6e1273190..303aa7e82e 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java @@ -240,6 +240,19 @@ public class SimpAnnotationMethodMessageHandlerTests { assertThat(this.testController.method).isEqualTo("scope"); } + @Test + public void interfaceBasedController() { + InterfaceBasedController controller = new InterfaceBasedController(); + + Message message = createMessage("/pre/binding/id/12"); + this.messageHandler.registerHandler(controller); + this.messageHandler.handleMessage(message); + + assertThat(controller.method).isEqualTo("simpleBinding"); + assertThat(controller.arguments.get("id")).as("should be bound to type long").isInstanceOf(Long.class); + assertThat(controller.arguments.get("id")).isEqualTo(12L); + } + @Test public void dotPathSeparator() { DotPathSeparatorController controller = new DotPathSeparatorController(); @@ -425,9 +438,9 @@ public class SimpAnnotationMethodMessageHandlerTests { @MessageMapping("/pre") private static class TestController { - private String method; + String method; - private Map arguments = new LinkedHashMap<>(); + Map arguments = new LinkedHashMap<>(); @MessageMapping("/headers") public void headers(@Header String foo, @Headers Map headers) { @@ -519,11 +532,34 @@ public class SimpAnnotationMethodMessageHandlerTests { } + private interface ControllerInterface { + + void simpleBinding(@DestinationVariable("id") Long id); + } + + + @Controller + @MessageMapping("pre") + private static class InterfaceBasedController implements ControllerInterface { + + String method; + + Map arguments = new LinkedHashMap<>(); + + @MessageMapping("/binding/id/{id}") + public void simpleBinding(Long id) { + this.method = "simpleBinding"; + this.arguments.put("id", id); + } + + } + + @Controller @MessageMapping("pre") private static class DotPathSeparatorController { - private String method; + String method; @MessageMapping("foo") public void handleFoo() { @@ -537,9 +573,9 @@ public class SimpAnnotationMethodMessageHandlerTests { @SuppressWarnings("deprecation") private static class ListenableFutureController { - private org.springframework.util.concurrent.ListenableFutureTask future; + org.springframework.util.concurrent.ListenableFutureTask future; - private boolean exceptionCaught = false; + boolean exceptionCaught = false; @MessageMapping("success") public org.springframework.util.concurrent.ListenableFutureTask handleListenableFuture() { @@ -565,9 +601,9 @@ public class SimpAnnotationMethodMessageHandlerTests { @Controller private static class CompletableFutureController { - private CompletableFuture future; + CompletableFuture future; - private boolean exceptionCaught = false; + boolean exceptionCaught = false; @MessageMapping("completable-future") public CompletableFuture handleCompletableFuture() { @@ -581,14 +617,15 @@ public class SimpAnnotationMethodMessageHandlerTests { } } + @Controller private static class ReactiveController { - private Sinks.One sinkOne; + Sinks.One sinkOne; - private Sinks.Many sinkMany; + Sinks.Many sinkMany; - private boolean exceptionCaught = false; + boolean exceptionCaught = false; @MessageMapping("mono") public Mono handleMono() { @@ -611,7 +648,7 @@ public class SimpAnnotationMethodMessageHandlerTests { private static class StringTestValidator implements Validator { - private final String invalidValue; + final String invalidValue; public StringTestValidator(String invalidValue) { this.invalidValue = invalidValue; 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 885b0afb3b..5c737d13f6 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,9 +18,6 @@ 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 java.util.StringJoiner; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -32,22 +29,17 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.core.BridgeMethodResolver; import org.springframework.core.MethodParameter; -import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotatedMethod; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotationPredicates; import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.http.HttpStatusCode; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.ResponseStatus; @@ -69,7 +61,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; * @author Sam Brannen * @since 3.1 */ -public class HandlerMethod { +public class HandlerMethod extends AnnotatedMethod { /** Logger that is available to subclasses. */ protected static final Log logger = LogFactory.getLog(HandlerMethod.class); @@ -84,12 +76,6 @@ public class HandlerMethod { private final Class beanType; - private final Method method; - - private final Method bridgedMethod; - - private final MethodParameter[] parameters; - private final boolean validateArguments; private final boolean validateReturnValue; @@ -103,9 +89,6 @@ public class HandlerMethod { @Nullable private HandlerMethod resolvedFromHandlerMethod; - @Nullable - private volatile List interfaceParameterAnnotations; - private final String description; @@ -122,20 +105,15 @@ public class HandlerMethod { * @since 5.3.10 */ protected HandlerMethod(Object bean, Method method, @Nullable MessageSource messageSource) { - Assert.notNull(bean, "Bean is required"); - Assert.notNull(method, "Method is required"); + super(method); this.bean = bean; this.beanFactory = null; this.messageSource = messageSource; this.beanType = ClassUtils.getUserClass(bean); - this.method = method; - this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); - ReflectionUtils.makeAccessible(this.bridgedMethod); - this.parameters = initMethodParameters(); - this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, this.parameters); - this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, this.bridgedMethod); + this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, getMethodParameters()); + this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, getBridgedMethod()); evaluateResponseStatus(); - this.description = initDescription(this.beanType, this.method); + this.description = initDescription(this.beanType, method); } /** @@ -143,20 +121,15 @@ public class HandlerMethod { * @throws NoSuchMethodException when the method cannot be found */ public HandlerMethod(Object bean, String methodName, Class... parameterTypes) throws NoSuchMethodException { - Assert.notNull(bean, "Bean is required"); - Assert.notNull(methodName, "Method name is required"); + super(bean.getClass().getMethod(methodName, parameterTypes)); this.bean = bean; this.beanFactory = null; this.messageSource = null; this.beanType = ClassUtils.getUserClass(bean); - this.method = bean.getClass().getMethod(methodName, parameterTypes); - this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(this.method); - ReflectionUtils.makeAccessible(this.bridgedMethod); - this.parameters = initMethodParameters(); - this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, this.parameters); - this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, this.bridgedMethod); + this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, getMethodParameters()); + this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, getBridgedMethod()); evaluateResponseStatus(); - this.description = initDescription(this.beanType, this.method); + this.description = initDescription(this.beanType, getMethod()); } /** @@ -176,9 +149,9 @@ public class HandlerMethod { String beanName, BeanFactory beanFactory, @Nullable MessageSource messageSource, Method method) { + super(method); Assert.hasText(beanName, "Bean name is required"); Assert.notNull(beanFactory, "BeanFactory is required"); - Assert.notNull(method, "Method is required"); this.bean = beanName; this.beanFactory = beanFactory; this.messageSource = messageSource; @@ -187,28 +160,21 @@ public class HandlerMethod { throw new IllegalStateException("Cannot resolve bean type for bean with name '" + beanName + "'"); } this.beanType = ClassUtils.getUserClass(beanType); - this.method = method; - this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); - ReflectionUtils.makeAccessible(this.bridgedMethod); - this.parameters = initMethodParameters(); - this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, this.parameters); - this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, this.bridgedMethod); + this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, getMethodParameters()); + this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, getBridgedMethod()); evaluateResponseStatus(); - this.description = initDescription(this.beanType, this.method); + this.description = initDescription(this.beanType, method); } /** * Copy constructor for use in subclasses. */ protected HandlerMethod(HandlerMethod handlerMethod) { - Assert.notNull(handlerMethod, "HandlerMethod is required"); + super(handlerMethod); this.bean = handlerMethod.bean; this.beanFactory = handlerMethod.beanFactory; this.messageSource = handlerMethod.messageSource; this.beanType = handlerMethod.beanType; - this.method = handlerMethod.method; - this.bridgedMethod = handlerMethod.bridgedMethod; - this.parameters = handlerMethod.parameters; this.validateArguments = handlerMethod.validateArguments; this.validateReturnValue = handlerMethod.validateReturnValue; this.responseStatus = handlerMethod.responseStatus; @@ -221,15 +187,12 @@ public class HandlerMethod { * Re-create HandlerMethod with the resolved handler. */ private HandlerMethod(HandlerMethod handlerMethod, Object handler) { - Assert.notNull(handlerMethod, "HandlerMethod is required"); + super(handlerMethod); Assert.notNull(handler, "Handler object is required"); this.bean = handler; this.beanFactory = handlerMethod.beanFactory; this.messageSource = handlerMethod.messageSource; this.beanType = handlerMethod.beanType; - this.method = handlerMethod.method; - this.bridgedMethod = handlerMethod.bridgedMethod; - this.parameters = handlerMethod.parameters; this.validateArguments = handlerMethod.validateArguments; this.validateReturnValue = handlerMethod.validateReturnValue; this.responseStatus = handlerMethod.responseStatus; @@ -238,14 +201,6 @@ public class HandlerMethod { this.description = handlerMethod.description; } - private MethodParameter[] initMethodParameters() { - int count = this.bridgedMethod.getParameterCount(); - MethodParameter[] result = new MethodParameter[count]; - for (int i = 0; i < count; i++) { - result[i] = new HandlerMethodParameter(i); - } - return result; - } private void evaluateResponseStatus() { ResponseStatus annotation = getMethodAnnotation(ResponseStatus.class); @@ -268,7 +223,7 @@ public class HandlerMethod { for (Class paramType : method.getParameterTypes()) { joiner.add(paramType.getSimpleName()); } - return beanType.getName() + "#" + method.getName() + joiner.toString(); + return beanType.getName() + "#" + method.getName() + joiner; } @@ -279,13 +234,6 @@ public class HandlerMethod { return this.bean; } - /** - * Return the method for this handler method. - */ - public Method getMethod() { - return this.method; - } - /** * This method returns the type of the handler for this handler method. *

Note that if the bean type is a CGLIB-generated class, the original @@ -295,19 +243,9 @@ public class HandlerMethod { return this.beanType; } - /** - * If the bean method is a bridge method, this method returns the bridged - * (user-defined) method. Otherwise, it returns the same method as {@link #getMethod()}. - */ - protected Method getBridgedMethod() { - return this.bridgedMethod; - } - - /** - * Return the method parameters for this handler method. - */ - public MethodParameter[] getMethodParameters() { - return this.parameters; + @Override + protected Class getContainingClass() { + return this.beanType; } /** @@ -357,51 +295,6 @@ public class HandlerMethod { return this.responseStatusReason; } - /** - * Return the HandlerMethod return type. - */ - public MethodParameter getReturnType() { - return new HandlerMethodParameter(-1); - } - - /** - * Return the actual return value type. - */ - public MethodParameter getReturnValueType(@Nullable Object returnValue) { - return new ReturnValueMethodParameter(returnValue); - } - - /** - * Return {@code true} if the method return type is void, {@code false} otherwise. - */ - public boolean isVoid() { - return Void.TYPE.equals(getReturnType().getParameterType()); - } - - /** - * Return a single annotation on the underlying method traversing its super methods - * if no annotation can be found on the given method itself. - *

Also supports merged composed annotations with attribute - * overrides as of Spring Framework 4.2.2. - * @param annotationType the type of annotation to introspect the method for - * @return the annotation, or {@code null} if none found - * @see AnnotatedElementUtils#findMergedAnnotation - */ - @Nullable - public A getMethodAnnotation(Class annotationType) { - return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType); - } - - /** - * Return whether the parameter is declared with the given annotation type. - * @param annotationType the annotation type to look for - * @since 4.3 - * @see AnnotatedElementUtils#hasAnnotation - */ - public boolean hasMethodAnnotation(Class annotationType) { - return AnnotatedElementUtils.hasAnnotation(this.method, annotationType); - } - /** * Return the HandlerMethod from which this HandlerMethod instance was * resolved via {@link #createWithResolvedBean()}. @@ -429,60 +322,19 @@ public class HandlerMethod { * @since 4.3 */ public String getShortLogMessage() { - return getBeanType().getName() + "#" + this.method.getName() + - "[" + this.method.getParameterCount() + " args]"; - } - - - private List getInterfaceParameterAnnotations() { - List parameterAnnotations = this.interfaceParameterAnnotations; - if (parameterAnnotations == null) { - parameterAnnotations = new ArrayList<>(); - for (Class ifc : ClassUtils.getAllInterfacesForClassAsSet(this.method.getDeclaringClass())) { - for (Method candidate : ifc.getMethods()) { - if (isOverrideFor(candidate)) { - parameterAnnotations.add(candidate.getParameterAnnotations()); - } - } - } - this.interfaceParameterAnnotations = parameterAnnotations; - } - return parameterAnnotations; - } - - private boolean isOverrideFor(Method candidate) { - if (!candidate.getName().equals(this.method.getName()) || - candidate.getParameterCount() != this.method.getParameterCount()) { - return false; - } - Class[] paramTypes = this.method.getParameterTypes(); - if (Arrays.equals(candidate.getParameterTypes(), paramTypes)) { - return true; - } - for (int i = 0; i < paramTypes.length; i++) { - if (paramTypes[i] != - ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).resolve()) { - return false; - } - } - return true; + return getBeanType().getName() + "#" + getMethod().getName() + + "[" + getMethod().getParameterCount() + " args]"; } @Override public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (!(other instanceof HandlerMethod otherMethod)) { - return false; - } - return (this.bean.equals(otherMethod.bean) && this.method.equals(otherMethod.method)); + return (this == other || (super.equals(other) && this.bean.equals(((HandlerMethod) other).bean))); } @Override public int hashCode() { - return (this.bean.hashCode() * 31 + this.method.hashCode()); + return (this.bean.hashCode() * 31 + super.hashCode()); } @Override @@ -491,24 +343,7 @@ public class HandlerMethod { } - // Support methods for use in "InvocableHandlerMethod" sub-class variants.. - - @Nullable - protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) { - if (!ObjectUtils.isEmpty(providedArgs)) { - for (Object providedArg : providedArgs) { - if (parameter.getParameterType().isInstance(providedArg)) { - return providedArg; - } - } - } - return null; - } - - protected static String formatArgumentError(MethodParameter param, String message) { - return "Could not resolve parameter [" + param.getParameterIndex() + "] in " + - param.getExecutable().toGenericString() + (StringUtils.hasText(message) ? ": " + message : ""); - } + // Support methods for use in subclass variants /** * Assert that the target bean class is an instance of the class where the given @@ -542,115 +377,6 @@ public class HandlerMethod { } - /** - * A MethodParameter with HandlerMethod-specific behavior. - */ - protected class HandlerMethodParameter extends SynthesizingMethodParameter { - - @Nullable - private volatile Annotation[] combinedAnnotations; - - public HandlerMethodParameter(int index) { - super(HandlerMethod.this.bridgedMethod, index); - } - - protected HandlerMethodParameter(HandlerMethodParameter original) { - super(original); - } - - @Override - @NonNull - public Method getMethod() { - return HandlerMethod.this.bridgedMethod; - } - - @Override - public Class getContainingClass() { - return HandlerMethod.this.getBeanType(); - } - - @Override - public T getMethodAnnotation(Class annotationType) { - return HandlerMethod.this.getMethodAnnotation(annotationType); - } - - @Override - public boolean hasMethodAnnotation(Class annotationType) { - return HandlerMethod.this.hasMethodAnnotation(annotationType); - } - - @Override - public Annotation[] getParameterAnnotations() { - Annotation[] anns = this.combinedAnnotations; - if (anns == null) { - anns = super.getParameterAnnotations(); - int index = getParameterIndex(); - if (index >= 0) { - for (Annotation[][] ifcAnns : getInterfaceParameterAnnotations()) { - if (index < ifcAnns.length) { - Annotation[] paramAnns = ifcAnns[index]; - if (paramAnns.length > 0) { - List merged = new ArrayList<>(anns.length + paramAnns.length); - merged.addAll(Arrays.asList(anns)); - for (Annotation paramAnn : paramAnns) { - boolean existingType = false; - for (Annotation ann : anns) { - if (ann.annotationType() == paramAnn.annotationType()) { - existingType = true; - break; - } - } - if (!existingType) { - merged.add(adaptAnnotation(paramAnn)); - } - } - anns = merged.toArray(new Annotation[0]); - } - } - } - } - this.combinedAnnotations = anns; - } - return anns; - } - - @Override - public HandlerMethodParameter clone() { - return new HandlerMethodParameter(this); - } - } - - - /** - * A MethodParameter for a HandlerMethod return type based on an actual return value. - */ - private class ReturnValueMethodParameter extends HandlerMethodParameter { - - @Nullable - private final Class returnValueType; - - public ReturnValueMethodParameter(@Nullable Object returnValue) { - super(-1); - this.returnValueType = (returnValue != null ? returnValue.getClass() : null); - } - - protected ReturnValueMethodParameter(ReturnValueMethodParameter original) { - super(original); - this.returnValueType = original.returnValueType; - } - - @Override - public Class getParameterType() { - return (this.returnValueType != null ? this.returnValueType : super.getParameterType()); - } - - @Override - public ReturnValueMethodParameter clone() { - return new ReturnValueMethodParameter(this); - } - } - - /** * Checks for the presence of {@code @Constraint} and {@code @Valid} * annotations on the method and method parameters. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java index a7b93891f8..d1d166880f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java @@ -272,7 +272,7 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { * that's null falling back on the generic type within the declared async * return type, e.g. Foo instead of {@code DeferredResult}. */ - private class ConcurrentResultMethodParameter extends HandlerMethodParameter { + private class ConcurrentResultMethodParameter extends AnnotatedMethodParameter { @Nullable private final Object returnValue;