Class ComponentScanner

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

@Internal public class ComponentScanner extends Object
Component scanner to find and register components.

A scan runs in two phases (decision P-07). The discovery phase walks every requested package, registers a bean definition for each component class, and only collects @Configuration classes, in discovery order. The assembly phase then instantiates each collected configuration class and its @Bean products. So no configuration class or @Bean product is created before the last definition of the requested packages is registered, and a product that needs a @Service loads whatever order the JAR or directory lists the two classes in (C51). This mirrors Spring, which registers every bean definition, @Bean methods included, before it creates singletons. Between two @Bean products the order is still the discovery order: one product that needs another product declared later is a documented limitation of registerSingleton (WR-02), unchanged here.

A configuration class that fails to link while the assembly phase creates it (a LinkageError: a failing static initializer, a @Bean signature naming a class of a missing optional plugin) is skipped exactly like a class that fails to load during discovery: a WARNING naming it, and the class counted in the package's one SEVERE summary, which is therefore emitted after the assembly phase. The rest of the module still loads.

A configuration class the container already created when the assembly phase reaches it (a configuration class assembled before it needed it, or the same class discovered through two overlapping requested packages) is reused, not constructed again; its @Bean methods run once.

  • Field Details

  • Constructor Details

  • Method Details

    • scanPackages

      public void scanPackages(String... basePackages)
      Scan packages for components: first discover every requested package, registering the component definitions it holds, then create the @Configuration classes found, in discovery order, together with their @Bean products.
      Parameters:
      basePackages - packages to scan
    • scanPackage

      public void scanPackage(String basePackage)
      Scan a single package for components; the same two phases as scanPackages(java.lang.String...).
      Parameters:
      basePackage - package to scan
    • discoverPackage

      private void discoverPackage(String basePackage, List<Class<?>> configurationClasses)
      The discovery phase for one package: registers its component definitions and appends its @Configuration classes to configurationClasses, creating none of them.
      Parameters:
      basePackage - package to scan
      configurationClasses - collects the configuration classes found, in discovery order
    • scanJar

      private void scanJar(URL resource, String basePackage, ClassLoader classLoader, List<Class<?>> configurationClasses)
      Scan inside a JAR file for class files.
    • scanDirectory

      private void scanDirectory(File directory, String packageName, ClassLoader classLoader, String moduleName, List<Class<?>> configurationClasses)
      Scan directory for class files.
      Parameters:
      directory - the directory to scan
      packageName - the dotted package name for directory, growing with each recursive descent into a subdirectory
      classLoader - the class loader to load discovered classes with
      moduleName - the D-19 diagnostic identifier for this scan -- fixed at the original basePackage for the whole recursion, deliberately NOT the per-recursion packageName above, so a class three subdirectories deep is still attributed to the same module scanPackages' finally block will emit a summary for
      configurationClasses - collects the configuration classes found, in discovery order
    • processClass

      private void processClass(Class<?> clazz, List<Class<?>> configurationClasses)
      Process a class for component annotations.

      Propagation rule (D-25), stated once here rather than duplicated at each per-class call site in scanJar/scanDirectory: ContainerException is the single type that propagates unconditionally out of this method and aborts the module's scan -- every other exception thrown while registering an individual class is logged and that one class is skipped, and the rest of the package still registers. One type name is the whole rule, so no allowlist has to be kept in sync. The two deliberate ContainerException throws today are the @Final contract violation below and a malformed @AliasFor declaration surfaced by MergedAnnotationResolver during the same scan; neither may ever be caught by a blanket handler.

      A @Configuration class is only collected here; scanPackages(java.lang.String...) creates it and its @Bean products after every requested package has been discovered (P-07). A ContainerException from that later step still aborts the scan the same way.

      Parameters:
      clazz - the discovered class
      configurationClasses - collects the configuration classes found, in discovery order
    • shouldRegister

      private boolean shouldRegister(Class<?> clazz)
      Check if a class should be registered based on @ConditionalOnConfig.

      Delegates to ConditionalRegistrationEvaluator, the single shared implementation of this decision (D-17). This is the only call site of that evaluator in src/main (verified: ListenerManager and CommandManager have no package-scan overload that consults it), so this scanner is the one place the annotation needs to be honoured correctly.

      Parameters:
      clazz - the class to check
      Returns:
      true if the class should be registered
    • isComponent

      private boolean isComponent(Class<?> clazz)
      Check if class is a component.
    • hasComponentAnnotation

      private boolean hasComponentAnnotation(Class<?> clazz)
      Check if class has a meta-component annotation, anywhere on its whole annotation tree.

      Widened from the previous hand-written implementation (D-03, 03-02), which inspected only the annotations declared directly on the class and only one meta-annotation level deep, so a stereotype annotation composed two levels above @Component was not recognised. MergedAnnotationResolver.isPresent(java.lang.Class<?>, java.lang.Class<? extends java.lang.annotation.Annotation>) walks the full annotation tree, so it now is. This is intentional -- a composed stereotype annotation exists precisely to be recognised -- and is recorded against COMPATIBILITY.md's behaviour-change criterion by 03-10.

      Exception: a UltiToolsPlugin subclass is never treated as a component here, regardless of what its annotation tree resolves to. @UltiToolsModule composes @Configuration, which is itself meta-annotated @Component -- so an unqualified whole-tree walk would also match every module's own main class. That class is already constructed by PluginManager *before* this scan runs (its `plugin.yml` metadata has to be read first), and PluginManager registers it as a singleton right *after* the scan, under its raw Class.getSimpleName() as the bean name. ComponentScanner's own bean-naming convention decapitalizes that same name, so the two registrations never collide, and treating the module main class as a scannable @Component would register a *second* bean definition for it under the decapitalized name -- which preInstantiateSingletons would then construct via reflection, re-running the module's entire no-arg constructor (`plugin.yml` re-parse, resource re-copy, a second config-entity registration under an orphaned plugin instance) a second time. Excluded here rather than in isComponent(java.lang.Class<?>), whose own four-way disjunction stays untouched (D-03 scope).

    • registerComponent

      private void registerComponent(Class<?> clazz)
      Register a component class.

      For a class carrying @CmdExecutor, the @CmdTarget class-versus-method composition is checked here - pure reflection, before registerBeanDefinition, and inside this method's own try/catch - because this is the isolation primitive: a check that lived in processClass (no try/catch, aborts the whole scan) or in the executor's own constructor (runs inside preInstantiateSingletons, no per-bean isolation, would fail the whole plugin) would take down every command in the module instead of just this one class. See T-01-01b in this plan's threat model and D-03.

    • assembleConfiguration

      private void assembleConfiguration(Class<?> configurationClass, String basePackage)
      The assembly phase for one configuration class: registerConfiguration(Class), with the per-class isolation the discovery walk gives a class that fails to load (D-25, D-26). A LinkageError skips this class only: the same WARNING, and the class recorded for its package's D-19 summary. Any other failure keeps its existing handling.
      Parameters:
      configurationClass - the configuration class to create
      basePackage - the requested package it was discovered in
    • registerConfiguration

      private void registerConfiguration(Class<?> clazz)
      Register a configuration class: create it and run its @Bean methods, or reuse the instance the container already holds.

      A @Configuration class is also a component definition, so the container can create it before the assembly phase reaches it: when a configuration class assembled earlier needs it, directly or through a service. A class discovered once per requested package, when the packages overlap, reaches this method twice. In both cases the instance the container already holds under the class's bean name is the one every holder has and the one closed with the container, so it is reused: constructing another would run the constructor and @PostConstruct again, replace the instance its earlier holders keep, and leave the first one never destroyed. Its @Bean methods run once per instance, recorded by the container.

    • processBeanMethod

      private void processBeanMethod(Object configInstance, Method method)
      Process @Bean method.

      The bean name is derived from @Bean's name()/value() (D-06) via resolveBeanNames(Method), falling back to the method's own name exactly as before this attribute took effect. The first resolved name is registered through SimpleContainer.registerSingleton(String, Object), which fully assembles the instance (D-14); any remaining names are bound as aliases to that same, already assembled reference via the package-visible SimpleContainer.addSingleton(String, Object) -- deliberately not a second registerSingleton call per name, which after D-14 would re-run autowiring and @PostConstruct once per alias on what should be one bean.

    • resolveBeanNames

      private String[] resolveBeanNames(Method method)
      Resolve the effective bean-name array for a @Bean method (D-06): name() if non-empty, else value() if non-empty, else the method's own name as a single-element array. name() and value() are mutual aliases -- declaring both with different content is a malformed declaration and hard-fails naming the method and both declared values, reusing the same ContainerException.malformedAliasFor(java.lang.Class<? extends java.lang.annotation.Annotation>, java.lang.String, java.lang.String) factory (and therefore the same message shape) plan 03-01 built for malformed @AliasFor declarations, even though @Bean itself deliberately does not declare a real @AliasFor between its two attributes (see this method's own class's scope boundary). Declaring both with identical content is legal. Every resolved element must be non-blank -- a blank or whitespace-only element is also a malformed declaration, because a name that cannot name anything is not a usable third state between "declared" and "absent" (D-02). No element is normalized: two names differing only by Unicode normalization form are two distinct declared names, decided purely by String.equals.
      Parameters:
      method - the @Bean-annotated factory method
      Returns:
      the resolved name array; index 0 is the registered bean name, the rest are aliases
    • getBeanName

      private String getBeanName(Class<?> clazz)
      Get bean name from class.