Add custom-starter module: a Boot 4 auto-configuration starter with conditions, metadata and runner tests

Two-jar starter (autoconfigure + starter), a demo app, and 17 captured transcripts
covering AutoConfiguration.imports, @ConditionalOn*, ordering, optional Jackson 3
integration, configuration metadata and the Boot 4 module split.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Uu7q8vPeREyT4218EJPzz1
This commit is contained in:
Claude
2026-09-24 06:53:07 +00:00
parent d6a2e1a5f2
commit c418589251
57 changed files with 1590 additions and 0 deletions
@@ -0,0 +1,6 @@
# The auto-configuration with no properties and no user beans
Masker beans : [masker]
Masker implementation : TailMasker
defaults bound : enabled=true replacement='*' visibleTail=4
mask("4111111111111111") = ************1111
@@ -0,0 +1,9 @@
# masker.* properties, and masker.enabled=false
--- masker.replacement=# masker.visible-tail=2 ---
mask("4111111111111111") = ##############11
--- masker.enabled=false ---
Masker beans : []
MaskerProperties beans : []
@@ -0,0 +1,13 @@
# @ConditionalOnMissingBean: backing off, and the order that decides it
--- user configuration + auto-configuration (the way Boot loads them) ---
Masker beans : [customMasker]
getBean(Masker.class) : mask -> <redacted>
--- the same two classes registered as ordinary configuration, starter first ---
Masker beans : [masker, customMasker]
getBean(Masker.class) : NoUniqueBeanDefinitionException
--- the same two classes, user configuration first ---
Masker beans : [customMasker]
@@ -0,0 +1,11 @@
# Jackson on the classpath: the module is registered and Sensitive is written masked
module beans : [maskerJacksonModule]
JSON : {"card":"************1111"}
--- what Boot recorded for the Jackson auto-configuration ---
com.ankurm.masker.MaskerJacksonAutoConfiguration [matched]
matched: @ConditionalOnClass found required class 'tools.jackson.databind.json.JsonMapper'
matched: @ConditionalOnBean (types: com.ankurm.masker.Masker; SearchStrategy: all) found bean 'masker'
com.ankurm.masker.MaskerJacksonAutoConfiguration#maskerJacksonModule [matched]
matched: @ConditionalOnMissingBean (types: com.ankurm.masker.MaskerJacksonModule; SearchStrategy: all) did not find any beans
@@ -0,0 +1,9 @@
# Jackson hidden with FilteredClassLoader: nothing fails, the integration just is not there
context failed : false
Masker beans : [masker]
module beans : []
--- what Boot recorded for the Jackson auto-configuration ---
com.ankurm.masker.MaskerJacksonAutoConfiguration [not matched]
did not match: @ConditionalOnClass did not find required class 'tools.jackson.databind.json.JsonMapper'
@@ -0,0 +1,17 @@
# @ConditionalOnBean(JsonMapper) with and without an ordering constraint
--- the order Boot evaluates them in ---
com.ankurm.masker.MaskerAutoConfiguration
com.ankurm.traps.EarlyModuleAutoConfiguration
org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration
com.ankurm.traps.OrderedModuleAutoConfiguration
--- the beans each one produced ---
Marker bean created by OrderedModuleAutoConfiguration
--- what Boot recorded ---
com.ankurm.traps.EarlyModuleAutoConfiguration [not matched]
did not match: @ConditionalOnBean (types: tools.jackson.databind.json.JsonMapper; SearchStrategy: all) did not find any beans of type tools.jackson.databind.json.JsonMapper
com.ankurm.traps.OrderedModuleAutoConfiguration [matched]
matched: @ConditionalOnBean (types: tools.jackson.databind.json.JsonMapper; SearchStrategy: all) found bean 'jacksonJsonMapper'
@@ -0,0 +1,12 @@
# @ConditionalOnClass(JsonMapper.class) on a @Bean method, versus on a nested class, with and without Jackson
--- condition on the @Bean method, classpath without any Jackson jar ---
context failed
exception chain: BeanDefinitionStoreException -> IllegalStateException -> NoClassDefFoundError -> ClassNotFoundException
root cause : java.lang.ClassNotFoundException: tools.jackson.databind.module.SimpleModule
--- condition on a nested class, classpath without any Jackson jar ---
context started; nestedModule bean present: false
--- condition on the @Bean method, classpath with Jackson ---
context started; methodLevelModule bean present: true
@@ -0,0 +1,14 @@
# An auto-configuration that is not listed in AutoConfiguration.imports (it is listed in spring.factories, which Boot 4 ignores for this)
--- ApplicationContextRunner: the test names the class itself ---
forgottenMarker bean : true
--- the starter author's application: @SpringBootApplication scanning the package that holds both classes ---
forgottenMarker (@AutoConfiguration) : false
forgottenPlainMarker (@Configuration) : true
--- a consumer application: its own package, the starter on the classpath ---
forgottenMarker (@AutoConfiguration) : false
forgottenPlainMarker (@Configuration) : false
Masker beans : [masker]
@@ -0,0 +1,6 @@
# ImportCandidates: what Boot itself reads from META-INF/spring/...AutoConfiguration.imports
entries under com.ankurm.masker: 2
com.ankurm.masker.MaskerAutoConfiguration
com.ankurm.masker.MaskerJacksonAutoConfiguration
ForgottenAutoConfiguration listed: false
@@ -0,0 +1,17 @@
# An application that adds masker-spring-boot-starter and nothing else
Masker beans : [masker]
MaskerProperties beans : [masker-com.ankurm.masker.MaskerProperties]
Jackson module beans : [maskerJacksonModule]
JSON of a Customer : {"name":"Asha","card":"************1111"}
--- what Boot recorded (the same lines --debug prints) ---
com.ankurm.masker.MaskerAutoConfiguration [matched]
matched: @ConditionalOnProperty (masker.enabled) matched
com.ankurm.masker.MaskerAutoConfiguration#masker [matched]
matched: @ConditionalOnMissingBean (types: com.ankurm.masker.Masker; SearchStrategy: all) did not find any beans
com.ankurm.masker.MaskerJacksonAutoConfiguration [matched]
matched: @ConditionalOnClass found required class 'tools.jackson.databind.json.JsonMapper'
matched: @ConditionalOnBean (types: com.ankurm.masker.Masker; SearchStrategy: all) found bean 'masker'
com.ankurm.masker.MaskerJacksonAutoConfiguration#maskerJacksonModule [matched]
matched: @ConditionalOnMissingBean (types: com.ankurm.masker.MaskerJacksonModule; SearchStrategy: all) did not find any beans
@@ -0,0 +1,16 @@
# The same application configured with masker.* properties, then switched off
--- --masker.replacement=# --masker.visible-tail=6 ---
JSON of a Customer : {"name":"Asha","card":"##########111111"}
--- --masker.enabled=false ---
Masker beans : []
Jackson module beans : []
--- what Boot recorded ---
com.ankurm.masker.MaskerAutoConfiguration [not matched]
did not match: @ConditionalOnProperty (masker.enabled) found different value in property 'enabled'
com.ankurm.masker.MaskerJacksonAutoConfiguration [not matched]
matched: @ConditionalOnClass found required class 'tools.jackson.databind.json.JsonMapper'
did not match: @ConditionalOnBean (types: com.ankurm.masker.Masker; SearchStrategy: all) did not find any beans of type com.ankurm.masker.Masker
@@ -0,0 +1,74 @@
# What is inside the two jars, and what the starter drags in
--- masker-spring-boot-starter-1.0.0.jar ---
META-INF/MANIFEST.MF
META-INF/maven/com.ankurm.masker/masker-spring-boot-starter/pom.xml
META-INF/maven/com.ankurm.masker/masker-spring-boot-starter/pom.properties
--- masker-spring-boot-autoconfigure-1.0.0.jar (META-INF and the classes) ---
META-INF/spring-autoconfigure-metadata.properties
META-INF/additional-spring-configuration-metadata.json
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
META-INF/spring-configuration-metadata.json
com/ankurm/masker/MaskerJacksonAutoConfiguration.class
com/ankurm/masker/Sensitive.class
com/ankurm/masker/Masker.class
com/ankurm/masker/MaskerAutoConfiguration.class
com/ankurm/masker/TailMasker.class
com/ankurm/masker/MaskerJacksonModule.class
com/ankurm/masker/MaskerProperties.class
com/ankurm/masker/MaskerJacksonModule$1.class
--- META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ---
com.ankurm.masker.MaskerAutoConfiguration
com.ankurm.masker.MaskerJacksonAutoConfiguration
--- dependency tree of masker-spring-boot-starter (Jackson is not in it) ---
com.ankurm.masker:masker-spring-boot-starter:jar:1.0.0
+- org.springframework.boot:spring-boot-starter:jar:4.1.1:compile
| +- org.springframework.boot:spring-boot-starter-logging:jar:4.1.1:compile
| | +- ch.qos.logback:logback-classic:jar:1.5.38:compile
| | | +- ch.qos.logback:logback-core:jar:1.5.38:compile
| | | \- org.slf4j:slf4j-api:jar:2.0.18:compile
| | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.25.5:compile
| | | \- org.apache.logging.log4j:log4j-api:jar:2.25.5:compile
| | \- org.slf4j:jul-to-slf4j:jar:2.0.18:compile
| +- org.springframework.boot:spring-boot-autoconfigure:jar:4.1.1:compile
| | \- org.springframework.boot:spring-boot:jar:4.1.1:compile
| | +- org.springframework:spring-core:jar:7.0.9:compile
| | | +- commons-logging:commons-logging:jar:1.3.6:compile
| | | \- org.jspecify:jspecify:jar:1.0.1:compile
| | \- org.springframework:spring-context:jar:7.0.9:compile
| | +- org.springframework:spring-aop:jar:7.0.9:compile
| | +- org.springframework:spring-beans:jar:7.0.9:compile
| | +- org.springframework:spring-expression:jar:7.0.9:compile
| | \- io.micrometer:micrometer-observation:jar:1.17.1:compile
| | \- io.micrometer:micrometer-commons:jar:1.17.1:compile
| +- jakarta.annotation:jakarta.annotation-api:jar:3.0.0:compile
| \- org.yaml:snakeyaml:jar:2.6:compile
\- com.ankurm.masker:masker-spring-boot-autoconfigure:jar:1.0.0:compile
--- dependency tree of masker-spring-boot-autoconfigure (Jackson is there, marked optional in the pom) ---
com.ankurm.masker:masker-spring-boot-autoconfigure:jar:1.0.0
+- org.springframework.boot:spring-boot-autoconfigure:jar:4.1.1:compile
| \- org.springframework.boot:spring-boot:jar:4.1.1:compile
| \- org.springframework:spring-context:jar:7.0.9:compile
| +- org.springframework:spring-aop:jar:7.0.9:compile
| +- org.springframework:spring-beans:jar:7.0.9:compile
| +- org.springframework:spring-expression:jar:7.0.9:compile
| \- io.micrometer:micrometer-observation:jar:1.17.1:compile
| \- io.micrometer:micrometer-commons:jar:1.17.1:compile
\- org.springframework.boot:spring-boot-starter-jackson:jar:4.1.1:compile (optional)
+- org.springframework.boot:spring-boot-starter:jar:4.1.1:compile
| +- org.springframework.boot:spring-boot-starter-logging:jar:4.1.1:compile
| | +- ch.qos.logback:logback-classic:jar:1.5.38:compile
| | | \- ch.qos.logback:logback-core:jar:1.5.38:compile
| | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.25.5:compile
| | | \- org.apache.logging.log4j:log4j-api:jar:2.25.5:compile
| | \- org.slf4j:jul-to-slf4j:jar:2.0.18:compile
| +- jakarta.annotation:jakarta.annotation-api:jar:3.0.0:compile
| \- org.yaml:snakeyaml:jar:2.6:compile
\- org.springframework.boot:spring-boot-jackson:jar:4.1.1:compile (optional)
\- tools.jackson.core:jackson-databind:jar:3.1.5:compile (optional)
+- com.fasterxml.jackson.core:jackson-annotations:jar:2.21:compile (optional)
\- tools.jackson.core:jackson-core:jar:3.1.5:compile (optional)
@@ -0,0 +1,80 @@
# Metadata the two annotation processors write into the auto-configuration jar
--- META-INF/spring-configuration-metadata.json, written by spring-boot-configuration-processor ---
{
"groups": [
{
"name": "masker",
"type": "com.ankurm.masker.MaskerProperties",
"sourceType": "com.ankurm.masker.MaskerProperties"
}
],
"properties": [
{
"name": "masker.enabled",
"type": "java.lang.Boolean",
"description": "whether the starter configures anything at all",
"sourceType": "com.ankurm.masker.MaskerProperties",
"defaultValue": true
},
{
"name": "masker.replacement",
"type": "java.lang.String",
"description": "the text that stands in for each hidden character",
"sourceType": "com.ankurm.masker.MaskerProperties",
"defaultValue": "*"
},
{
"name": "masker.visible-tail",
"type": "java.lang.Integer",
"description": "how many characters at the end of the value stay readable",
"sourceType": "com.ankurm.masker.MaskerProperties",
"defaultValue": 4
}
],
"hints": [
{
"name": "masker.replacement",
"values": [
{
"value": "*",
"description": "Asterisk (the default)."
},
{
"value": "#",
"description": "Hash sign."
},
{
"value": "•",
"description": "Bullet."
}
]
}
],
"ignored": {
"properties": []
}
}
--- META-INF/additional-spring-configuration-metadata.json, hand-written and kept as is ---
{
"hints": [
{
"name": "masker.replacement",
"values": [
{ "value": "*", "description": "Asterisk (the default)." },
{ "value": "#", "description": "Hash sign." },
{ "value": "•", "description": "Bullet." }
]
}
]
}
--- META-INF/spring-autoconfigure-metadata.properties, written by spring-boot-autoconfigure-processor ---
com.ankurm.masker.MaskerAutoConfiguration=
com.ankurm.masker.MaskerJacksonAutoConfiguration=
com.ankurm.masker.MaskerJacksonAutoConfiguration.AutoConfigureAfter=com.ankurm.masker.MaskerAutoConfiguration
com.ankurm.masker.MaskerJacksonAutoConfiguration.ConditionalOnBean=com.ankurm.masker.Masker
com.ankurm.masker.MaskerJacksonAutoConfiguration.ConditionalOnClass=tools.jackson.databind.json.JsonMapper
@@ -0,0 +1,30 @@
# Boot 4.1.1: where the auto-configuration classes a starter refers to now live
--- entries in spring-boot-autoconfigure's own AutoConfiguration.imports ---
12
--- JacksonAutoConfiguration: which jar, which package ---
spring-boot-autoconfigure-4.1.1.jar, classes named JacksonAutoConfiguration: 0
spring-boot-jackson-4.1.1.jar: org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.class
--- what @AutoConfiguration is made of (javap -v, class-level annotations) ---
org.springframework.context.annotation.Configuration(
proxyBeanMethods=false
org.springframework.boot.autoconfigure.AutoConfigureBefore
org.springframework.boot.autoconfigure.AutoConfigureAfter
--- AutoConfigurationExcludeFilter, the filter @SpringBootApplication's scan applies: the classes it tests for (javap -c) ---
ldc #26 // class org/springframework/context/annotation/Configuration
ldc #40 // class org/springframework/boot/autoconfigure/AutoConfiguration
ldc #40 // class org/springframework/boot/autoconfigure/AutoConfiguration
invokestatic #65 // Method org/springframework/boot/context/annotation/ImportCandidates.load:(Ljava/lang/Class;Ljava/lang/ClassLoader;)Lorg/springframework/boot/context/annotation/ImportCandidates;
--- the Boot 3 import, compiled against the 4.1.1 jars ---
masker-spring-boot-autoconfigure/src/broken/OldJacksonImport.java:4: error: package org.springframework.boot.autoconfigure.jackson does not exist
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
^
masker-spring-boot-autoconfigure/src/broken/OldJacksonImport.java:7: error: cannot find symbol
@AutoConfiguration(after = JacksonAutoConfiguration.class)
^
symbol: class JacksonAutoConfiguration
2 errors
@@ -0,0 +1,3 @@
# The processor path with <version>${project.parent.version}</version>, when the module's parent is not Spring Boot's
Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.15.0:compile (default-compile) on project masker-spring-boot-autoconfigure: Resolution of annotationProcessorPath dependencies failed: The following artifacts could not be resolved: org.springframework.boot:spring-boot-configuration-processor:jar:1.0.0 (absent): Could not find artifact org.springframework.boot:spring-boot-configuration-processor:jar:1.0.0 in central (https://repo.maven.apache.org/maven2)
@@ -0,0 +1,10 @@
# @ConditionalOnProperty(prefix = "traps", name = "enabled") with no matchIfMissing
--- no property set ---
Marker beans : 0
com.ankurm.traps.DefaultOffAutoConfiguration [not matched]
did not match: @ConditionalOnProperty (traps.enabled) did not find property 'enabled'
--- traps.enabled=true ---
Marker beans : 1
@@ -0,0 +1,15 @@
# Which 4.1.1 jar holds each Boot class this starter and its tests import (searched in every Boot jar the build resolved)
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/AutoConfiguration.class
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/AutoConfigureAfter.class
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/AutoConfigurations.class
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/condition/ConditionalOnClass.class
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/condition/ConditionalOnBean.class
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.class
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.class
spring-boot-autoconfigure-4.1.1.jar org/springframework/boot/autoconfigure/logging/ConditionEvaluationReportLoggingListener.class
spring-boot-4.1.1.jar org/springframework/boot/context/annotation/ImportCandidates.class
spring-boot-4.1.1.jar org/springframework/boot/context/annotation/Configurations.class
spring-boot-jackson-4.1.1.jar org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.class
spring-boot-test-4.1.1.jar org/springframework/boot/test/context/runner/ApplicationContextRunner.class
spring-boot-test-4.1.1.jar org/springframework/boot/test/context/FilteredClassLoader.class