From 419e34e571b975adfc1355abd551f19c0c682cd9 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 7 Jan 2024 16:33:06 +0100 Subject: [PATCH] Introduce getMostSpecificMethod variant on BridgeMethodResolver This is able to resolve the original method even if no bridge method has been generated at the same class hierarchy level (a known difference between the Eclipse compiler and regular javac). Closes gh-21843 --- .../AsyncExecutionInterceptor.java | 12 ++- .../springframework/aop/support/AopUtils.java | 7 +- .../MethodValidationAdapter.java | 6 +- .../core/BridgeMethodResolver.java | 78 +++++++++++++------ .../core/BridgeMethodResolverTests.java | 13 +++- 5 files changed, 77 insertions(+), 39 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java index e89040d1f1e0..c2ea5aad5a4f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -32,7 +32,6 @@ import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; /** * AOP Alliance {@code MethodInterceptor} that processes method invocations @@ -101,10 +100,9 @@ public AsyncExecutionInterceptor(@Nullable Executor defaultExecutor, AsyncUncaug @Nullable public Object invoke(final MethodInvocation invocation) throws Throwable { Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); - Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass); - final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + final Method userMethod = BridgeMethodResolver.getMostSpecificMethod(invocation.getMethod(), targetClass); - AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod); + AsyncTaskExecutor executor = determineAsyncExecutor(userMethod); if (executor == null) { throw new IllegalStateException( "No executor specified and no default executor set on AsyncExecutionInterceptor either"); @@ -118,10 +116,10 @@ public Object invoke(final MethodInvocation invocation) throws Throwable { } } catch (ExecutionException ex) { - handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments()); + handleError(ex.getCause(), userMethod, invocation.getArguments()); } catch (Throwable ex) { - handleError(ex, userDeclaredMethod, invocation.getArguments()); + handleError(ex, userMethod, invocation.getArguments()); } return null; }; diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java index e30e4519c713..6b30d707a16d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -199,12 +199,11 @@ public static boolean isFinalizeMethod(@Nullable Method method) { * @return the specific target method, or the original method if the * {@code targetClass} doesn't implement it or is {@code null} * @see org.springframework.util.ClassUtils#getMostSpecificMethod + * @see org.springframework.core.BridgeMethodResolver#getMostSpecificMethod */ public static Method getMostSpecificMethod(Method method, @Nullable Class targetClass) { Class specificTargetClass = (targetClass != null ? ClassUtils.getUserClass(targetClass) : null); - Method resolvedMethod = ClassUtils.getMostSpecificMethod(method, specificTargetClass); - // If we are dealing with method with generic parameters, find the original method. - return BridgeMethodResolver.findBridgedMethod(resolvedMethod); + return BridgeMethodResolver.getMostSpecificMethod(method, specificTargetClass); } /** diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java index da156f85c731..b7d696e68b98 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -51,7 +51,6 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.function.SingletonSupplier; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; @@ -264,8 +263,7 @@ public final Set> invokeValidatorForArguments( catch (IllegalArgumentException ex) { // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011 // Let's try to find the bridged method on the implementation class... - Method mostSpecificMethod = ClassUtils.getMostSpecificMethod(method, target.getClass()); - Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(mostSpecificMethod); + Method bridgedMethod = BridgeMethodResolver.getMostSpecificMethod(method, target.getClass()); violations = execVal.validateParameters(target, bridgedMethod, arguments, groups); } return violations; diff --git a/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java index 70cddfd49348..f8bbfbf78fb9 100644 --- a/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java +++ b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -50,43 +50,70 @@ */ public final class BridgeMethodResolver { - private static final Map cache = new ConcurrentReferenceHashMap<>(); + private static final Map cache = new ConcurrentReferenceHashMap<>(); private BridgeMethodResolver() { } /** - * Find the original method for the supplied {@link Method bridge Method}. + * Find the local original method for the supplied {@link Method bridge Method}. *

It is safe to call this method passing in a non-bridge {@link Method} instance. * In such a case, the supplied {@link Method} instance is returned directly to the caller. * Callers are not required to check for bridging before calling this method. - * @param bridgeMethod the method to introspect + * @param bridgeMethod the method to introspect against its declaring class * @return the original method (either the bridged method or the passed-in method * if no more specific one could be found) + * @see #getMostSpecificMethod(Method, Class) */ public static Method findBridgedMethod(Method bridgeMethod) { - if (!bridgeMethod.isBridge()) { + return resolveBridgeMethod(bridgeMethod, bridgeMethod.getDeclaringClass()); + } + + /** + * Determine the most specific method for the supplied {@link Method bridge Method} + * in the given class hierarchy, even if not available on the local declaring class. + *

This is effectively a combination of {@link ClassUtils#getMostSpecificMethod} + * and {@link #findBridgedMethod}, resolving the original method even if no bridge + * method has been generated at the same class hierarchy level (a known difference + * between the Eclipse compiler and regular javac). + * @param bridgeMethod the method to introspect against the given target class + * @param targetClass the target class to find methods on + * @return the original method (either the bridged method or the passed-in method + * if no more specific one could be found) + * @since 6.1.3 + * @see #findBridgedMethod + * @see org.springframework.util.ClassUtils#getMostSpecificMethod + */ + public static Method getMostSpecificMethod(Method bridgeMethod, @Nullable Class targetClass) { + Method specificMethod = ClassUtils.getMostSpecificMethod(bridgeMethod, targetClass); + return resolveBridgeMethod(specificMethod, + (targetClass != null ? targetClass : specificMethod.getDeclaringClass())); + } + + private static Method resolveBridgeMethod(Method bridgeMethod, Class targetClass) { + boolean localBridge = (targetClass == bridgeMethod.getDeclaringClass()); + if (!bridgeMethod.isBridge() && localBridge) { return bridgeMethod; } - Method bridgedMethod = cache.get(bridgeMethod); + + Object cacheKey = (localBridge ? bridgeMethod : new MethodClassKey(bridgeMethod, targetClass)); + Method bridgedMethod = cache.get(cacheKey); if (bridgedMethod == null) { // Gather all methods with matching name and parameter size. List candidateMethods = new ArrayList<>(); - MethodFilter filter = candidateMethod -> - isBridgedCandidateFor(candidateMethod, bridgeMethod); - ReflectionUtils.doWithMethods(bridgeMethod.getDeclaringClass(), candidateMethods::add, filter); + MethodFilter filter = (candidateMethod -> isBridgedCandidateFor(candidateMethod, bridgeMethod)); + ReflectionUtils.doWithMethods(targetClass, candidateMethods::add, filter); if (!candidateMethods.isEmpty()) { - bridgedMethod = candidateMethods.size() == 1 ? - candidateMethods.get(0) : - searchCandidates(candidateMethods, bridgeMethod); + bridgedMethod = (candidateMethods.size() == 1 ? candidateMethods.get(0) : + searchCandidates(candidateMethods, bridgeMethod, targetClass)); } if (bridgedMethod == null) { // A bridge method was passed in but we couldn't find the bridged method. // Let's proceed with the passed-in method and hope for the best... bridgedMethod = bridgeMethod; } - cache.put(bridgeMethod, bridgedMethod); + cache.put(cacheKey, bridgedMethod); } return bridgedMethod; } @@ -110,19 +137,19 @@ private static boolean isBridgedCandidateFor(Method candidateMethod, Method brid * @return the bridged method, or {@code null} if none found */ @Nullable - private static Method searchCandidates(List candidateMethods, Method bridgeMethod) { + private static Method searchCandidates(List candidateMethods, Method bridgeMethod, Class targetClass) { if (candidateMethods.isEmpty()) { return null; } Method previousMethod = null; boolean sameSig = true; for (Method candidateMethod : candidateMethods) { - if (isBridgeMethodFor(bridgeMethod, candidateMethod, bridgeMethod.getDeclaringClass())) { + if (isBridgeMethodFor(bridgeMethod, candidateMethod, targetClass)) { return candidateMethod; } else if (previousMethod != null) { - sameSig = sameSig && - Arrays.equals(candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes()); + sameSig = sameSig && Arrays.equals( + candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes()); } previousMethod = candidateMethod; } @@ -133,12 +160,12 @@ else if (previousMethod != null) { * Determines whether the bridge {@link Method} is the bridge for the * supplied candidate {@link Method}. */ - static boolean isBridgeMethodFor(Method bridgeMethod, Method candidateMethod, Class declaringClass) { - if (isResolvedTypeMatch(candidateMethod, bridgeMethod, declaringClass)) { + static boolean isBridgeMethodFor(Method bridgeMethod, Method candidateMethod, Class targetClass) { + if (isResolvedTypeMatch(candidateMethod, bridgeMethod, targetClass)) { return true; } Method method = findGenericDeclaration(bridgeMethod); - return (method != null && isResolvedTypeMatch(method, candidateMethod, declaringClass)); + return (method != null && isResolvedTypeMatch(method, candidateMethod, targetClass)); } /** @@ -147,14 +174,14 @@ static boolean isBridgeMethodFor(Method bridgeMethod, Method candidateMethod, Cl * are equal after resolving all types against the declaringType, otherwise * returns {@code false}. */ - private static boolean isResolvedTypeMatch(Method genericMethod, Method candidateMethod, Class declaringClass) { + private static boolean isResolvedTypeMatch(Method genericMethod, Method candidateMethod, Class targetClass) { Type[] genericParameters = genericMethod.getGenericParameterTypes(); if (genericParameters.length != candidateMethod.getParameterCount()) { return false; } Class[] candidateParameters = candidateMethod.getParameterTypes(); for (int i = 0; i < candidateParameters.length; i++) { - ResolvableType genericParameter = ResolvableType.forMethodParameter(genericMethod, i, declaringClass); + ResolvableType genericParameter = ResolvableType.forMethodParameter(genericMethod, i, targetClass); Class candidateParameter = candidateParameters[i]; if (candidateParameter.isArray()) { // An array type: compare the component type. @@ -163,7 +190,8 @@ private static boolean isResolvedTypeMatch(Method genericMethod, Method candidat } } // A non-array type: compare the type itself. - if (!ClassUtils.resolvePrimitiveIfNecessary(candidateParameter).equals(ClassUtils.resolvePrimitiveIfNecessary(genericParameter.toClass()))) { + if (!ClassUtils.resolvePrimitiveIfNecessary(candidateParameter).equals( + ClassUtils.resolvePrimitiveIfNecessary(genericParameter.toClass()))) { return false; } } @@ -177,6 +205,10 @@ private static boolean isResolvedTypeMatch(Method genericMethod, Method candidat */ @Nullable private static Method findGenericDeclaration(Method bridgeMethod) { + if (!bridgeMethod.isBridge()) { + return bridgeMethod; + } + // Search parent types for method that has same signature as bridge. Class superclass = bridgeMethod.getDeclaringClass().getSuperclass(); while (superclass != null && Object.class != superclass) { diff --git a/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java b/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java index d5fb2f1f63d8..449d886d920c 100644 --- a/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -86,6 +86,17 @@ void findBridgedMethodInHierarchy() throws Exception { assertThat(bridgedMethod.getParameterTypes()[0]).isEqualTo(Date.class); } + @Test + void findBridgedMethodFromOriginalMethodInHierarchy() throws Exception { + Method originalMethod = Adder.class.getMethod("add", Object.class); + assertThat(originalMethod.isBridge()).isFalse(); + Method bridgedMethod = BridgeMethodResolver.getMostSpecificMethod(originalMethod, DateAdder.class); + assertThat(bridgedMethod.isBridge()).isFalse(); + assertThat(bridgedMethod.getName()).isEqualTo("add"); + assertThat(bridgedMethod.getParameterCount()).isEqualTo(1); + assertThat(bridgedMethod.getParameterTypes()[0]).isEqualTo(Date.class); + } + @Test void isBridgeMethodFor() throws Exception { Method bridged = MyBar.class.getDeclaredMethod("someMethod", String.class, Object.class);