[ 
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)

Reply via email to