From 76855255a1ebc42371bea3122a5bf74fa26599c5 Mon Sep 17 00:00:00 2001 From: junhyeong9812 Date: Wed, 19 Aug 2026 16:35:03 +0900 Subject: [PATCH] Extend TypeNotPresentException probing to nested annotations AttributeMethods probes Class, Class[], and enum attributes so that annotations whose values cannot be resolved at runtime are filtered during scanning. Annotation-typed attributes were not probed, and probing them by invocation alone is not sufficient: the JDK returns the nested annotation proxy successfully and only throws when one of its own attributes is accessed. An annotation whose nested annotation references a stale enum constant therefore passed canLoad() and leaked EnumConstantNotPresentException (or the exception instance itself via attribute maps) on later access. canLoad() and validate() now recurse into annotation and annotation array attribute values, reusing the same probing for the nested annotation's own attributes. Recursion is guaranteed to terminate because the JLS forbids cyclic annotation member types. Signed-off-by: junhyeong9812 --- .../core/annotation/AttributeMethods.java | 39 ++++- .../annotation/AttributeMethodsTests.java | 157 ++++++++++++++++++ 2 files changed, 192 insertions(+), 4 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java index 44007afb4fe9..b25478562fb2 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java @@ -77,11 +77,13 @@ private AttributeMethods(@Nullable Class annotationType, M if (!foundDefaultValueMethod && (method.getDefaultValue() != null)) { foundDefaultValueMethod = true; } - if (!foundNestedAnnotation && (type.isAnnotation() || (type.isArray() && type.componentType().isAnnotation()))) { + boolean nestedAnnotation = (type.isAnnotation() || (type.isArray() && type.componentType().isAnnotation())); + if (!foundNestedAnnotation && nestedAnnotation) { foundNestedAnnotation = true; } ReflectionUtils.makeAccessible(method); - this.canThrowTypeNotPresentException[i] = (type == Class.class || type == Class[].class || type.isEnum()); + this.canThrowTypeNotPresentException[i] = (type == Class.class || type == Class[].class || + type.isEnum() || nestedAnnotation); } this.hasDefaultValueMethod = foundDefaultValueMethod; this.hasNestedAnnotation = foundNestedAnnotation; @@ -104,7 +106,10 @@ boolean canLoad(Annotation annotation, AnnotatedElement source) { for (int i = 0; i < size(); i++) { if (canThrowTypeNotPresentException(i)) { try { - AnnotationUtils.invokeAnnotationMethod(get(i), annotation); + Object value = AnnotationUtils.invokeAnnotationMethod(get(i), annotation); + if (!canLoadNestedAnnotations(value, source)) { + return false; + } } catch (IllegalStateException ex) { // Plain invocation failure to expose -> leave up to attribute retrieval @@ -123,6 +128,20 @@ boolean canLoad(Annotation annotation, AnnotatedElement source) { return true; } + private boolean canLoadNestedAnnotations(@Nullable Object value, AnnotatedElement source) { + if (value instanceof Annotation nested) { + return forAnnotationType(nested.annotationType()).canLoad(nested, source); + } + if (value instanceof Annotation[] nestedArray) { + for (Annotation nested : nestedArray) { + if (!forAnnotationType(nested.annotationType()).canLoad(nested, source)) { + return false; + } + } + } + return true; + } + /** * Check if values from the given annotation can be safely accessed without causing * any {@link TypeNotPresentException TypeNotPresentExceptions}. @@ -138,7 +157,8 @@ void validate(Annotation annotation) { for (int i = 0; i < size(); i++) { if (canThrowTypeNotPresentException(i)) { try { - AnnotationUtils.invokeAnnotationMethod(get(i), annotation); + Object value = AnnotationUtils.invokeAnnotationMethod(get(i), annotation); + validateNestedAnnotations(value); } catch (IllegalStateException ex) { throw ex; @@ -152,6 +172,17 @@ void validate(Annotation annotation) { } } + private void validateNestedAnnotations(@Nullable Object value) { + if (value instanceof Annotation nested) { + forAnnotationType(nested.annotationType()).validate(nested); + } + else if (value instanceof Annotation[] nestedArray) { + for (Annotation nested : nestedArray) { + forAnnotationType(nested.annotationType()).validate(nested); + } + } + } + private void assertAnnotation(Annotation annotation) { Assert.notNull(annotation, "Annotation must not be null"); if (this.annotationType != null) { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java index e9602d452376..5d1a6c029fd3 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java @@ -89,6 +89,18 @@ void canThrowTypeNotPresentExceptionWhenHasClassArrayAttributeReturnsTrue() { assertThat(methods.canThrowTypeNotPresentException(0)).isTrue(); } + @Test + void canThrowTypeNotPresentExceptionWhenHasAnnotationAttributeReturnsTrue() { + AttributeMethods methods = AttributeMethods.forAnnotationType(NestedValue.class); + assertThat(methods.canThrowTypeNotPresentException(0)).isTrue(); + } + + @Test + void canThrowTypeNotPresentExceptionWhenHasAnnotationArrayAttributeReturnsTrue() { + AttributeMethods methods = AttributeMethods.forAnnotationType(NestedArrayValue.class); + assertThat(methods.canThrowTypeNotPresentException(0)).isTrue(); + } + @Test void canThrowTypeNotPresentExceptionWhenNotClassOrClassArrayAttributeReturnsFalse() { AttributeMethods methods = AttributeMethods.forAnnotationType(ValueOnly.class); @@ -141,6 +153,117 @@ void validateWhenDoesNotHaveTypeNotPresentExceptionThrowsNothing() { attributes.validate(annotation); } + @Test + void isValidWhenNestedAnnotationHasEnumConstantNotPresentExceptionReturnsFalse() { + EnumValueInner inner = mockBrokenEnumValueInner(); + NestedValue annotation = mockAnnotation(NestedValue.class); + given(annotation.value()).willReturn(inner); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + assertThat(attributes.canLoad(annotation, getClass())).isFalse(); + } + + @Test + void isValidWhenNestedAnnotationDoesNotHaveEnumConstantNotPresentExceptionReturnsTrue() { + EnumValueInner inner = mockHealthyEnumValueInner(); + NestedValue annotation = mockAnnotation(NestedValue.class); + given(annotation.value()).willReturn(inner); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + assertThat(attributes.canLoad(annotation, getClass())).isTrue(); + } + + @Test + void validateWhenNestedAnnotationHasEnumConstantNotPresentExceptionThrowsException() { + EnumValueInner inner = mockBrokenEnumValueInner(); + NestedValue annotation = mockAnnotation(NestedValue.class); + given(annotation.value()).willReturn(inner); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + assertThatIllegalStateException().isThrownBy(() -> attributes.validate(annotation)) + .withMessageContaining("EnumValueInner") + .withCauseInstanceOf(EnumConstantNotPresentException.class); + } + + @Test + void validateWhenNestedAnnotationDoesNotHaveEnumConstantNotPresentExceptionThrowsNothing() { + EnumValueInner inner = mockHealthyEnumValueInner(); + NestedValue annotation = mockAnnotation(NestedValue.class); + given(annotation.value()).willReturn(inner); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + attributes.validate(annotation); + } + + @Test + void isValidWhenNestedAnnotationArrayHasEnumConstantNotPresentExceptionReturnsFalse() { + EnumValueInner inner = mockBrokenEnumValueInner(); + NestedArrayValue annotation = mockAnnotation(NestedArrayValue.class); + given(annotation.value()).willReturn(new EnumValueInner[] {inner}); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + assertThat(attributes.canLoad(annotation, getClass())).isFalse(); + } + + @Test + void isValidWhenNestedAnnotationArrayDoesNotHaveEnumConstantNotPresentExceptionReturnsTrue() { + EnumValueInner inner = mockHealthyEnumValueInner(); + NestedArrayValue annotation = mockAnnotation(NestedArrayValue.class); + given(annotation.value()).willReturn(new EnumValueInner[] {inner}); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + assertThat(attributes.canLoad(annotation, getClass())).isTrue(); + } + + @Test + void isValidWhenNestedAnnotationArrayIsEmptyReturnsTrue() { + NestedArrayValue annotation = mockAnnotation(NestedArrayValue.class); + given(annotation.value()).willReturn(new EnumValueInner[0]); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + assertThat(attributes.canLoad(annotation, getClass())).isTrue(); + } + + @Test + void validateWhenNestedAnnotationArrayHasEnumConstantNotPresentExceptionThrowsException() { + EnumValueInner inner = mockBrokenEnumValueInner(); + NestedArrayValue annotation = mockAnnotation(NestedArrayValue.class); + given(annotation.value()).willReturn(new EnumValueInner[] {inner}); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + assertThatIllegalStateException().isThrownBy(() -> attributes.validate(annotation)) + .withMessageContaining("EnumValueInner") + .withCauseInstanceOf(EnumConstantNotPresentException.class); + } + + @Test + void isValidWhenDeeplyNestedAnnotationHasEnumConstantNotPresentExceptionReturnsFalse() { + EnumValueInner inner = mockBrokenEnumValueInner(); + NestedValue nested = mockAnnotation(NestedValue.class); + given(nested.value()).willReturn(inner); + DeepNestedValue annotation = mockAnnotation(DeepNestedValue.class); + given(annotation.value()).willReturn(nested); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + assertThat(attributes.canLoad(annotation, getClass())).isFalse(); + } + + @Test + void validateWhenDeeplyNestedAnnotationHasEnumConstantNotPresentExceptionThrowsException() { + EnumValueInner inner = mockBrokenEnumValueInner(); + NestedValue nested = mockAnnotation(NestedValue.class); + given(nested.value()).willReturn(inner); + DeepNestedValue annotation = mockAnnotation(DeepNestedValue.class); + given(annotation.value()).willReturn(nested); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + assertThatIllegalStateException().isThrownBy(() -> attributes.validate(annotation)) + .withMessageContaining("EnumValueInner") + .withCauseInstanceOf(EnumConstantNotPresentException.class); + } + + private EnumValueInner mockBrokenEnumValueInner() { + EnumValueInner inner = mockAnnotation(EnumValueInner.class); + given(inner.value()).willThrow(new EnumConstantNotPresentException(ExampleEnum.class, "MISSING")); + return inner; + } + + private EnumValueInner mockHealthyEnumValueInner() { + EnumValueInner inner = mockAnnotation(EnumValueInner.class); + given(inner.value()).willReturn(ExampleEnum.ONE); + return inner; + } + private List getAll(AttributeMethods attributes) { List result = new ArrayList<>(attributes.size()); for (int i = 0; i < attributes.size(); i++) { @@ -208,4 +331,38 @@ private A mockAnnotation(Class annotationType) { } + @Retention(RetentionPolicy.RUNTIME) + @interface EnumValueInner { + + ExampleEnum value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface NestedValue { + + EnumValueInner value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface NestedArrayValue { + + EnumValueInner[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface DeepNestedValue { + + NestedValue value(); + + } + + enum ExampleEnum { + + ONE + + } + }