Ivan Ponomarev created LANG-1815:
------------------------------------
Summary: AnnotationUtils.equals(..) returns false for equal
package-private annotations due to reflective access failure
Key: LANG-1815
URL: https://issues.apache.org/jira/browse/LANG-1815
Project: Commons Lang
Issue Type: Bug
Components: lang.*
Reporter: Ivan Ponomarev
h3. Description
{{AnnotationUtils.equals(Annotation a1, Annotation a2)}} is intended to
implement the equality rules described by {{Annotation.equals(Object)}} (as
stated in its Javadoc). However, the current implementation can incorrectly
return false for equal annotations when the annotation type is not reflectively
accessible from the caller’s package.
The issue is caused by handling reflective failures via:
{code:java}
} catch (final ReflectiveOperationException ex) {
return false;
}
{code}
This is problematic because reflective access errors are not a “not equal”
condition; they indicate that the implementation failed to read an annotation
member value. Returning {{false}} silently violates the expected semantics and
makes the method’s result depend on the caller’s package/module access rather
than annotation content.
A concrete case is a package-private annotation type accessed from outside
{{org.apache.commons.lang3}}: invoking its member method reflectively can throw
an access-related reflective exception, which is currently swallowed and
converted to {{false}}.
(!) To reproduce, the test class must be *outside* the
{{org.apache.commons.lang3}} package.
{code:java}
package org.apache.commons.lang3.external;
import org.apache.commons.lang3.AnnotationUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
// CAUTION: in order to reproduce the ReflectiveOperationException bug, this
test
// MUST be located OUTSIDE org.apache.commons.lang3 package.
// Do NOT move it to the org.apache.commons.lang3 package!
public class AnnotationEqualsTest {
@Retention(RetentionPolicy.RUNTIME)
@interface Tag {
String value();
}
@Tag("value")
private final Object a = new Object();
@Tag("value")
private final Object b = new Object();
@Test
void equalsWorksOnPackagePrivateAnnotations() throws Exception {
Tag tagA = getClass().getDeclaredField("a").getAnnotation(Tag.class);
Tag tagB = getClass().getDeclaredField("b").getAnnotation(Tag.class);
// Expected true; currently returns false because a reflective access
exception is swallowed.
Assertions.assertTrue(AnnotationUtils.equals(tagA, tagB));
}
}
{code}
h3.Proposed fix
# Ensure member methods are accessible before invocation via
{{m.setAccessible(true);}} This change makes the reproducer test "green".
# Do not silently convert reflective failures into {{false}}, wrap and rethrow
into {{IllegalStateException}} instead. This makes failures visible and
prevents incorrect “false” results caused by reflective access issues.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)