diff --git a/logback-core/src/main/java/ch/qos/logback/core/joran/spi/NoAutoStartUtil.java b/logback-core/src/main/java/ch/qos/logback/core/joran/spi/NoAutoStartUtil.java index c64019ed29..aaa595cc57 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/joran/spi/NoAutoStartUtil.java +++ b/logback-core/src/main/java/ch/qos/logback/core/joran/spi/NoAutoStartUtil.java @@ -13,6 +13,10 @@ */ package ch.qos.logback.core.joran.spi; + +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.Set; import ch.qos.logback.core.spi.LifeCycle; public class NoAutoStartUtil { @@ -24,13 +28,83 @@ public class NoAutoStartUtil { * @param o * @return true for classes not marked with the NoAutoStart annotation */ - static public boolean notMarkedWithNoAutoStart(Object o) { + public static boolean notMarkedWithNoAutoStart(Object o) { + if (o == null) { + return false; + } Class clazz = o.getClass(); - NoAutoStart a = clazz.getAnnotation(NoAutoStart.class); + NoAutoStart a = findAnnotation(clazz, NoAutoStart.class); return a == null; } - /** + /** + * Find a single {@link Annotation} of {@code annotationType} on the + * supplied {@link Class}, traversing its interfaces, annotations, and + * superclasses if the annotation is not directly present on + * the given class itself. + *

This method explicitly handles class-level annotations which are not + * declared as {@link java.lang.annotation.Inherited inherited} as well + * as meta-annotations and annotations on interfaces. + *

The algorithm operates as follows: + *

    + *
  1. Search for the annotation on the given class and return it if found. + *
  2. Recursively search through all annotations that the given class declares. + *
  3. Recursively search through all interfaces that the given class declares. + *
  4. Recursively search through the superclass hierarchy of the given class. + *
+ *

Note: in this context, the term recursively means that the search + * process continues by returning to step #1 with the current interface, + * annotation, or superclass as the class to look for annotations on. + * @param clazz the class to look for annotations on + * @param annotationType the type of annotation to look for + * @return the first matching annotation, or {@code null} if not found + */ + private static A findAnnotation(Class clazz, Class annotationType) { + return findAnnotation(clazz, annotationType, new HashSet<>()); + } + + /** + * Perform the search algorithm for {@link #findAnnotation(Class, Class)}, + * avoiding endless recursion by tracking which annotations have already + * been visited. + * @param clazz the class to look for annotations on + * @param annotationType the type of annotation to look for + * @param visited the set of annotations that have already been visited + * @return the first matching annotation, or {@code null} if not found + */ + @SuppressWarnings("unchecked") + private static A findAnnotation(Class clazz, Class annotationType, Set visited) { + + Annotation[] anns = clazz.getDeclaredAnnotations(); + for (Annotation ann : anns) { + if (ann.annotationType() == annotationType) { + return (A) ann; + } + } + for (Annotation ann : anns) { + if (visited.add(ann)) { + A annotation = findAnnotation(ann.annotationType(), annotationType, visited); + if (annotation != null) { + return annotation; + } + } + } + + for (Class ifc : clazz.getInterfaces()) { + A annotation = findAnnotation(ifc, annotationType, visited); + if (annotation != null) { + return annotation; + } + } + + Class superclass = clazz.getSuperclass(); + if (superclass == null || Object.class == superclass) { + return null; + } + return findAnnotation(superclass, annotationType, visited); + } + + /** * Is the object a {@link LifeCycle} and is it marked not marked with * the NoAutoStart annotation. * @param o @@ -43,5 +117,4 @@ static public boolean shouldBeStarted(Object o) { } else return false; } - } diff --git a/logback-core/src/test/java/ch/qos/logback/core/joran/spi/NoAutoStartUtilTest.java b/logback-core/src/test/java/ch/qos/logback/core/joran/spi/NoAutoStartUtilTest.java index 4cd995cd89..4a5390a446 100644 --- a/logback-core/src/test/java/ch/qos/logback/core/joran/spi/NoAutoStartUtilTest.java +++ b/logback-core/src/test/java/ch/qos/logback/core/joran/spi/NoAutoStartUtilTest.java @@ -15,6 +15,12 @@ import org.junit.jupiter.api.Test; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -31,4 +37,71 @@ public void markedWithNoAutoStart() { DoNotAutoStart o = new DoNotAutoStart(); assertFalse(NoAutoStartUtil.notMarkedWithNoAutoStart(o)); } + + + + /* + * Annotation declared on implemented interface + */ + @Test + public void noAutoStartOnInterface() { + ComponentWithNoAutoStartOnInterface o = new ComponentWithNoAutoStartOnInterface(); + assertFalse(NoAutoStartUtil.notMarkedWithNoAutoStart(o)); + } + + @NoAutoStart + public interface NoAutoStartInterface { + } + + private static class ComponentWithNoAutoStartOnInterface implements NoAutoStartInterface { + } + + + + /* + * Annotation declared on ancestor + */ + @Test + public void noAutoStartOnAncestor() { + ComponentWithNoAutoStartOnAncestor o = new ComponentWithNoAutoStartOnAncestor(); + assertFalse(NoAutoStartUtil.notMarkedWithNoAutoStart(o)); + } + + private static class ComponentWithNoAutoStartOnAncestor extends DoNotAutoStart { + } + + + + /* + * Annotation declared on interface implemented by an ancestor + */ + @Test + public void noAutoStartOnInterfaceImplementedByAncestor() { + ComponentWithAncestorImplementingInterfaceWithNoAutoStart o = new ComponentWithAncestorImplementingInterfaceWithNoAutoStart(); + assertFalse(NoAutoStartUtil.notMarkedWithNoAutoStart(o)); + } + + private static class ComponentWithAncestorImplementingInterfaceWithNoAutoStart extends ComponentWithNoAutoStartOnInterface { + } + + + + /* + * Custom annotation annotated with @NoAutoStart + */ + @Test + public void noAutoStartAsMetaAnnotation() { + ComponentWithMetaAnnotation o = new ComponentWithMetaAnnotation(); + assertFalse(NoAutoStartUtil.notMarkedWithNoAutoStart(o)); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @NoAutoStart + public @interface MetaNoAutoStart { + } + + @MetaNoAutoStart + private static class ComponentWithMetaAnnotation { + } }