Spring · 04

Boot Auto-Configuration & Startup

How a three-line main class becomes a running web service: starters shape the classpath, @SpringBootApplication scans your code and imports ~150 conditional configurations, @Conditional* annotations choose which survive, and SpringApplication.run() drives startup to a ready embedded Tomcat. Includes writing your own starter and reading the conditions report.

13
Chapters
8
@Conditional kinds
10
Startup phases
2
Runnable demos
01
Chapter One · The Problem

Why Spring Boot Exists

Before Boot, a Spring MVC + JPA service started with an afternoon of plumbing: pick compatible versions of 15 libraries, write a web.xml or WebApplicationInitializer, declare a DataSource, an EntityManagerFactory, a TransactionManager, a ViewResolver, a Jackson converter, then package a WAR and deploy it to a Tomcat someone else had installed. None of it was business logic and all of it was the same in every project.

Before — hand-wired Spring MVC + JPA (abridged)
@Configuration @EnableWebMvc @EnableTransactionManagement @ComponentScan("com.acme") public class AppConfig implements WebMvcConfigurer { @Bean public DataSource dataSource() { HikariConfig c = new HikariConfig(); c.setJdbcUrl("jdbc:postgresql://db/app"); c.setUsername("app"); c.setPassword("secret"); return new HikariDataSource(c); } @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource ds) { LocalContainerEntityManagerFactoryBean f = new LocalContainerEntityManagerFactoryBean(); f.setDataSource(ds); f.setPackagesToScan("com.acme.domain"); f.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); return f; } @Bean public PlatformTransactionManager transactionManager(EntityManagerFactory emf) { return new JpaTransactionManager(emf); } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new MappingJackson2HttpMessageConverter()); } // + web.xml / WebApplicationInitializer, + Tomcat install, + WAR build ... }
After — Spring Boot 3
@SpringBootApplication public class AcmeApplication { public static void main(String[] args) { SpringApplication.run(AcmeApplication.class, args); } }
application.yml
spring: datasource: url: jdbc:postgresql://db/app username: app password: from-env-or-vault

Same running system: a pooled DataSource, an EntityManagerFactory, a transaction manager, Jackson-enabled MVC and an embedded Tomcat on port 8080. The code did not vanish — the same @Bean definitions now live inside the Boot jars as auto-configuration classes, and each one only activates when the classpath and your configuration say it should. Boot is a very large, very well-organised library of conditional @Configuration classes plus a launcher.

Boot = your code + a catalogue of conditional configurations
You write@SpringBootApplicationyour @Service / @Controlleryour @Repositoryapplication.ymlpom: 3 startersBoot supplies, only if conditions holdDataSourceAutoConfigurationHikariCP poolHibernateJpaAutoConfigurationEntityManagerFactoryTransactionAutoConfigurationtx manager + @TransactionalWebMvcAutoConfigurationMVC defaultsJacksonAutoConfigurationObjectMapperServletWebServerFactoryAutoCfgembedded TomcatYour beans always win: an auto-config bean is skipped when you define your own of the same type (back-off, Chapter 6).
Analogy: Boot is a hotel with a thousand pre-made rooms, each locked until it is needed. The classpath hands over keys (“we have JDBC and Hibernate on the shelf”), the property files say what you ordered, and a room opens only when its condition is met — unless you brought your own furniture (your bean), in which case the hotel’s version stays in storage.
What Boot is not
  • Not a code generator — nothing is written into your source tree.
  • Not a different framework — everything it does can be done with plain Spring @Configuration classes; you could copy any auto-config into your own project.
  • Not magic that runs at build time — all the decisions happen at startup by evaluating conditions against the classpath and Environment.
02
Chapter Two · Mental Model

Classpath In, Beans Out

You declare dependencies through starters (spring-boot-starter-web, -data-jpa)
▶
Maven/Gradle pulls a curated, version-aligned set of jars onto the classpath. The classpath is now the first input signal.
@SpringBootApplication enables @EnableAutoConfiguration
▶
A DeferredImportSelector reads a list of ~150 candidate configuration classes from META-INF/spring/...AutoConfiguration.imports in every jar.
Candidates are filtered by @Conditional* annotations
▶
Is DispatcherServlet on the classpath? Is there already a DataSource bean? Is spring.datasource.url set? Non-matching classes are dropped.
Surviving configurations register their @Bean definitions after your own
▶
Because yours are already registered, @ConditionalOnMissingBean backs off wherever you customised.
SpringApplication.run() refreshes the context
▶
Beans are created; the embedded web server is built during refresh and started at the very end; runners execute; the app is "ready".
Environment properties feed everything
▶
@ConfigurationProperties classes bind spring.datasource.*, server.*, … from yaml, env vars, CLI args (precedence order).
The decision pipeline Boot runs at every startup
classpathjars present?imports file~150 candidates@Conditionalfilter eachregistrybean definitionsrefresh()instantiate beansyour @Configuration + @Componentregistered FIRST (scan)already in registryEnvironmentyaml + env + CLI args + system props -> conditions and @ConfigurationProperties

Keep one picture: candidates in, conditions as filters, bean definitions out. When something is auto-configured that you did not expect — or something you expected is missing — the answer is always in one of three places: the classpath, the Environment, or a bean you already declared. Chapter 10 shows how to read the report that tells you which.

03
Chapter Three · Dependencies

Starters & Dependency Management

A starter is a dependency descriptor with no code of its own. spring-boot-starter-web is an empty jar whose POM pulls in spring-boot-starter (core + logging + YAML), spring-webmvc, spring-boot-starter-json (Jackson) and spring-boot-starter-tomcat. Adding it changes the classpath, and the classpath changes what auto-configuration activates. That is the entire trick.

StarterBrings (main pieces)Auto-config it unlocks
spring-boot-starter-webSpring MVC, Jackson, embedded TomcatDispatcherServlet, MVC defaults, Jackson ObjectMapper, Tomcat factory
spring-boot-starter-data-jpaSpring Data JPA, Hibernate, HikariCP, spring-txDataSource, EntityManagerFactory, JpaTransactionManager, repositories scanning
spring-boot-starter-securitySpring SecurityDefault SecurityFilterChain (everything authenticated, form login + basic), generated password
spring-boot-starter-actuatorMicrometer, actuator endpoints/actuator/health, metrics registry
spring-boot-starter-validationHibernate ValidatorMethod validation post-processor, @Valid support
spring-boot-starter-aopAspectJ weaver library@EnableAspectJAutoProxy with CGLIB
spring-boot-starter-testJUnit, AssertJ, Mockito, Spring TestTest slices, @SpringBootTest

The other half is dependency management. spring-boot-starter-parent (or the spring-boot-dependencies BOM imported into your own parent) pins hundreds of library versions that are known to work together, which is why you write jackson-databind without a version. It also configures the compiler (-parameters), resource filtering and the repackage plugin.

pom.xml — the whole build for a REST + JPA service
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> </parent> <properties><java.version>21</java.version></properties> <dependencies> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency> <dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><scope>runtime</scope></dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <!-- repackage into an executable jar --> </plugin> </plugins> </build>
Swapping Tomcat for Jetty — starters are just dependencies
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency>
⚠ Production gotchas
  • Overriding a managed version carelessly. Setting <jackson.version> in properties is supported; hard-coding a single jackson-databind version produces NoSuchMethodError / NoClassDefFoundError at runtime because the other Jackson modules stay on the BOM version. Override the BOM property so all modules move together.
  • Two starters bring competing implementations (Tomcat + Jetty, Logback + Log4j2). Boot picks one deterministically, but logs a warning (Class path contains multiple SLF4J providers); exclude the one you do not want.
  • A starter on the classpath you do not need activates its auto-config. Adding spring-boot-starter-security to experiment locks every endpoint behind a generated password: Using generated security password: 7f3e... in the log.
  • JDBC driver missing with starter-data-jpa: Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. Add a driver (or h2) or set spring.datasource.url.
  • Spring Boot 3 requires Java 17+ and Jakarta EE 10: javax.* imports from older libraries cause ClassNotFoundException: javax.servlet...; migrate dependencies, do not mix.
04
Chapter Four · Decomposition

@SpringBootApplication Taken Apart

@SpringBootApplication is not a special keyword — it is three ordinary annotations in a trench coat. Writing it out shows exactly where each behaviour comes from.

What @SpringBootApplication means
@SpringBootConfiguration // = @Configuration: this class can declare @Bean methods @EnableAutoConfiguration // the catalogue of conditional configs (Chapter 5) @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { ... }
PartDoesDetail worth knowing
@SpringBootConfigurationMarks the class as a configuration source (specialisation of @Configuration)Test slices search upward from the test package for exactly one @SpringBootConfiguration — why a missing/duplicate one gives Unable to find a @SpringBootConfiguration
@ComponentScanScans the annotated class’s package and sub-packages for @Component/@Service/@Controller/@ConfigurationNo basePackages given → package of the class. A main class in the default package scans the whole classpath (never do it)
TypeExcludeFilterLets test slices (@WebMvcTest) exclude componentsWhy @WebMvcTest does not load your services
AutoConfigurationExcludeFilterKeeps auto-configuration classes out of your scanThey must only load via @EnableAutoConfiguration so ordering and conditions apply
@EnableAutoConfiguration@AutoConfigurationPackage + @Import(AutoConfigurationImportSelector.class)@AutoConfigurationPackage records your package so JPA/Mongo repositories and entities are found by default
The three annotations behind @SpringBootApplication
@SpringBootApplication@SpringBootConfiguration= @Configuration@ComponentScanyour package tree@EnableAutoConfigurationconditional catalogue@AutoConfigurationPackage@Import(ImportSelector)scan roots at AcmeApplication package:com.acme.orders.AcmeApplication -> scans com.acme.orders.** -> NOT com.acme.billing (sibling!)
Customising the pieces explicitly
@SpringBootApplication( scanBasePackages = {"com.acme.orders", "com.acme.shared"}, exclude = {DataSourceAutoConfiguration.class} // or excludeName = "..." ) public class AcmeApplication { ... }
⚠ Production gotchas
  • Main class in a sub-package (com.acme.app.Main) while services live in com.acme.orders → nothing found: NoSuchBeanDefinitionException: No qualifying bean of type 'com.acme.orders.OrderService'. Put the main class in the root package.
  • Third-party library packages are not scanned. A @Component inside a jar under org.thirdparty is ignored. @Import the class or expose it through its own auto-configuration.
  • Scanning picks up an auto-config class you wrote in your own tree: it ignores ordering/conditions and may register too early. Put auto-configs in a separate module, listed only in the imports file.
  • exclude naming a class not on the classpath fails with The following classes could not be excluded because they are not auto-configuration classes — use excludeName for optional types.
  • @SpringBootApplication on a class that also declares heavy @Bean methods makes tests slower and sliced tests load them; keep it empty and move beans to @Configuration classes.
05
Chapter Five · The Catalogue

AutoConfiguration.imports & Deferred Import

@EnableAutoConfiguration imports AutoConfigurationImportSelector. It is a DeferredImportSelector, which means Spring processes it last, after every one of your @Configuration classes and component scans has registered its beans. That ordering is what makes back-off possible: by the time Boot’s defaults are evaluated, your definitions are already in the registry.

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (inside spring-boot-autoconfigure.jar)
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration org.springframework.boot.autoconfigure.aop.AopAutoConfiguration org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration # ... well over a hundred more
AutoConfigurationImportSelector.selectImports() in five steps
1 loadread every *.AutoConfiguration.imports on the classpath (ImportCandidates)2 deduperemove duplicates, apply exclude / spring.autoconfigure.exclude3 pre-filterAutoConfigurationImportFilter drops classes failing OnClass/OnBean/OnWebApplication4 sort@AutoConfiguration(before/after) + @AutoConfigureOrder5 importprocessed like a normal @Configuration; class and @Bean @Conditional* run again
A real auto-configuration class (pattern, abridged from Boot’s own source)
@AutoConfiguration(after = DataSourceAutoConfiguration.class) // ordering hint, not a dependency @ConditionalOnClass({JdbcTemplate.class, PlatformTransactionManager.class}) // are the libs present? @ConditionalOnSingleCandidate(DataSource.class) // exactly one (or one @Primary) DataSource @EnableConfigurationProperties(JdbcProperties.class) public class JdbcTemplateAutoConfiguration { @Bean @Primary @ConditionalOnMissingBean(JdbcOperations.class) // BACK-OFF: your JdbcTemplate wins JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) { JdbcTemplate t = new JdbcTemplate(dataSource); t.setFetchSize(properties.getTemplate().getFetchSize()); return t; } }
MechanismUsed forBoot 3.x status
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsListing auto-configuration classesThe way since 2.7; spring.factories auto-config entries are no longer read in Boot 3
META-INF/spring.factoriesOther extension points: ApplicationContextInitializer, ApplicationListener, EnvironmentPostProcessor, FailureAnalyzer, …Still supported for these
@AutoConfigurationReplaces @Configuration on an auto-config class (proxyBeanMethods=false built in)Use for all new starters
@AutoConfigureBefore/After/OrderOrdering between auto-configs (not vs your configs — those are always first)Attributes before/after on @AutoConfiguration
Excluding an auto-configuration, three ways
// 1. on the annotation @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 2. by name (safe if the class may be absent) @EnableAutoConfiguration(excludeName = "org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration") // 3. externally - no code change, per environment: // spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
⚠ Production gotchas
  • Ordering confusion. @AutoConfigureAfter only orders among auto-configurations. Your own configs are always processed before; you cannot make a user config run after an auto-config by annotating it.
  • Putting the imports file in the wrong directory (META-INF/spring.imports, or under resources/ root without META-INF/spring/): silently ignored. Name must be exactly org.springframework.boot.autoconfigure.AutoConfiguration.imports.
  • Using spring.factories with EnableAutoConfiguration key in a Boot 3 starter — no longer honoured, the starter works on Boot 2.x and does nothing on 3.x.
  • Component-scanning an auto-config bypasses deferred import. Keep the package outside the app’s scan root.
  • @Bean methods in auto-config should be non-public-class-friendly: use @AutoConfiguration (no CGLIB subclass) and do not call one @Bean method from another — pass dependencies as parameters.
06
Chapter Six · Conditions

@Conditional Family & Back-Off

A @Conditional is a predicate on a configuration class or @Bean method: Condition.matches(ConditionContext, AnnotatedTypeMetadata). Boot ships a family of ready-made ones in org.springframework.boot.autoconfigure.condition. They are evaluated in two phases: PARSE_CONFIGURATION (can this config class even be read — used by class-level class/property checks) and REGISTER_BEAN (when each @Bean is about to be registered — used by @ConditionalOnBean because it must see what is already defined).

AnnotationMatches whenTypical use
@ConditionalOnClass / @ConditionalOnMissingClassA class is / is not on the classpath (read via ASM metadata, so the class is never loaded — no NoClassDefFoundError)Activate Jackson config only if Jackson exists
@ConditionalOnBean / @ConditionalOnMissingBeanA bean of the type/name is / is not already registeredBack-off: provide a default unless the user did
@ConditionalOnSingleCandidateExactly one bean of the type (or one @Primary)JdbcTemplate needs one DataSource
@ConditionalOnPropertyA property exists / equals a value (havingValue, matchIfMissing)Feature flags: acme.cache.enabled=true
@ConditionalOnWebApplicationServlet / reactive / any web contextMVC config only in servlet apps
@ConditionalOnResourceA file/classpath resource existsLoad banner.txt, logback.xml
@ConditionalOnExpressionA SpEL expression is trueComposite flags (use sparingly)
@ConditionalOnCloudPlatform / @ConditionalOnJavaKubernetes/Cloud Foundry / JVM versionPlatform-specific tuning
@ProfileA Spring profile is active (a plain @Conditional)Environment-specific beans
Back-off in practice
// ---- Boot's auto-configuration (simplified) ---- @AutoConfiguration @ConditionalOnClass(ObjectMapper.class) public class JacksonAutoConfiguration { @Bean @Primary @ConditionalOnMissingBean // default: type inferred from return type public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { return builder.createXmlMapper(false).build(); } } // ---- Your code: just declare one. Boot's version silently disappears ---- @Configuration class JsonConfig { @Bean ObjectMapper objectMapper() { return JsonMapper.builder() .addModule(new JavaTimeModule()) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .build(); } }
How @ConditionalOnMissingBean backs off
WITHOUT your beanJacksonAutoConfiguration.jacksonObjectMapper@ConditionalOnMissingBean(ObjectMapper): registry has none-> condition TRUE -> Boot bean registeredWITH your beanJsonConfig.objectMapper (yours)registered first (component scan before deferred import)jacksonObjectMapper SKIPPEDcondition FALSE -> Boot backs offOrder of evaluation is the whole mechanism: user beans -> @Conditional on auto-config -> registry
Custom conditions: feature flag with matchIfMissing
@Configuration @ConditionalOnProperty(prefix = "acme.audit", name = "enabled", havingValue = "true", matchIfMissing = false) class AuditConfig { @Bean AuditPublisher auditPublisher(KafkaTemplate<String, String> kafka) { return new KafkaAuditPublisher(kafka); } } // A hand-written Condition for anything the stock ones cannot express: class OnLinuxCondition implements Condition { @Override public boolean matches(ConditionContext ctx, AnnotatedTypeMetadata md) { return ctx.getEnvironment().getProperty("os.name", "").toLowerCase().contains("linux"); } } @Bean @Conditional(OnLinuxCondition.class) NativeLibLoader loader() { return new NativeLibLoader(); }
⚠ Production gotchas
  • @ConditionalOnBean in your own configuration is order-dependent. It only sees beans registered before that point; it is designed for auto-configuration. In user code it works or not depending on scan order — use @Import/explicit ordering or restructure.
  • @ConditionalOnMissingBean without a type infers it from the @Bean return type. If you return a supertype or interface your own subtype bean may not count as "present". Declare the specific return type.
  • @ConditionalOnProperty with matchIfMissing forgotten: flag absent → condition false → bean silently missing → NoSuchBeanDefinitionException far away. Decide the default explicitly.
  • @ConditionalOnClass placed on a @Bean method with a class argument that is absent: the method signature itself loads the class → NoClassDefFoundError. Put it on the class (or a nested config class) so ASM handles it.
  • Your @Bean of the same name as an auto-config bean overrides or conflicts: Boot 2.1+ disallows bean definition overriding by default → BeanDefinitionOverrideException: Invalid bean definition with name 'x'. Rely on back-off by type instead of fighting names.
07
Chapter Seven · Startup

The SpringApplication.run() Sequence

SpringApplication.run(...) is a fixed choreography. Knowing the order tells you where each hook fits: where application.yml is read, when beans exist, when the port opens, and when your CommandLineRunner executes.

SpringApplication.run() in order (top to bottom)
new SpringApplication()deduce web type, load initializers + listeners from spring.factoriesrun(): startingApplicationStartingEvent, stopwatch, headlessprepareEnvironmentproperty sources, config data (yaml), profiles -> ApplicationEnvironmentPreparedEventprintBanner / createContextAnnotationConfigServletWebServerApplicationContextprepareContextinitializers, main class registered; ContextInitialized + Prepared eventsrefreshContext (refresh())ConfigurationClassPostProcessor, auto-config, beans, web server createdfinishRefreshSmartLifecycle: web server binds port, WebServerInitializedEvent, ContextRefreshedEventApplicationStartedEventliveness = CORRECTcallRunnersApplicationRunner, CommandLineRunner (ordered)ApplicationReadyEventreadiness = ACCEPTING_TRAFFIC; app serves traffic
PhaseWhat happensHook you can use
ConstructReads META-INF/spring.factories for ApplicationContextInitializer and ApplicationListener; picks SERVLET / REACTIVE / NONE by classpathnew SpringApplicationBuilder(...), setBannerMode, setLazyInitialization
EnvironmentApplicationEnvironmentPreparedEvent: EnvironmentPostProcessors add property sources; ConfigDataEnvironmentPostProcessor loads application.yml, profile files, spring.config.importEnvironmentPostProcessor (register in spring.factories)
Context creationContext type chosen from web type; bean factory created; sources (your main class) registered as a bean definitionApplicationContextInitializer
refresh()BeanFactoryPostProcessors run (ConfigurationClassPostProcessor parses configs, scans, imports auto-config); BeanPostProcessors registered; onRefresh() creates the web server; singletons instantiatedBeanFactoryPostProcessor, BeanPostProcessor, @PostConstruct
finishRefreshSmartLifecycle beans start — including WebServerStartStopLifecycle, which starts the web server and publishes ServletWebServerInitializedEvent; then ContextRefreshedEventSmartLifecycle, @EventListener
RunnersApplicationRunner / CommandLineRunner beans in @Order order; port is already openWarm-up, seed data (keep short!)
ReadyApplicationReadyEvent, then ReadinessState.ACCEPTING_TRAFFIC@EventListener(ApplicationReadyEvent.class)
FailureAny exception: ApplicationFailedEvent, FailureAnalyzers turn it into the readable "APPLICATION FAILED TO START" block, context closed, exit code 1FailureAnalyzer, ExitCodeGenerator
Hooks at each stage, with the guarantees they give you
@SpringBootApplication public class AcmeApplication { public static void main(String[] args) { SpringApplication app = new SpringApplication(AcmeApplication.class); app.setBannerMode(Banner.Mode.OFF); app.addListeners((ApplicationListener<ApplicationEnvironmentPreparedEvent>) e -> System.out.println("env ready, profiles=" + Arrays.toString(e.getEnvironment().getActiveProfiles()))); app.run(args); } } @Component @Order(1) class WarmUp implements ApplicationRunner { // context refreshed, port OPEN, readiness not yet ACCEPTING @Override public void run(ApplicationArguments args) { /* prime caches */ } } @Component class Ready { @EventListener(ApplicationReadyEvent.class) // everything is up void onReady() { log.info("serving traffic"); } }
Typical startup log, mapped to phases
:: Spring Boot :: (v3.3.5) <- banner (printed after environment prepared) Starting AcmeApplication using Java 21 on host with PID 4242 <- prepareContext (logStartupInfo) The following 1 profile is active: "prod" <- prepareContext (profile info) Bootstrapping Spring Data JPA repositories in DEFAULT mode. <- inside refresh() (auto-config) Tomcat initialized with port 8080 (http) <- onRefresh(): server CREATED, port not yet bound HikariPool-1 - Start completed. <- singleton instantiation Tomcat started on port 8080 (http) with context path '/' <- finishRefresh: server STARTED Started AcmeApplication in 4.318 seconds (process running for 4.9) <- before runners and ReadyEvent
⚠ Production gotchas
  • Doing slow work in a CommandLineRunner delays ApplicationReadyEvent and therefore readiness. The port is open but Kubernetes readiness is not yet ACCEPTING_TRAFFIC — that is correct behaviour; a runner that takes 5 minutes makes the pod flap. Use async warm-up or a startup probe.
  • Reading properties in a static initializer or constructor of main: the Environment does not exist yet. Use an EnvironmentPostProcessor if you must contribute properties early.
  • "Started in 4.3 seconds" is misleading. It is measured to just before runners. Time-to-ready must be measured separately (ApplicationReadyEvent timestamp).
  • Failure analysis blocks hide the root cause: run with --debug or look for Caused by: lines; for the common ones the analyzer prints Description and Action (e.g. Web server failed to start. Port 8080 was already in use.).
  • spring.main.lazy-initialization=true speeds startup but moves bean creation errors to the first request and changes @PostConstruct timing. Use consciously.
08
Chapter Eight · The Server

Embedded Tomcat & the Executable Jar

"Embedded" means your application contains the servlet container as a library, instead of being deployed into one. ServletWebServerFactoryAutoConfiguration imports a factory for whichever server is on the classpath (TomcatServletWebServerFactory, Jetty or Undertow). During onRefresh() the context asks that factory to create a WebServer and hands it a list of ServletContextInitializer beans — among them the DispatcherServletRegistrationBean that registers the Spring MVC DispatcherServlet at / and every Filter bean you declared.

What lives inside one Boot process
JVM process (java -jar app.jar)Embedded Tomcat (org.apache.catalina.startup.Tomcat)Connector :8080NIO, max-connections 8192Executor threadsmin-spare 10 .. max 200StandardContext ""one webapp, path /Filter chainCharacterEncoding, Security, ...DispatcherServletmapped to /Controllers, servicesordinary Spring beans
Property (default)Controls
server.port (8080)Listen port; 0 picks a random free port (tests)
server.tomcat.threads.max (200)Max request-processing threads — the real concurrency limit for blocking MVC
server.tomcat.threads.min-spare (10)Threads kept ready
server.tomcat.accept-count (100)Queue of pending connections once all connections/threads are busy
server.tomcat.max-connections (8192)Max concurrently open connections the server accepts
server.shutdown=graceful (default immediate)Stop accepting, let in-flight requests finish up to spring.lifecycle.timeout-per-shutdown-phase (30s)
spring.threads.virtual.enabled=trueBoot 3.2+ on Java 21: request handling on virtual threads
The executable ("fat") jar produced by spring-boot-maven-plugin
app.jar |-- META-INF/MANIFEST.MF | Main-Class: org.springframework.boot.loader.launch.JarLauncher (Boot 3.2+; earlier: ...loader.JarLauncher) | Start-Class: com.acme.AcmeApplication |-- org/springframework/boot/loader/... <- the launcher classes |-- BOOT-INF/classes/ <- YOUR compiled code + application.yml |-- BOOT-INF/lib/ <- every dependency jar, nested and uncompressed |-- BOOT-INF/classpath.idx, layers.idx <- ordering + Docker layer hints

java -jar app.jar starts the JarLauncher, which builds a classloader that can read nested jars, then calls Start-Class.main(). This is why ClassLoader.getResource on a file in BOOT-INF/classes works but new File("classpath:...") does not — there is no file on disk, only an entry inside a jar.

⚠ Production gotchas
  • Web server failed to start. Port 8080 was already in use. Another process (or a previous instance) holds the port. Find it with ss -ltnp | grep 8080; or use server.port=0 in tests.
  • Thread-pool starvation: all 200 Tomcat threads blocked on a slow downstream. Symptom: latency spikes while CPU is idle; tomcat.threads.busy equals tomcat.threads.config.max in metrics. Fix with timeouts/bulkheads, not just a bigger pool.
  • Dropped requests on deploy without server.shutdown=graceful and a matching Kubernetes preStop / terminationGracePeriodSeconds larger than the shutdown timeout.
  • Both spring-boot-starter-tomcat and -jetty on the classpath: Boot chooses by condition order (Tomcat wins) — your Jetty config is ignored. Exclude Tomcat explicitly.
  • Deploying the fat jar as a WAR into an external Tomcat needs SpringBootServletInitializer, packaging war and the Tomcat starter as provided; otherwise two containers start and fight over ports.
  • Reading files from src/main/resources as File works in the IDE and fails in the jar (FileNotFoundException: class path resource ... cannot be resolved to absolute file path). Use ClassPathResource.getInputStream().
09
Chapter Nine · Extending Boot

Writing Your Own Starter

The best way to understand auto-configuration is to write one. We will build acme-spring-boot-starter: a tiny library that offers a GreetingService configured by acme.greeting.* properties, which backs off if the application declares its own, and can be switched off with a flag. Convention: third-party starters are named acme-spring-boot-starter (the spring-boot-starter-* prefix is reserved for Spring).

Starter + autoconfigure module layout
your-appdepends on ONE artifactacme-spring-boot-starterempty jar: only a pomacme-spring-boot-autoconfigurecode + imports fileAcmeAutoConfigurationAcmeProperties (record)GreetingServiceMETA-INF/spring/...importsWhy two modules?starter = what users depend on (curated transitive deps)autoconfigure = the code; optional deps stay optional(small libraries may merge both into one artifact)
AcmeProperties.java — typed, validated configuration
@ConfigurationProperties(prefix = "acme.greeting") public record AcmeProperties( @DefaultValue("Hello") String salutation, @DefaultValue("!") String punctuation, @DefaultValue("true") boolean enabled) { }
GreetingService.java — the library’s actual feature
public class GreetingService { private final String salutation; private final String punctuation; public GreetingService(String salutation, String punctuation) { this.salutation = salutation; this.punctuation = punctuation; } public String greet(String name) { return salutation + ", " + name + punctuation; } }
AcmeAutoConfiguration.java — where the decisions are made
@AutoConfiguration @ConditionalOnClass(GreetingService.class) // the library is present @ConditionalOnProperty(prefix = "acme.greeting", name = "enabled", matchIfMissing = true) @EnableConfigurationProperties(AcmeProperties.class) // registers + binds the record public class AcmeAutoConfiguration { @Bean @ConditionalOnMissingBean // user's GreetingService wins GreetingService greetingService(AcmeProperties props) { return new GreetingService(props.salutation(), props.punctuation()); } }
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.acme.greeting.autoconfigure.AcmeAutoConfiguration
AcmeAutoConfigurationTest.java — ApplicationContextRunner, no full app needed
class AcmeAutoConfigurationTest { private final ApplicationContextRunner runner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(AcmeAutoConfiguration.class)); @Test void createsDefaultBean() { runner.run(ctx -> { assertThat(ctx).hasSingleBean(GreetingService.class); assertThat(ctx.getBean(GreetingService.class).greet("Ana")).isEqualTo("Hello, Ana!"); }); } @Test void bindsProperties() { runner.withPropertyValues("acme.greeting.salutation=Hola", "acme.greeting.punctuation=.") .run(ctx -> assertThat(ctx.getBean(GreetingService.class).greet("Ana")).isEqualTo("Hola, Ana.")); } @Test void backsOffWhenUserDefinesBean() { runner.withUserConfiguration(Custom.class).run(ctx -> { assertThat(ctx).hasSingleBean(GreetingService.class); assertThat(ctx.getBean(GreetingService.class).greet("Ana")).isEqualTo("Yo, Ana?"); }); } @Test void disabledByFlag() { runner.withPropertyValues("acme.greeting.enabled=false") .run(ctx -> assertThat(ctx).doesNotHaveBean(GreetingService.class)); } @Configuration(proxyBeanMethods = false) static class Custom { @Bean GreetingService mine() { return new GreetingService("Yo", "?"); } } }

Usage from an application is then one dependency and zero code:

application.yml of a consumer
acme: greeting: salutation: Welcome
⚠ Production gotchas
  • Forgetting @ConditionalOnMissingBean — users cannot customise; they get NoUniqueBeanDefinitionException or have to exclude your auto-config.
  • Hard dependency on optional libraries. Mark them <optional>true</optional> in the autoconfigure module and guard the config with @ConditionalOnClass, or the starter forces them on every consumer.
  • Eager side effects in @Bean methods (opening connections) — auto-config runs for every consumer at startup; make beans lazy-friendly and fail with a clear message.
  • Property names without metadata: add spring-boot-configuration-processor (annotation processor) so IDEs autocomplete acme.greeting.* and flag typos.
  • Testing only with @SpringBootTest is slow and hides condition bugs. ApplicationContextRunner starts a minimal context per test and lets you vary classpath (withClassLoader(new FilteredClassLoader(...))) and properties.
10
Chapter Ten · Diagnosis

Debugging Auto-Configuration

Almost every Boot mystery is a condition answering differently than you assumed. Boot records every condition outcome in a ConditionEvaluationReport; you only need to ask for it.

ToolHowGives you
--debug / debug=truejava -jar app.jar --debugLogs the full CONDITIONS EVALUATION REPORT at startup (also enables DEBUG for core loggers)
/actuator/conditionsexpose management.endpoints.web.exposure.include=conditionsThe same report as JSON on a running app
/actuator/beansexpose beansEvery bean, its type, scope, dependencies
/actuator/configprops, /actuator/envexpose themBound property values and which property source supplied them
logging.level.org.springframework.boot.autoconfigure=DEBUGpropertyPer-condition trace without the whole debug firehose
ApplicationContextRunnerin testsReproduce a condition outcome in isolation
BufferingApplicationStartup + /actuator/startupapp.setApplicationStartup(new BufferingApplicationStartup(2048))Timing per startup step — find the slow bean
Excerpt of the report printed by --debug
============================ CONDITIONS EVALUATION REPORT ============================ Positive matches: ----------------- DispatcherServletAutoConfiguration matched: - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition) - found 'session' scope (OnWebApplicationCondition) JacksonAutoConfiguration.JacksonObjectMapperConfiguration#jacksonObjectMapper: - @ConditionalOnMissingBean (types: com.fasterxml.jackson.databind.ObjectMapper; SearchStrategy: all) did not find any beans (OnBeanCondition) Negative matches: ----------------- ActiveMQAutoConfiguration: Did not match: - @ConditionalOnClass did not find required class 'jakarta.jms.ConnectionFactory' (OnClassCondition) DataSourceAutoConfiguration.EmbeddedDatabaseConfiguration: Did not match: - EmbeddedDataSource did not find an embedded database (EmbeddedDataSourceCondition) Exclusions: ----------- org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration Unconditional classes: ---------------------- org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration

Read it with two questions. For something unexpectedly present, search Positive matches for its auto-config and read which classpath/property fact satisfied it, then remove the cause (starter, property) or add exclude. For something unexpectedly absent, search Negative matches for it and read the first line under Did not match — that is the failing condition, and it names the missing class, bean or property.

A failure analyzer in action
*************************** APPLICATION FAILED TO START *************************** Description: Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. Reason: Failed to determine a suitable driver class Action: Consider the following: If you want an embedded database (H2, HSQL or Derby), please put it on the classpath. If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).
Peeking at the same data from code
@Component class ConditionsDump implements ApplicationRunner { private final ConfigurableApplicationContext ctx; ConditionsDump(ConfigurableApplicationContext ctx) { this.ctx = ctx; } @Override public void run(ApplicationArguments args) { ConditionEvaluationReport report = ConditionEvaluationReport.get(ctx.getBeanFactory()); report.getConditionAndOutcomesBySource().forEach((source, outcomes) -> { if (source.contains("Kafka")) System.out.println(source + " -> " + (outcomes.isFullMatch() ? "MATCH" : "NO")); }); } }
⚠ Production gotchas
  • Exposing /actuator/conditions, /beans, /env publicly leaks classpath and configuration (values are sanitized, but structure is not). Put the management port behind the network boundary or secure the endpoints.
  • Unexpected Spring Security lock-down after adding a dependency (transitive spring-security-* from another starter): search Positive matches for SecurityAutoConfiguration / UserDetailsServiceAutoConfiguration.
  • "Why is my bean not injected?" is usually not Boot at all: check component-scan reach (Chapter 4) before diving into conditions.
  • Using --debug in production floods logs; use the actuator endpoint or run once locally with the production classpath.
  • Slow startup: do not guess — record with BufferingApplicationStartup and sort steps by duration; typical culprits are Hibernate schema validation, huge component scans and eager remote calls in @PostConstruct.
11
Chapter Eleven · Build It Yourself

A Mini Auto-Configuration Engine

Here is the whole idea in about 80 lines of plain Java — no Spring: user beans registered first, an ordered list of auto-config candidates, each with an ordered list of conditions (the first failing one is recorded, as the report does), and a registry that supports missing bean of type. Compile and run it.

MiniAutoConfig.java
import java.util.*; import java.util.function.Predicate; import java.util.function.Supplier; public class MiniAutoConfig { /** The registry plays the role of the BeanDefinitionRegistry. */ static class Registry { final Map<String, Object> beans = new LinkedHashMap<>(); final Map<String, String> props; final List<String> report = new ArrayList<>(); Registry(Map<String, String> props) { this.props = props; } boolean hasBeanOfType(Class<?> t) { return beans.values().stream().anyMatch(t::isInstance); } } /** One @Bean method plus its @Conditional* checks (evaluated in order, first failure wins). */ record Candidate(String name, Class<?> type, Supplier<Object> factory, Cond... conds) {} record Cond(String label, Predicate<Registry> test) {} static Cond onClass(String fqcn) { return new Cond("@ConditionalOnClass(" + fqcn + ")", r -> { try { Class.forName(fqcn); return true; } catch (ClassNotFoundException e) { return false; } }); } static Cond onMissingBean(Class<?> t) { return new Cond("@ConditionalOnMissingBean(" + t.getSimpleName() + ")", r -> !r.hasBeanOfType(t)); } static Cond onProperty(String key, String having) { return new Cond("@ConditionalOnProperty(" + key + "=" + having + ")", r -> having.equals(r.props.getOrDefault(key, "true"))); } interface DataSource { String url(); } record HikariDs(String url) implements DataSource {} record H2Ds(String url) implements DataSource {} record JdbcTemplate(DataSource ds) {} record ObjectMapper(String mode) {} public static void main(String[] args) { // 1) "user" beans are registered FIRST (component scan runs before auto-config is deferred-imported) Registry reg = new Registry(Map.of("app.jackson.enabled", "true", "spring.jdbc.template", "true")); reg.beans.put("myCustomDs", new H2Ds("jdbc:h2:mem:custom")); // 2) the "AutoConfiguration.imports" list, in order List<Candidate> autoConfig = List.of( new Candidate("hikariDataSource", DataSource.class, () -> new HikariDs("jdbc:postgresql://db/app"), onClass("java.sql.Connection"), onMissingBean(DataSource.class)), new Candidate("jdbcTemplate", JdbcTemplate.class, () -> new JdbcTemplate(null), onClass("java.sql.Connection"), onProperty("spring.jdbc.template", "true"), onMissingBean(JdbcTemplate.class)), new Candidate("objectMapper", ObjectMapper.class, () -> new ObjectMapper("default"), onClass("com.fasterxml.jackson.databind.ObjectMapper"), onMissingBean(ObjectMapper.class)), new Candidate("kafkaTemplate", Object.class, Object::new, onClass("org.apache.kafka.clients.producer.Producer")) ); // 3) evaluate for (Candidate c : autoConfig) { String failed = null; for (Cond cond : c.conds()) { if (!cond.test().test(reg)) { failed = cond.label(); break; } } if (failed == null) { reg.beans.put(c.name(), c.factory().get()); reg.report.add(" Positive: " + c.name()); } else { reg.report.add(" Negative: " + c.name() + " -> did not match: " + failed); } } System.out.println("CONDITIONS EVALUATION REPORT"); reg.report.forEach(System.out::println); System.out.println("\nFINAL BEANS"); reg.beans.forEach((k, v) -> System.out.println(" " + k + " = " + v)); } }
javac MiniAutoConfig.java && java MiniAutoConfig
CONDITIONS EVALUATION REPORT Negative: hikariDataSource -> did not match: @ConditionalOnMissingBean(DataSource) Positive: jdbcTemplate Negative: objectMapper -> did not match: @ConditionalOnClass(com.fasterxml.jackson.databind.ObjectMapper) Negative: kafkaTemplate -> did not match: @ConditionalOnClass(org.apache.kafka.clients.producer.Producer) FINAL BEANS myCustomDs = H2Ds[url=jdbc:h2:mem:custom] jdbcTemplate = JdbcTemplate[ds=null]

Reading the output as Boot would: the app declared its own DataSource (myCustomDs), so Boot’s Hikari one backed off; the JdbcTemplate candidate passed all three conditions (class present, flag true, none defined); Jackson was absent from this bare javac classpath, and Kafka likewise. This is literally the structure of the real report, minus ASM metadata, ordering and bean-factory machinery. Note the evaluation order inside a candidate — Boot also orders cheap checks (OnClass) before costly ones (OnBean).

Try this
  • Add reg.beans.put("jdbcTemplate", ...) before evaluation and confirm the template candidate flips to Negative.
  • Put onClass("java.sql.Connection") last in a candidate’s list: the report now blames a different condition when several fail. Boot evaluates and reports in declaration order too, so keep specific conditions first.
12
Chapter Twelve · Interviews

Interview Angles

  • What does @SpringBootApplication do?
    Meta-annotation for @SpringBootConfiguration (a @Configuration), @ComponentScan (from the class’s package down, with test/auto-config exclude filters) and @EnableAutoConfiguration.
  • How does auto-configuration actually find its classes?
    AutoConfigurationImportSelector (a DeferredImportSelector) reads META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports from all jars, filters/sorts them, and imports each as a normal configuration. In Boot 3 spring.factories is no longer used for this.
  • Why do your own beans override Boot’s defaults?
    Auto-config is imported after user configuration (deferred) and guarded with @ConditionalOnMissingBean; so when yours exists, the condition is false and the default is skipped.
  • Name five @Conditional* annotations and when each is evaluated.
    OnClass, OnMissingBean, OnProperty, OnWebApplication, OnResource. Class-level class/property checks at config parsing; bean-based ones at bean registration (REGISTER_BEAN phase).
  • What is a starter?
    A POM-only artifact that groups compatible dependencies (and often an autoconfigure module). It works because the extra jars on the classpath trigger conditional configuration.
  • Walk through SpringApplication.run().
    Construct (web type, factories) → environment prepared (yaml, profiles) → create context → prepare (initializers) → refresh (config parsing, auto-config, bean creation, server created) → finishRefresh (server started) → Started event → runners → Ready event.
  • When is the HTTP port actually opened?
    Server object is created in onRefresh() but connectors are only bound when WebServerStartStopLifecycle starts in finishRefresh — after all singletons exist.
  • How do you find out why a bean exists or doesn’t?
    --debug or /actuator/conditions and read Positive/Negative matches; /actuator/beans for the resulting graph.
  • How do you disable an auto-configuration?
    exclude/excludeName on the annotation or spring.autoconfigure.exclude; or change the condition input (remove the starter, set the property).
  • How would you swap Tomcat for Jetty or Undertow?
    Exclude spring-boot-starter-tomcat from starter-web and add spring-boot-starter-jetty / -undertow; the factory auto-config switches on the classpath.
  • What is in an executable Boot jar and how does it start?
    BOOT-INF/classes + BOOT-INF/lib nested jars; Main-Class is JarLauncher, Start-Class is your main class.
13
Chapter Thirteen · Review

Summary & Cheat Sheet

StageMechanismWhere to look when wrong
DependenciesStarters + BOM (spring-boot-starter-parent)mvn dependency:tree
Discovery@SpringBootApplication scan from main-class package; imports file for auto-configPackage layout; imports file path/name
Selection@Conditional* on candidates (class, bean, property, web type)--debug report, /actuator/conditions
CustomisationYour beans back off defaults; @ConfigurationProperties tweaks themBean type vs return type; property prefix
StartupSpringApplication.run: env → context → refresh → server start → runners → readyStartup log, BufferingApplicationStartup
PackagingFat jar + JarLauncher; embedded Tomcatjar tf app.jar, server.* properties
∑ Boot internals — what to remember
  • Boot is a catalogue of conditional configurations; starters change the classpath that conditions inspect.
  • @SpringBootApplication = configuration + component scan (your package tree) + auto-configuration.
  • Auto-config classes come from AutoConfiguration.imports, imported after yours via a deferred selector.
  • @ConditionalOnMissingBean is the back-off contract: declare your own bean and Boot’s steps aside.
  • run(): environment first, beans during refresh, server starts at finishRefresh, runners, then Ready.
  • The executable jar is just a launcher + nested jars; Tomcat is a library inside your process.
  • Always debug with the conditions report before changing code.
Related pages