Class ComponentScanner

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

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

  • Constructor Details

  • Method Details

    • scanPackages

      public void scanPackages(String... basePackages)
      Scan packages for components.
      Parameters:
      basePackages - packages to scan
    • scanPackage

      public void scanPackage(String basePackage)
      Scan a single package for components.
      Parameters:
      basePackage - package to scan
    • scanJar

      private void scanJar(URL resource, String basePackage, ClassLoader classLoader)
      Scan inside a JAR file for class files.
    • scanDirectory

      private void scanDirectory(File directory, String packageName, ClassLoader classLoader, String moduleName)
      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 scanPackage's finally block will emit a summary for
    • processClass

      private void processClass(Class<?> clazz)
      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.

    • 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 and registered as a singleton by PluginManager *before* this scan ever runs (its `plugin.yml` metadata has to be read first), 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.

    • registerConfiguration

      private void registerConfiguration(Class<?> clazz)
      Register a configuration class.
    • 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.