From de8c4e2c9ebdd88bfeb1a064eae7d13bf6065c64 Mon Sep 17 00:00:00 2001 From: Ivan Date: Mon, 26 Jan 2026 17:35:22 +0000 Subject: [PATCH 1/2] fix LANG-1815 --- .../apache/commons/lang3/AnnotationUtils.java | 3 +- .../lang3/external/AnnotationEqualsTest.java | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/apache/commons/lang3/external/AnnotationEqualsTest.java diff --git a/src/main/java/org/apache/commons/lang3/AnnotationUtils.java b/src/main/java/org/apache/commons/lang3/AnnotationUtils.java index 2a995e3fc1f..00b5cb5be9b 100644 --- a/src/main/java/org/apache/commons/lang3/AnnotationUtils.java +++ b/src/main/java/org/apache/commons/lang3/AnnotationUtils.java @@ -212,6 +212,7 @@ public static boolean equals(final Annotation a1, final Annotation a2) { for (final Method m : type1.getDeclaredMethods()) { if (m.getParameterTypes().length == 0 && isValidAnnotationMemberType(m.getReturnType())) { + m.setAccessible(true); final Object v1 = m.invoke(a1); final Object v2 = m.invoke(a2); if (!memberEquals(m.getReturnType(), v1, v2)) { @@ -220,7 +221,7 @@ && isValidAnnotationMemberType(m.getReturnType())) { } } } catch (final ReflectiveOperationException ex) { - return false; + throw new IllegalStateException(ex); } return true; } diff --git a/src/test/java/org/apache/commons/lang3/external/AnnotationEqualsTest.java b/src/test/java/org/apache/commons/lang3/external/AnnotationEqualsTest.java new file mode 100644 index 00000000000..29ce8e27162 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/external/AnnotationEqualsTest.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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 https://issues.apache.org/jira/browse/LANG-1815, +// 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); + Assertions.assertTrue(AnnotationUtils.equals(tagA, tagB)); + } +} From 37d696561fa4e6885cc6745cae414bc4aa22513c Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 27 Jan 2026 10:17:49 +0000 Subject: [PATCH 2/2] fix review comments --- .../apache/commons/lang3/AnnotationUtils.java | 4 +- .../lang3/external/AnnotationEqualsTest.java | 56 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/AnnotationUtils.java b/src/main/java/org/apache/commons/lang3/AnnotationUtils.java index 00b5cb5be9b..fc1d01625b5 100644 --- a/src/main/java/org/apache/commons/lang3/AnnotationUtils.java +++ b/src/main/java/org/apache/commons/lang3/AnnotationUtils.java @@ -212,7 +212,9 @@ public static boolean equals(final Annotation a1, final Annotation a2) { for (final Method m : type1.getDeclaredMethods()) { if (m.getParameterTypes().length == 0 && isValidAnnotationMemberType(m.getReturnType())) { - m.setAccessible(true); + if (!m.isAccessible()) { + m.setAccessible(true); + } final Object v1 = m.invoke(a1); final Object v2 = m.invoke(a2); if (!memberEquals(m.getReturnType(), v1, v2)) { diff --git a/src/test/java/org/apache/commons/lang3/external/AnnotationEqualsTest.java b/src/test/java/org/apache/commons/lang3/external/AnnotationEqualsTest.java index 29ce8e27162..27deeec16a9 100644 --- a/src/test/java/org/apache/commons/lang3/external/AnnotationEqualsTest.java +++ b/src/test/java/org/apache/commons/lang3/external/AnnotationEqualsTest.java @@ -17,21 +17,54 @@ 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.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.InvocationTargetException; -// CAUTION: in order to reproduce https://issues.apache.org/jira/browse/LANG-1815, -// this test MUST be located OUTSIDE org.apache.commons.lang3 package. -// Do NOT move it to the org.apache.commons.lang3 package! +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Regression test for LANG-1815. + *

+ * Verifies that {@code AnnotationUtils.equals(Annotation, Annotation)} treats two equal + * package-private annotations as equal, and also wraps a possible ReflectiveOperationException. + *

+ * + *

Important

+ *

+ * This test relies on reflective access rules that differ depending on the caller's package. + * To reproduce the original bug, this class must remain outside the + * {@code org.apache.commons.lang3} package. + *

+ *

+ * Do not move this class into {@code org.apache.commons.lang3}, + * otherwise the test may no longer exercise the failing scenario from LANG-1815. + *

+ */ public class AnnotationEqualsTest { @Retention(RetentionPolicy.RUNTIME) @interface Tag { String value(); } + static class ThrowingTag implements Tag { + @Override + public String value() { + throw new IllegalArgumentException("boom"); + } + + @Override + public Class annotationType() { + return Tag.class; + } + } + @Tag("value") private final Object a = new Object(); @Tag("value") @@ -41,6 +74,19 @@ public class AnnotationEqualsTest { void equalsWorksOnPackagePrivateAnnotations() throws Exception { Tag tagA = getClass().getDeclaredField("a").getAnnotation(Tag.class); Tag tagB = getClass().getDeclaredField("b").getAnnotation(Tag.class); - Assertions.assertTrue(AnnotationUtils.equals(tagA, tagB)); + assertTrue(AnnotationUtils.equals(tagA, tagB)); } + + @Test + void equalsWrapsReflectiveOperationException() throws Exception { + // Proxy annotation instances: calling Tag#value() will throw at runtime + final Tag tagA = new ThrowingTag(); + final Tag tagB = getClass().getDeclaredField("b").getAnnotation(Tag.class); + + final IllegalStateException ex = + assertThrows(IllegalStateException.class, () -> AnnotationUtils.equals(tagA, tagB)); + assertInstanceOf(InvocationTargetException.class, ex.getCause()); + assertEquals("boom", ((InvocationTargetException) ex.getCause()).getTargetException().getMessage()); + } + }