From 62d09be2ae47b0d8bbfa3d4bdcf63592fad80b0d Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:00:24 +0100 Subject: [PATCH] Log warning if meta-annotation is ignored due to types not present Prior to this commit, if a meta-annotation could not be loaded because its attributes referenced types not present in the classpath, the meta-annotation was silently ignored. To improve diagnostics for such use cases, this commit introduces WARN support in IntrospectionFailureLogger and revises AttributeMethods.canLoad() to log a warning if a meta-annotation is ignored due to an exception thrown while attempting to load its attributes. For example, a warning similar to the following is now logged in such cases. WARN o.s.c.a.MergedAnnotation - Failed to introspect meta-annotation @example.MyAnnotation on class example.Config: java.lang.TypeNotPresentException: Type example.OptionalDependency not present This commit also improves log messages in AnnotationTypeMappings. Closes gh-35927 --- .../core/annotation/AnnotationTypeMappings.java | 10 +++++----- .../core/annotation/AnnotationsScanner.java | 2 +- .../core/annotation/AttributeMethods.java | 10 +++++++++- .../annotation/IntrospectionFailureLogger.java | 16 ++++++++++++++-- .../core/annotation/AttributeMethodsTests.java | 4 ++-- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java index 5b4e6fd847d..95ac57f86e1 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java @@ -79,7 +79,7 @@ final class AnnotationTypeMappings { private void addAllMappings(Class annotationType, Set> visitedAnnotationTypes) { Deque queue = new ArrayDeque<>(); - addIfPossible(queue, null, annotationType, null, visitedAnnotationTypes); + addIfPossible(queue, null, annotationType, null, false, visitedAnnotationTypes); while (!queue.isEmpty()) { AnnotationTypeMapping mapping = queue.removeFirst(); this.mappings.add(mapping); @@ -109,12 +109,12 @@ final class AnnotationTypeMappings { } private void addIfPossible(Deque queue, AnnotationTypeMapping source, Annotation ann) { - addIfPossible(queue, source, ann.annotationType(), ann, new HashSet<>()); + addIfPossible(queue, source, ann.annotationType(), ann, true, new HashSet<>()); } private void addIfPossible(Deque queue, @Nullable AnnotationTypeMapping source, Class annotationType, @Nullable Annotation ann, - Set> visitedAnnotationTypes) { + boolean meta, Set> visitedAnnotationTypes) { try { queue.addLast(new AnnotationTypeMapping(source, annotationType, ann, visitedAnnotationTypes)); @@ -122,8 +122,8 @@ final class AnnotationTypeMappings { catch (Exception ex) { AnnotationUtils.rethrowAnnotationConfigurationException(ex); if (failureLogger.isEnabled()) { - failureLogger.log("Failed to introspect meta-annotation " + annotationType.getName(), - (source != null ? source.getAnnotationType() : null), ex); + failureLogger.log("Failed to introspect " + (meta ? "meta-annotation @" : "annotation @") + + annotationType.getName(), (source != null ? source.getAnnotationType() : null), ex); } } } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java index e191ed828bc..b4609cc04e2 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java @@ -443,7 +443,7 @@ abstract class AnnotationsScanner { Annotation annotation = annotations[i]; //noinspection DataFlowIssue if (isIgnorable(annotation.annotationType()) || - !AttributeMethods.forAnnotationType(annotation.annotationType()).canLoad(annotation)) { + !AttributeMethods.forAnnotationType(annotation.annotationType()).canLoad(annotation, source)) { annotations[i] = null; } else { 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 a53aae38992..a8ad9efbf22 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 @@ -17,6 +17,7 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Comparator; @@ -38,6 +39,8 @@ import org.springframework.util.ReflectionUtils; */ final class AttributeMethods { + private static final IntrospectionFailureLogger failureLogger = IntrospectionFailureLogger.WARN; + static final AttributeMethods NONE = new AttributeMethods(null, new Method[0]); static final Map, AttributeMethods> cache = new ConcurrentReferenceHashMap<>(); @@ -91,10 +94,11 @@ final class AttributeMethods { * exceptions for {@code Class} values (instead of the more typical early * {@code Class.getAnnotations() failure} on a regular JVM). * @param annotation the annotation to check + * @param source the element that the supplied annotation is declared on * @return {@code true} if all values are present * @see #validate(Annotation) */ - boolean canLoad(Annotation annotation) { + boolean canLoad(Annotation annotation, AnnotatedElement source) { assertAnnotation(annotation); for (int i = 0; i < size(); i++) { if (canThrowTypeNotPresentException(i)) { @@ -107,6 +111,10 @@ final class AttributeMethods { } catch (Throwable ex) { // TypeNotPresentException etc. -> annotation type not actually loadable. + if (failureLogger.isEnabled()) { + failureLogger.log("Failed to introspect meta-annotation @" + + getName(annotation.annotationType()), source, ex); + } return false; } } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java b/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java index 7c8d99a7c5e..9cee0de9736 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java @@ -24,9 +24,10 @@ import org.jspecify.annotations.Nullable; * Log facade used to handle annotation introspection failures (in particular * {@code TypeNotPresentExceptions}). Allows annotation processing to continue, * assuming that when Class attribute values are not resolvable the annotation - * should effectively disappear. + * should effectively be ignored. * * @author Phillip Webb + * @author Sam Brannen * @since 5.2 */ enum IntrospectionFailureLogger { @@ -51,13 +52,24 @@ enum IntrospectionFailureLogger { public void log(String message) { getLogger().info(message); } + }, + + WARN { + @Override + public boolean isEnabled() { + return getLogger().isWarnEnabled(); + } + @Override + public void log(String message) { + getLogger().warn(message); + } }; private static @Nullable Log logger; - void log(String message, @Nullable Object source, Exception ex) { + void log(String message, @Nullable Object source, Throwable ex) { String on = (source != null ? " on " + source : ""); log(message + on + ": " + ex); } 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 653f1f2feb0..e9602d45237 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 @@ -112,7 +112,7 @@ class AttributeMethodsTests { ClassValue annotation = mockAnnotation(ClassValue.class); given(annotation.value()).willThrow(TypeNotPresentException.class); AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); - assertThat(attributes.canLoad(annotation)).isFalse(); + assertThat(attributes.canLoad(annotation, getClass())).isFalse(); } @Test @@ -121,7 +121,7 @@ class AttributeMethodsTests { ClassValue annotation = mock(); given(annotation.value()).willReturn((Class) InputStream.class); AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); - assertThat(attributes.canLoad(annotation)).isTrue(); + assertThat(attributes.canLoad(annotation, getClass())).isTrue(); } @Test