Class ComponentScanner
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 Summary
Fields -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionprivate voidassembleConfiguration(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).private voiddiscoverPackage(String basePackage, List<Class<?>> configurationClasses) The discovery phase for one package: registers its component definitions and appends its@Configurationclasses toconfigurationClasses, creating none of them.private StringgetBeanName(Class<?> clazz) Get bean name from class.private booleanhasComponentAnnotation(Class<?> clazz) Check if class has a meta-component annotation, anywhere on its whole annotation tree.private booleanisComponent(Class<?> clazz) Check if class is a component.private voidprocessBeanMethod(Object configInstance, Method method) Process @Bean method.private voidprocessClass(Class<?> clazz, List<Class<?>> configurationClasses) Process a class for component annotations.private voidregisterComponent(Class<?> clazz) Register a component class.private voidregisterConfiguration(Class<?> clazz) Register a configuration class: create it and run its@Beanmethods, or reuse the instance the container already holds.private String[]resolveBeanNames(Method method) Resolve the effective bean-name array for a@Beanmethod (D-06):name()if non-empty, elsevalue()if non-empty, else the method's own name as a single-element array.private voidscanDirectory(File directory, String packageName, ClassLoader classLoader, String moduleName, List<Class<?>> configurationClasses) Scan directory for class files.private voidscanJar(URL resource, String basePackage, ClassLoader classLoader, List<Class<?>> configurationClasses) Scan inside a JAR file for class files.voidscanPackage(String basePackage) Scan a single package for components; the same two phases asscanPackages(java.lang.String...).voidscanPackages(String... basePackages) Scan packages for components: first discover every requested package, registering the component definitions it holds, then create the@Configurationclasses found, in discovery order, together with their@Beanproducts.private booleanshouldRegister(Class<?> clazz) Check if a class should be registered based on @ConditionalOnConfig.
-
Field Details
-
LOGGER
-
container
-
-
Constructor Details
-
ComponentScanner
-
-
Method Details
-
scanPackages
Scan packages for components: first discover every requested package, registering the component definitions it holds, then create the@Configurationclasses found, in discovery order, together with their@Beanproducts.- Parameters:
basePackages- packages to scan
-
scanPackage
Scan a single package for components; the same two phases asscanPackages(java.lang.String...).- Parameters:
basePackage- package to scan
-
discoverPackage
The discovery phase for one package: registers its component definitions and appends its@Configurationclasses toconfigurationClasses, creating none of them.- Parameters:
basePackage- package to scanconfigurationClasses- 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 scanpackageName- the dotted package name fordirectory, growing with each recursive descent into a subdirectoryclassLoader- the class loader to load discovered classes withmoduleName- the D-19 diagnostic identifier for this scan -- fixed at the originalbasePackagefor the whole recursion, deliberately NOT the per-recursionpackageNameabove, so a class three subdirectories deep is still attributed to the same modulescanPackages'finallyblock will emit a summary forconfigurationClasses- collects the configuration classes found, in discovery order
-
processClass
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:ContainerExceptionis 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 deliberateContainerExceptionthrows today are the@Finalcontract violation below and a malformed@AliasFordeclaration surfaced byMergedAnnotationResolverduring the same scan; neither may ever be caught by a blanket handler.A
@Configurationclass is only collected here;scanPackages(java.lang.String...)creates it and its@Beanproducts after every requested package has been discovered (P-07). AContainerExceptionfrom that later step still aborts the scan the same way.- Parameters:
clazz- the discovered classconfigurationClasses- collects the configuration classes found, in discovery order
-
shouldRegister
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 insrc/main(verified:ListenerManagerandCommandManagerhave 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
Check if class is a component. -
hasComponentAnnotation
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
@Componentwas 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 againstCOMPATIBILITY.md's behaviour-change criterion by 03-10.Exception: a
UltiToolsPluginsubclass is never treated as a component here, regardless of what its annotation tree resolves to.@UltiToolsModulecomposes@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 byPluginManager*before* this scan runs (its `plugin.yml` metadata has to be read first), andPluginManagerregisters it as a singleton right *after* the scan, under its rawClass.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@Componentwould register a *second* bean definition for it under the decapitalized name -- whichpreInstantiateSingletonswould 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 inisComponent(java.lang.Class<?>), whose own four-way disjunction stays untouched (D-03 scope). -
registerComponent
Register a component class.For a class carrying
@CmdExecutor, the@CmdTargetclass-versus-method composition is checked here - pure reflection, beforeregisterBeanDefinition, and inside this method's own try/catch - because this is the isolation primitive: a check that lived inprocessClass(no try/catch, aborts the whole scan) or in the executor's own constructor (runs insidepreInstantiateSingletons, 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
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). ALinkageErrorskips this class only: the sameWARNING, and the class recorded for its package's D-19 summary. Any other failure keeps its existing handling.- Parameters:
configurationClass- the configuration class to createbasePackage- the requested package it was discovered in
-
registerConfiguration
Register a configuration class: create it and run its@Beanmethods, or reuse the instance the container already holds.A
@Configurationclass 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@PostConstructagain, replace the instance its earlier holders keep, and leave the first one never destroyed. Its@Beanmethods run once per instance, recorded by the container. -
processBeanMethod
Process @Bean method.The bean name is derived from
@Bean'sname()/value()(D-06) viaresolveBeanNames(Method), falling back to the method's own name exactly as before this attribute took effect. The first resolved name is registered throughSimpleContainer.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-visibleSimpleContainer.addSingleton(String, Object)-- deliberately not a secondregisterSingletoncall per name, which after D-14 would re-run autowiring and@PostConstructonce per alias on what should be one bean. -
resolveBeanNames
Resolve the effective bean-name array for a@Beanmethod (D-06):name()if non-empty, elsevalue()if non-empty, else the method's own name as a single-element array.name()andvalue()are mutual aliases -- declaring both with different content is a malformed declaration and hard-fails naming the method and both declared values, reusing the sameContainerException.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@AliasFordeclarations, even though@Beanitself deliberately does not declare a real@AliasForbetween 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 byString.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
Get bean name from class.
-