Browse Source

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
pull/34532/merge
Sam Brannen 2 weeks ago
parent
commit
62d09be2ae
  1. 10
      spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java
  2. 2
      spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java
  3. 10
      spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java
  4. 16
      spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java
  5. 4
      spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java

10
spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java

@ -79,7 +79,7 @@ final class AnnotationTypeMappings {
private void addAllMappings(Class<? extends Annotation> annotationType, private void addAllMappings(Class<? extends Annotation> annotationType,
Set<Class<? extends Annotation>> visitedAnnotationTypes) { Set<Class<? extends Annotation>> visitedAnnotationTypes) {
Deque<AnnotationTypeMapping> queue = new ArrayDeque<>(); Deque<AnnotationTypeMapping> queue = new ArrayDeque<>();
addIfPossible(queue, null, annotationType, null, visitedAnnotationTypes); addIfPossible(queue, null, annotationType, null, false, visitedAnnotationTypes);
while (!queue.isEmpty()) { while (!queue.isEmpty()) {
AnnotationTypeMapping mapping = queue.removeFirst(); AnnotationTypeMapping mapping = queue.removeFirst();
this.mappings.add(mapping); this.mappings.add(mapping);
@ -109,12 +109,12 @@ final class AnnotationTypeMappings {
} }
private void addIfPossible(Deque<AnnotationTypeMapping> queue, AnnotationTypeMapping source, Annotation ann) { private void addIfPossible(Deque<AnnotationTypeMapping> 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<AnnotationTypeMapping> queue, @Nullable AnnotationTypeMapping source, private void addIfPossible(Deque<AnnotationTypeMapping> queue, @Nullable AnnotationTypeMapping source,
Class<? extends Annotation> annotationType, @Nullable Annotation ann, Class<? extends Annotation> annotationType, @Nullable Annotation ann,
Set<Class<? extends Annotation>> visitedAnnotationTypes) { boolean meta, Set<Class<? extends Annotation>> visitedAnnotationTypes) {
try { try {
queue.addLast(new AnnotationTypeMapping(source, annotationType, ann, visitedAnnotationTypes)); queue.addLast(new AnnotationTypeMapping(source, annotationType, ann, visitedAnnotationTypes));
@ -122,8 +122,8 @@ final class AnnotationTypeMappings {
catch (Exception ex) { catch (Exception ex) {
AnnotationUtils.rethrowAnnotationConfigurationException(ex); AnnotationUtils.rethrowAnnotationConfigurationException(ex);
if (failureLogger.isEnabled()) { if (failureLogger.isEnabled()) {
failureLogger.log("Failed to introspect meta-annotation " + annotationType.getName(), failureLogger.log("Failed to introspect " + (meta ? "meta-annotation @" : "annotation @") +
(source != null ? source.getAnnotationType() : null), ex); annotationType.getName(), (source != null ? source.getAnnotationType() : null), ex);
} }
} }
} }

2
spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java

@ -443,7 +443,7 @@ abstract class AnnotationsScanner {
Annotation annotation = annotations[i]; Annotation annotation = annotations[i];
//noinspection DataFlowIssue //noinspection DataFlowIssue
if (isIgnorable(annotation.annotationType()) || if (isIgnorable(annotation.annotationType()) ||
!AttributeMethods.forAnnotationType(annotation.annotationType()).canLoad(annotation)) { !AttributeMethods.forAnnotationType(annotation.annotationType()).canLoad(annotation, source)) {
annotations[i] = null; annotations[i] = null;
} }
else { else {

10
spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java

@ -17,6 +17,7 @@
package org.springframework.core.annotation; package org.springframework.core.annotation;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
@ -38,6 +39,8 @@ import org.springframework.util.ReflectionUtils;
*/ */
final class AttributeMethods { final class AttributeMethods {
private static final IntrospectionFailureLogger failureLogger = IntrospectionFailureLogger.WARN;
static final AttributeMethods NONE = new AttributeMethods(null, new Method[0]); static final AttributeMethods NONE = new AttributeMethods(null, new Method[0]);
static final Map<Class<? extends Annotation>, AttributeMethods> cache = new ConcurrentReferenceHashMap<>(); static final Map<Class<? extends Annotation>, AttributeMethods> cache = new ConcurrentReferenceHashMap<>();
@ -91,10 +94,11 @@ final class AttributeMethods {
* exceptions for {@code Class} values (instead of the more typical early * exceptions for {@code Class} values (instead of the more typical early
* {@code Class.getAnnotations() failure} on a regular JVM). * {@code Class.getAnnotations() failure} on a regular JVM).
* @param annotation the annotation to check * @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 * @return {@code true} if all values are present
* @see #validate(Annotation) * @see #validate(Annotation)
*/ */
boolean canLoad(Annotation annotation) { boolean canLoad(Annotation annotation, AnnotatedElement source) {
assertAnnotation(annotation); assertAnnotation(annotation);
for (int i = 0; i < size(); i++) { for (int i = 0; i < size(); i++) {
if (canThrowTypeNotPresentException(i)) { if (canThrowTypeNotPresentException(i)) {
@ -107,6 +111,10 @@ final class AttributeMethods {
} }
catch (Throwable ex) { catch (Throwable ex) {
// TypeNotPresentException etc. -> annotation type not actually loadable. // TypeNotPresentException etc. -> annotation type not actually loadable.
if (failureLogger.isEnabled()) {
failureLogger.log("Failed to introspect meta-annotation @" +
getName(annotation.annotationType()), source, ex);
}
return false; return false;
} }
} }

16
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 * Log facade used to handle annotation introspection failures (in particular
* {@code TypeNotPresentExceptions}). Allows annotation processing to continue, * {@code TypeNotPresentExceptions}). Allows annotation processing to continue,
* assuming that when Class attribute values are not resolvable the annotation * assuming that when Class attribute values are not resolvable the annotation
* should effectively disappear. * should effectively be ignored.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Sam Brannen
* @since 5.2 * @since 5.2
*/ */
enum IntrospectionFailureLogger { enum IntrospectionFailureLogger {
@ -51,13 +52,24 @@ enum IntrospectionFailureLogger {
public void log(String message) { public void log(String message) {
getLogger().info(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; 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 : ""); String on = (source != null ? " on " + source : "");
log(message + on + ": " + ex); log(message + on + ": " + ex);
} }

4
spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java

@ -112,7 +112,7 @@ class AttributeMethodsTests {
ClassValue annotation = mockAnnotation(ClassValue.class); ClassValue annotation = mockAnnotation(ClassValue.class);
given(annotation.value()).willThrow(TypeNotPresentException.class); given(annotation.value()).willThrow(TypeNotPresentException.class);
AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType());
assertThat(attributes.canLoad(annotation)).isFalse(); assertThat(attributes.canLoad(annotation, getClass())).isFalse();
} }
@Test @Test
@ -121,7 +121,7 @@ class AttributeMethodsTests {
ClassValue annotation = mock(); ClassValue annotation = mock();
given(annotation.value()).willReturn((Class) InputStream.class); given(annotation.value()).willReturn((Class) InputStream.class);
AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType());
assertThat(attributes.canLoad(annotation)).isTrue(); assertThat(attributes.canLoad(annotation, getClass())).isTrue();
} }
@Test @Test

Loading…
Cancel
Save