Class MergedAnnotationResolver

java.lang.Object
com.ultikits.ultitools.context.MergedAnnotationResolver

@Internal public final class MergedAnnotationResolver extends Object
General merged-annotation resolution: a search of the whole annotation tree, with explicit meta-annotation attribute override via AliasFor.

Nearest selection (decision P-10). The occurrence of the requested annotation nearest the class is the one resolved: a directly declared annotation (distance 0) wins over any meta-present one, then meta-annotations are searched breadth-first by distance with declaration order breaking ties, and a superclass is consulted only when the class's own annotation trees hold no occurrence. This follows Spring's @ComponentScan javadoc: "Locally declared @ComponentScan annotations always take precedence over and effectively hide @ComponentScan meta-annotations, which allows explicit local configuration to override configuration that is meta-present." A module that declares both @UltiToolsModule and a direct @ComponentScan (or @EnableAutoRegister) therefore gets the direct one, whatever order the two are written in (C45).

AnnotationUtils.findAnnotation(java.lang.Class<?>, java.lang.Class<T>) only walks one meta-annotation level and never inspects AliasFor at all, which is why @UltiToolsModule.scanBasePackages works today (its call site hand-reads the meta-annotation) while every other @AliasFor attribute on that same annotation is declared but has no reader. Per-site special-casing was rejected (D-01) because it does not remove the defect class, it only moves it to the next attribute nobody wired up yet -- this class is the single general replacement.

Deliberately cacheless. Phase 1 (D-35/D-38) forbids a static Class-keyed cache anywhere in this codebase: it would pin every module's Class objects for the lifetime of the JVM and defeat the plugin class loader's release on module unload (see AnnotationLookupCache's class javadoc for the full reasoning). The walk this class performs runs once per class at scan/load time, not on a per-invocation hot path, so a v1 without a cache is a deliberate choice, not an oversight. Any future cache added here must be instance-scoped and owned by the container/scanner instance that uses it -- never a static field on this class.

This type is @ApiStatus.Internal: module authors need @AliasFor to work, not to call this resolver directly (D-04).

Since:
6.3.0
  • Field Details

  • Constructor Details

    • MergedAnnotationResolver

      private MergedAnnotationResolver()
  • Method Details

    • find

      public static <A extends Annotation> A find(Class<?> clazz, Class<A> annotationType)
      Find an annotation on a class, or on any of its (meta-)annotations, or on its superclass hierarchy -- generalizing AnnotationUtils.findAnnotation from one meta-annotation level to the whole annotation tree, and merging in any @AliasFor override found along the path to the selected occurrence.

      The occurrence nearest the class is selected (see the class javadoc): a directly declared annotationType hides every meta-present one, and the class's own annotations are exhausted before its superclass is searched.

      Type Parameters:
      A - the annotation type
      Parameters:
      clazz - the class to search
      annotationType - the annotation type to find
      Returns:
      the merged annotation if found, or null
    • isPresent

      public static boolean isPresent(Class<?> clazz, Class<? extends Annotation> annotationType)
      Whether annotationType is present anywhere on clazz's annotation tree or superclass hierarchy.
      Parameters:
      clazz - the class to search
      annotationType - the annotation type to look for
      Returns:
      true if present
    • validateAliases

      public static void validateAliases(Class<? extends Annotation> annotationType)
      Validate that every AliasFor-annotated attribute declared on annotationType satisfies Spring's documented Implementation Requirements for an explicit meta-annotation alias, throwing a ContainerException naming the annotation and the offending attribute when it does not (D-02).

      The structural checks land in this class's Task 2 commit; this stub exists so every caller of find(java.lang.Class<?>, java.lang.Class<A>) is already wired against the real method signature before the checks themselves are filled in.

      Parameters:
      annotationType - the annotation type to validate
    • isMetaPresent

      private static boolean isMetaPresent(Class<? extends Annotation> declaring, Class<? extends Annotation> target)
      Whether target is present, directly or transitively, on declaring's own meta-annotation graph -- Spring's "meta-present" requirement for an explicit @AliasFor(annotation = ...). Cycle-guarded the same way findNearest(java.lang.Class<?>, java.lang.Class<? extends java.lang.annotation.Annotation>, java.util.Set<java.lang.Class<? extends java.lang.annotation.Annotation>>) is.
    • isMetaPresent

      private static boolean isMetaPresent(Class<? extends Annotation> current, Class<? extends Annotation> target, Set<Class<? extends Annotation>> visited)
    • findOnClassHierarchy

      private static <A extends Annotation> A findOnClassHierarchy(Class<?> clazz, Class<A> annotationType, Set<Class<? extends Annotation>> visited)
      Nearest selection over the class hierarchy (decision P-10): the class's own annotation trees are searched completely before its superclass is consulted. visited is shared across the whole find(java.lang.Class<?>, java.lang.Class<A>) call so each annotation type is entered at most once; a type already entered on a subclass cannot lead to the target on the superclass either, because its whole subtree was already searched.
    • findNearest

      private static MergedAnnotationResolver.Occurrence findNearest(Class<?> clazz, Class<? extends Annotation> annotationType, Set<Class<? extends Annotation>> visited)
      Breadth-first search of one class's annotation graph for the occurrence of annotationType nearest the class: its directly declared annotations are distance 0, their meta-annotations distance 1, and so on; at equal distance, declaration order breaks the tie. A directly declared target therefore always wins over a meta-present one, whatever order the annotations are written in -- Spring's rule that a locally declared annotation "always take[s] precedence over and effectively hide[s]" a meta-present one (@ComponentScan javadoc).

      visited is shared across the whole find(java.lang.Class<?>, java.lang.Class<A>) call (not reset per branch) so a cyclic meta-annotation graph terminates (T-03-03): once an annotation type has been enqueued anywhere in this call, it is never enqueued again. Every annotation type the search enters is passed to validateAliases(java.lang.Class<? extends java.lang.annotation.Annotation>), as the depth-first walk did.

      Returns:
      the nearest occurrence with its parent chain, or null if this class's own annotation graph holds none
    • enqueue

      private static void enqueue(Deque<MergedAnnotationResolver.Occurrence> queue, Annotation annotation, MergedAnnotationResolver.Occurrence parent, Set<Class<? extends Annotation>> visited)
    • merge

      private static <A extends Annotation> A merge(Annotation found, Class<A> annotationType, List<Annotation> ancestors)
      Compute the merged attribute view of found (the located instance of annotationType) by walking the selected path top-down (decision P-10): from the annotation declared on the class, through every intermediate meta-annotation, down to found. Each level's candidate values are computed by collectOverrideCandidates(java.util.List<java.lang.annotation.Annotation>, int, java.util.List<java.util.Map<java.lang.String, java.util.List<com.ultikits.ultitools.context.MergedAnnotationResolver.Candidate>>>) from the levels above it, so a value set on an outer annotation travels through every intermediate @AliasFor to the final target (C46): an attribute aliasing @UltiToolsModule.scanBasePackages reaches @ComponentScan.basePackages, because scanBasePackages itself aliases it.

      Every candidate records the annotation instance that declared it and that instance's distance from the class. For each attribute of found the rule is (C47):

      • if one annotation instance supplies two different values -- through a mutual alias, or through two routes that reach the same final attribute -- the declaration contradicts itself and is refused with a ContainerException naming that instance's annotation;
      • otherwise the value declared by the instance nearest the class wins. Values from different instances never conflict: an outer override beats a value an inner meta-annotation declares.
      This is Spring's @AliasFor contract: an explicit meta-annotation attribute override wins over the meta-annotation's own value, the lower level (closer to the annotated element) wins, and only an attribute and its alias declared with different values in one annotation is an error.
    • resolveNearest

      private static Object resolveNearest(Class<? extends Annotation> annotationType, String attribute, List<MergedAnnotationResolver.Candidate> candidates)
      Apply the per-instance conflict rule and nearest-wins rule (see merge(java.lang.annotation.Annotation, java.lang.Class<A>, java.util.List<java.lang.annotation.Annotation>)) to one attribute's candidates. Each level of the path is exactly one annotation instance, so the candidates' distance identifies their declaring instance.
    • selfContradiction

      private static ContainerException selfContradiction(Class<? extends Annotation> annotationType, String attribute, MergedAnnotationResolver.Candidate conflicting, List<MergedAnnotationResolver.Candidate> candidates)
    • collectOverrideCandidates

      private static Map<String,List<MergedAnnotationResolver.Candidate>> collectOverrideCandidates(List<Annotation> path, int level, List<Map<String,List<MergedAnnotationResolver.Candidate>>> outerLevels)
      Collect the candidate values for every attribute of the annotation at path[level], given the already-computed candidates of every level above it (outerLevels, indexed by distance from the class):
      1. Forwarded. For each outer level, each attribute declaring @AliasFor(annotation = <this level's type>) forwards all of its own candidates to the aliased attribute here. A forwarded candidate keeps the instance that originally declared it -- an intermediate annotation only relays it.
      2. Own. Each of this annotation's attributes that declares @AliasFor and whose value differs from its declared default is a candidate for itself, declared by this instance. (Attributes taking part in no alias never need a candidate: the merged view falls back to the found instance for them.)
      3. Mirrored. Two attributes of this annotation that alias each other (a within-annotation mutual alias) share one candidate list, so foo=x with bar=y surfaces as two different values for the same attribute.
      Candidates are only collected here, never reconciled: a list may hold values from several instances. merge(java.lang.annotation.Annotation, java.lang.Class<A>, java.util.List<java.lang.annotation.Annotation>) applies the rule once, at the found annotation -- two different values from one instance are refused, otherwise the instance nearest the class wins -- which is Spring's @AliasFor contract (explicit meta-annotation overrides, transitive aliases, lower level wins, same-annotation contradiction is an error).
    • addAllDistinct

      private static void addAllDistinct(List<MergedAnnotationResolver.Candidate> target, List<MergedAnnotationResolver.Candidate> source)
    • addCandidateIfNonDefault

      private static void addCandidateIfNonDefault(Map<String,List<MergedAnnotationResolver.Candidate>> out, Method sourceMethod, Annotation sourceInstance, int distance)
    • resolveAttributeName

      private static String resolveAttributeName(AliasFor aliasFor, Method declaringMethod)
    • findAttributeMethod

      static Method findAttributeMethod(Class<? extends Annotation> type, String name)
    • invokeAttribute

      private static Object invokeAttribute(Method method, Annotation instance)
    • isJavaLangAnnotation

      private static boolean isJavaLangAnnotation(Class<? extends Annotation> type)
    • createProxy

      private static <A extends Annotation> A createProxy(A original, Class<A> annotationType, Map<String,Object> overrides)