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
This commit is contained in:
Juergen Hoeller
2023-07-04 20:42:30 +02:00
parent ae23f5a594
commit 6fa09e1783
6 changed files with 440 additions and 531 deletions

View File

@@ -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.
* <p>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.
* <p>Also supports <em>merged</em> 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 extends Annotation> A getMethodAnnotation(Class<A> 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 <A extends Annotation> boolean hasMethodAnnotation(Class<A> 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 extends Annotation> T getMethodAnnotation(Class<T> annotationType) {
return HandlerMethod.this.getMethodAnnotation(annotationType);
}
@Override
public <T extends Annotation> boolean hasMethodAnnotation(Class<T> 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);
}
}
}

View File

@@ -197,7 +197,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
}
private class AsyncResultMethodParameter extends HandlerMethodParameter {
private class AsyncResultMethodParameter extends AnnotatedMethodParameter {
@Nullable
private final Object returnValue;

View File

@@ -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<String, Object> arguments = new LinkedHashMap<>();
Map<String, Object> arguments = new LinkedHashMap<>();
@MessageMapping("/headers")
public void headers(@Header String foo, @Headers Map<String, Object> 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<String, Object> 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<String> future;
org.springframework.util.concurrent.ListenableFutureTask<String> future;
private boolean exceptionCaught = false;
boolean exceptionCaught = false;
@MessageMapping("success")
public org.springframework.util.concurrent.ListenableFutureTask<String> handleListenableFuture() {
@@ -565,9 +601,9 @@ public class SimpAnnotationMethodMessageHandlerTests {
@Controller
private static class CompletableFutureController {
private CompletableFuture<String> future;
CompletableFuture<String> future;
private boolean exceptionCaught = false;
boolean exceptionCaught = false;
@MessageMapping("completable-future")
public CompletableFuture<String> handleCompletableFuture() {
@@ -581,14 +617,15 @@ public class SimpAnnotationMethodMessageHandlerTests {
}
}
@Controller
private static class ReactiveController {
private Sinks.One<String> sinkOne;
Sinks.One<String> sinkOne;
private Sinks.Many<String> sinkMany;
Sinks.Many<String> sinkMany;
private boolean exceptionCaught = false;
boolean exceptionCaught = false;
@MessageMapping("mono")
public Mono<String> 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;