[
https://issues.apache.org/jira/browse/LANG-1835?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
William Degrange closed LANG-1835.
----------------------------------
> AnnotationUtils equals(), hashCode() and toString() invoke the synthetic
> methods of the annotation type, such as lambda bodies
> ------------------------------------------------------------------------------------------------------------------------------
>
> Key: LANG-1835
> URL: https://issues.apache.org/jira/browse/LANG-1835
> Project: Commons Lang
> Issue Type: Bug
> Components: lang.*
> Affects Versions: 3.21.0
> Environment: OpenJDK 26.0.2 (not JDK specific: javac generates these
> synthetic methods since Java 8)
> Reporter: William Degrange
> Priority: Major
> Fix For: 3.21.1
>
>
> h3. Problem
> {{AnnotationUtils.equals(Annotation, Annotation)}},
> {{AnnotationUtils.hashCode(Annotation)}} and
> {{AnnotationUtils.toString(Annotation)}} consider every method returned by
> {{annotationType().getDeclaredMethods()}} as an annotation member ({{equals}}
> and {{toString}} only skip the methods with parameters). But an annotation
> type can declare methods which are not members:
> * the compiler generates a private static synthetic method for each lambda
> expression used in a constant of the annotation type: a {{Runnable}} constant
> initialized with a lambda gives {{private static void lambda$static$0()}};
> * bytecode instrumentation can add more synthetic methods to such an
> annotation type, e.g. JaCoCo adds a {{$jacocoInit}} method to instrument the
> lambda body.
> Before 3.21.0, invoking these private methods failed with an
> {{IllegalAccessException}}: {{hashCode}} and {{toString}} threw an
> {{UncheckedException}}, and {{equals}} returned {{false}}. Since 3.21.0, the
> methods are made accessible before being invoked (LANG-1815), so these
> methods are now *executed*:
> * {{hashCode}} throws an {{IllegalStateException}} ("Annotation method ...
> returned null") when such a method returns {{void}}, like the one of a
> {{Runnable}} lambda;
> * {{hashCode}} throws an {{IllegalArgumentException}} ("wrong number of
> arguments") when such a method has parameters, like the one of a {{Function}}
> lambda, since {{hashCode}} does not check the number of parameters;
> * otherwise, the returned value is hashed and printed as if it were the value
> of a member: {{AnnotationUtils.hashCode(a)}} differs from {{a.hashCode()}},
> although the Javadoc of {{hashCode(Annotation)}} refers to the algorithm of
> {{Annotation.hashCode()}};
> * in every case, the body of the lambda expression runs during {{equals}},
> {{hashCode}} and {{toString}}.
> As a result, a dynamic proxy implementing an annotation with
> {{AnnotationUtils}} (the use case described in its Javadoc) is not consistent
> with the JDK annotations: its {{hashCode}} throws, or differs from the one of
> an equal JDK annotation (e.g. in a {{HashSet}}).
> h3. Reproducer
> {code:java}
> import static org.junit.jupiter.api.Assertions.assertEquals;
> import static org.junit.jupiter.api.Assertions.assertFalse;
> import java.lang.annotation.Retention;
> import java.lang.annotation.RetentionPolicy;
> import java.util.function.Supplier;
> import org.apache.commons.lang3.AnnotationUtils;
> import org.junit.jupiter.api.Test;
> class AnnotationUtilsSyntheticMethodTest {
> @Retention(RetentionPolicy.RUNTIME)
> public @interface WithRunnable {
> // javac generates: private static synthetic void lambda$static$0()
> Runnable RUNNABLE = () -> { };
> String value();
> }
> @Retention(RetentionPolicy.RUNTIME)
> public @interface WithSupplier {
> // javac generates: private static synthetic String lambda$static$0()
> Supplier<String> SUPPLIER = () -> "not a member";
> String value();
> }
> @WithRunnable("a")
> @WithSupplier("a")
> static class Annotated {
> }
> @Test
> void testHashCodeWithVoidSyntheticMethod() {
> final WithRunnable annotation =
> Annotated.class.getAnnotation(WithRunnable.class);
> assertEquals(annotation.hashCode(),
> AnnotationUtils.hashCode(annotation));
> }
> @Test
> void testHashCodeWithSyntheticMethod() {
> final WithSupplier annotation =
> Annotated.class.getAnnotation(WithSupplier.class);
> assertEquals(annotation.hashCode(),
> AnnotationUtils.hashCode(annotation));
> }
> @Test
> void testToStringWithSyntheticMethod() {
> final WithSupplier annotation =
> Annotated.class.getAnnotation(WithSupplier.class);
> assertFalse(AnnotationUtils.toString(annotation).contains("lambda$"));
> }
> }
> {code}
> With 3.21.0, the three tests fail:
> * {{testHashCodeWithVoidSyntheticMethod}}: {{IllegalStateException:
> Annotation method private static void
> AnnotationUtilsSyntheticMethodTest$WithRunnable.lambda$static$0() returned
> null}};
> * {{testHashCodeWithSyntheticMethod}}: {{expected: <1335633774> but was:
> <-1572135523>}}, the value returned by {{lambda$static$0()}} being included
> in the hash code;
> * {{testToStringWithSyntheticMethod}}: the string contains
> {{lambda$static$0=not a member}}.
> With 3.20.0, the three tests fail with an {{UncheckedException}} wrapping an
> {{IllegalAccessException}}.
> h3. Expected behavior
> Only the members of the annotation type are considered, as the JDK does:
> {{sun.reflect.annotation.AnnotationType}} only treats the public, abstract
> and non-synthetic methods as members, and
> {{AnnotationInvocationHandler.validateAnnotationMethods}} explicitly skips
> the synthetic private or static methods (its comment mentions the static
> initializer of a lambda stored in an interface field).
> h3. Suggested fix
> Filter the declared methods as {{AnnotationType}} does, in the loops of
> {{equals}}, {{hashCode}} and {{toString}}:
> {code:java}
> private static boolean isMember(final Method method) {
> final int modifiers = method.getModifiers();
> return Modifier.isPublic(modifiers) && Modifier.isAbstract(modifiers) &&
> !method.isSynthetic();
> }
> {code}
--
This message was sent by Atlassian Jira
(v8.20.10#820010)