Skip to content

Commit 36348e4

Browse files
committed
Annotation lookup matches unrelated overloads via assignable-parameter
resolution MethodUtils.getAnnotation(Method, Class<A>, boolean, boolean)
1 parent 0c7942f commit 36348e4

2 files changed

Lines changed: 124 additions & 3 deletions

File tree

src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,11 +272,56 @@ public static <A extends Annotation> A getAnnotation(final Method method, final
272272
A annotation = method.getAnnotation(annotationCls);
273273
if (annotation == null && searchSupers) {
274274
final Class<?> mcls = method.getDeclaringClass();
275+
final String methodName = method.getName();
276+
final Class<?>[] paramTypes = method.getParameterTypes();
275277
final List<Class<?>> classes = getAllSuperclassesAndInterfaces(mcls);
276278
for (final Class<?> acls : classes) {
277-
final Method equivalentMethod = ignoreAccess ? getMatchingMethod(acls, method.getName(), method.getParameterTypes())
278-
: getMatchingAccessibleMethod(acls, method.getName(), method.getParameterTypes());
279-
if (equivalentMethod != null) {
279+
// First, attempt an exact parameter-type match (getDeclaredMethod) to
280+
// find a true override. This avoids matching unrelated overloads that
281+
// are merely assignable-compatible (e.g. process(Integer) vs
282+
// process(Number)).
283+
Method equivalentMethod = null;
284+
try {
285+
equivalentMethod = acls.getDeclaredMethod(methodName, paramTypes);
286+
} catch (final NoSuchMethodException ignored) {
287+
// No exact match; check for generic-bridge scenario: the declaring
288+
// class may use a type variable whose erased form is Object (or
289+
// another bound). In that case the parent method's erased
290+
// parameter types differ from the child's concrete types, so we
291+
// scan declared methods for a same-name method whose *erased*
292+
// parameter count matches and whose erased types are assignable
293+
// from our concrete types.
294+
for (final Method candidate : acls.getDeclaredMethods()) {
295+
if (!candidate.getName().equals(methodName)) {
296+
continue;
297+
}
298+
final Class<?>[] candidateParams = candidate.getParameterTypes();
299+
if (candidateParams.length != paramTypes.length) {
300+
continue;
301+
}
302+
// Require that every concrete param type is assignable to the
303+
// candidate's (erased) param type AND that the candidate is
304+
// generic (has at least one TypeVariable in its generic
305+
// parameter types). This prevents matching plain overloads.
306+
boolean genericMatch = false;
307+
boolean paramsMatch = true;
308+
final java.lang.reflect.Type[] genericParams = candidate.getGenericParameterTypes();
309+
for (int i = 0; i < candidateParams.length; i++) {
310+
if (genericParams[i] instanceof java.lang.reflect.TypeVariable) {
311+
genericMatch = true;
312+
}
313+
if (!ClassUtils.isAssignable(paramTypes[i], candidateParams[i], true)) {
314+
paramsMatch = false;
315+
break;
316+
}
317+
}
318+
if (paramsMatch && genericMatch) {
319+
equivalentMethod = candidate;
320+
break;
321+
}
322+
}
323+
}
324+
if (equivalentMethod != null && (ignoreAccess || MemberUtils.isAccessible(equivalentMethod))) {
280325
annotation = equivalentMethod.getAnnotation(annotationCls);
281326
if (annotation != null) {
282327
break;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.commons.lang3.reflect;
19+
20+
import static org.junit.jupiter.api.Assertions.assertNotNull;
21+
import static org.junit.jupiter.api.Assertions.assertNull;
22+
23+
import java.lang.reflect.Method;
24+
25+
import org.junit.jupiter.api.Test;
26+
27+
/**
28+
* Tests {@link MethodUtils#getAnnotation(Method, Class, boolean, boolean)}.
29+
* <p>
30+
* getMatchingMethod allows assignable params, potentially finding annotations on unrelated overloads.
31+
* </p>
32+
*/
33+
public class MethodUtilsAnnotationsTest {
34+
35+
/** Interface with a method taking Number, annotated @Deprecated */
36+
public interface Processor {
37+
38+
@SuppressWarnings("javadoc")
39+
@Deprecated
40+
void process(Number n);
41+
}
42+
43+
/** Implementation that does NOT annotate process(Integer) */
44+
public static class ProcessorImpl implements Processor {
45+
46+
// Overload with Integer — NOT annotated
47+
@SuppressWarnings("javadoc")
48+
public void process(final Integer i) {
49+
// intentionally no @Deprecated
50+
}
51+
52+
@SuppressWarnings("deprecation")
53+
@Override
54+
public void process(final Number n) {
55+
// inherited, annotated on interface
56+
}
57+
}
58+
59+
/**
60+
* getAnnotation() for process(Integer) should return null because the Integer overload is NOT an override of process(Number).
61+
* <ul>
62+
* <li>Pre-patch: getMatchingMethod finds process(Number) (since Integer is assignable to Number) and returns the {@code @Deprecated} annotation
63+
* incorrectly.</li>
64+
* <li>Post-patch: uses getDeclaredMethod with exact types, finds nothing, returns null.</li>
65+
* </ul>
66+
*/
67+
@SuppressWarnings("javadoc")
68+
@Test
69+
public void testAnnotationLookupDoesNotMatchAssignableOverload() throws NoSuchMethodException {
70+
final Method integerMethod = ProcessorImpl.class.getDeclaredMethod("process", Integer.class);
71+
final Deprecated ann = MethodUtils.getAnnotation(integerMethod, Deprecated.class, true, true);
72+
assertNull(ann, "process(Integer) is NOT an override of process(Number); its annotation lookup must return null");
73+
final Method numberMethod = ProcessorImpl.class.getDeclaredMethod("process", Number.class);
74+
assertNotNull(MethodUtils.getAnnotation(numberMethod, Deprecated.class, true, true));
75+
}
76+
}

0 commit comments

Comments
 (0)