Module 37 - Spring Boot
Spring Boot makes Spring-based applications runnable with minimal configuration: Auto-configuration - infers and wires beans based on classpath and properties; Starters - curated dependency groups that pull in everything a feature needs; Profiles - swap configuration between environments without code changes; @ConfigurationProperties - type-safe, validated binding from property files to Java classes; Actuator - production-ready observability endpoints out of the box.
How Auto-Configuration Works
Your application starts
|
v SpringApplication.run()
|
v Load application.properties / environment
|
v @EnableAutoConfiguration (inside @SpringBootApplication)
| reads META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
| each listed class is a @Configuration with conditions
|
v For each auto-configuration class:
| evaluate conditions (@ConditionalOnClass, @ConditionalOnMissingBean, ...)
| if all pass: register the @Bean methods
| if any fail: skip the whole configuration
|
v ApplicationContext ready
Key Conditional Annotations
// Bean created only if a class is on the classpath
// (e.g. JPA auto-config runs only when Hibernate is present)
@ConditionalOnClass(DataSource.class)
// Bean created only if no other bean of that type exists yet
// (lets users override the default by defining their own)
@ConditionalOnMissingBean(DataSource.class)
// Bean created when a property has a specific value
@ConditionalOnProperty(
prefix = "app.feature-flags",
name = "notifications-enabled",
havingValue = "true",
matchIfMissing = false) // absent property = condition fails
// Bean created when the app runs as a web application
@ConditionalOnWebApplication
// Bean created when a specific bean exists
@ConditionalOnBean(SomeService.class)
@ConditionalOnMissingBean - The Override Pattern
Spring Boot ships: DataSourceAutoConfiguration creates a DataSource bean.
You need a custom DataSource? Just define your own @Bean DataSource.
Spring Boot sees your bean → @ConditionalOnMissingBean → skips the default.
This pattern appears everywhere in Spring Boot auto-configuration:
"Provide a sensible default, but step aside when the user takes over."
// In your @Configuration - overrides Spring Boot's default:
@Bean
public DataSource myDataSource() {
return new HikariDataSource(customConfig());
}
// Spring Boot's DataSourceAutoConfiguration is now skipped.
Starters
A starter is a single Maven/Gradle dependency that brings in a coherent
set of libraries, auto-configuration, and managed versions.
spring-boot-starter-web
includes: spring-webmvc, spring-core, jackson-databind,
spring-boot-starter-tomcat, spring-boot-starter-validation
auto-configures: DispatcherServlet, Jackson ObjectMapper,
embedded Tomcat server
spring-boot-starter-data-jpa
includes: hibernate-core, spring-data-jpa, spring-orm
auto-configures: EntityManagerFactory, JpaTransactionManager,
Spring Data repository proxies
spring-boot-starter-actuator
includes: micrometer-core, spring-boot-actuator
auto-configures: /actuator endpoints, health indicators, metrics
You pick the starters you need. Spring Boot wires everything together.
No XML, no manual bean registration.
@ConfigurationProperties
Why Not @Value?
@Value("${app.max-connections}") @ConfigurationProperties(prefix = "app")
private int maxConnections; public class AppProperties {
@Min(1) @Max(200)
private int maxConnections;
}
@Value problems:
Scattered across the codebase - no single place to see all config
No type validation - wrong value → runtime crash
No IDE autocompletion
Repeated prefix everywhere
@ConfigurationProperties benefits:
All related properties in one class
@Validated: fail fast on startup with a clear error
IDE completion (with spring-boot-configuration-processor)
Relaxed binding: max-connections, maxConnections, MAX_CONNECTIONS all work
Binding Setup
// Option 1: @ConfigurationPropertiesScan on the main class (preferred)
@SpringBootApplication
@ConfigurationPropertiesScan // discovers all @ConfigurationProperties in package tree
public class MyApp { ... }
// Option 2: @EnableConfigurationProperties per class
@SpringBootApplication
@EnableConfigurationProperties({AppProperties.class, DatabaseProperties.class})
public class MyApp { ... }
Simple and Nested Properties
// application.properties:
// app.name=Java Training App
// app.max-connections=50
// app.feature-flags.notifications-enabled=true
// app.feature-flags.analytics-enabled=false
@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {
@NotBlank
private String name;
@Min(1) @Max(200)
private int maxConnections;
@Valid // cascades validation into the nested object
private FeatureFlags featureFlags = new FeatureFlags();
// getters + setters required for binding
public static class FeatureFlags {
private boolean notificationsEnabled; // ← app.feature-flags.notifications-enabled
private boolean analyticsEnabled;
// getters + setters
}
}
Relaxed Binding Rules
Property key form Example Maps to Java field
──────────────────────────────────────────────────────────────────────
Kebab-case (canonical) app.max-connections maxConnections
Camel-case app.maxConnections maxConnections
Underscore app.max_connections maxConnections
SCREAMING_SNAKE APP_MAX_CONNECTIONS maxConnections
(environment variable form)
All four forms bind to the same field.
Canonical form (kebab-case) is recommended in .properties files.
Validation on Startup
@Validated on the @ConfigurationProperties class activates Bean Validation
(Hibernate Validator) on the bound values at startup.
If app.max-connections=0 (violates @Min(1)):
→ Application refuses to start
→ Error: "Field error in object 'app' on field 'maxConnections': rejected value [0]"
→ Explicit, actionable error - not a NullPointerException 3 layers deep
Profiles
Profile-Specific Beans
// Loaded only when "dev" profile is active
@Component
@Profile("dev")
public class DevEnvironmentInfo implements EnvironmentInfo {
public String getName() { return "development"; }
public boolean isDebugEnabled() { return true; }
}
// Loaded only when "prod" profile is active
@Component
@Profile("prod")
public class ProdEnvironmentInfo implements EnvironmentInfo {
public String getName() { return "production"; }
public boolean isDebugEnabled() { return false; }
}
Profile-Specific Properties
File Loaded when
──────────────────────────────────────────────────────────────
application.properties always (base values)
application-dev.properties "dev" profile active (overrides base)
application-prod.properties "prod" profile active (overrides base)
Merge rule: base + profile-specific; profile wins on conflict.
application.properties application-dev.properties
───────────────────────── ─────────────────────────────────
app.database.url=defaultdb app.database.url=devdb ← wins
app.database.pool-size=10 app.database.pool-size=5 ← wins
app.name=Java Training App (not set) → inherits "Java Training App"
Activating Profiles
CLI argument: java -jar app.jar --spring.profiles.active=dev
Property file: spring.profiles.active=dev (not recommended in prod)
Environment var: SPRING_PROFILES_ACTIVE=dev
Test annotation: @ActiveProfiles("dev")
Multiple profiles: --spring.profiles.active=dev,oauth
Actuator
Actuator adds production-ready HTTP endpoints to your application.
Exposing Endpoints
# application.properties
management.endpoints.web.exposure.include=health,info,metrics,env
management.endpoint.health.show-details=always # show component-level details
management.info.env.enabled=true # expose info.* properties
Built-in Endpoints
Endpoint URL Purpose
─────────────────────────────────────────────────────────────────────────
/actuator/health GET /actuator/health UP/DOWN + per-component details
/actuator/info GET /actuator/info Application metadata
/actuator/metrics GET /actuator/metrics Micrometer counters/gauges/timers
/actuator/env GET /actuator/env All environment properties + sources
/actuator/beans GET /actuator/beans All Spring beans in the context
/actuator/conditions GET ... Which auto-configs matched/missed
/actuator/health Response
{
"status": "UP",
"components": {
"app": {
"status": "UP",
"details": { "app": "Java Training App", "maxConnections": 50 }
},
"diskSpace": { "status": "UP", "details": { "free": 120000000000 } },
"ping": { "status": "UP" }
}
}
Custom HealthIndicator
@Component
public class AppHealthIndicator implements HealthIndicator {
private final AppProperties props;
public AppHealthIndicator(AppProperties props) {
this.props = props;
}
@Override
public Health health() {
if (props.getMaxConnections() > 0) {
return Health.up()
.withDetail("app", props.getName())
.withDetail("maxConnections", props.getMaxConnections())
.build();
}
return Health.down()
.withDetail("reason", "maxConnections must be positive")
.build();
}
}
Spring Boot auto-discovers all HealthIndicator beans.
The overall /health status is the WORST of all component statuses:
all UP → UP
any DOWN → DOWN
any OUT_OF_SERVICE → OUT_OF_SERVICE
Custom InfoContributor
@Component
public class BuildInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("build", Map.of(
"artifact", "my-app",
"javaVersion", System.getProperty("java.version")
));
}
}
// /actuator/info response:
{
"app": { "name": "Java Training App", "version": "1.0.0" },
"build": { "artifact": "my-app", "javaVersion": "21.0.1" }
}
Lesson: Stale Test Resources in Maven
Problem: src/test/resources/application.properties was created, then deleted.
But Maven had already compiled it into target/test-classes/application.properties.
The stale file shadowed src/main/resources/application.properties.
Result: app.name was null → @NotBlank validation failed → context refused to start.
Rule: test classpath (target/test-classes) has higher priority than main
classpath (target/classes). A same-named file in test resources shadows the
main one entirely - it does not merge.
Fix: mvn clean test clears target/ before compiling. When in doubt after
deleting or renaming resources, always clean first.
Alternative: use @SpringBootTest(properties = {"key=value"}) for test-specific
properties rather than a properties file, to avoid the shadowing problem.
Module 37 - What Was Built
module-37-spring-boot/
├── pom.xml (Spring Boot 3.3.5, spring-boot-starter-web,
│ spring-boot-starter-actuator, spring-boot-starter-validation,
│ spring-boot-starter-test)
└── src/
├── main/
│ ├── java/com/javatraining/springboot/
│ │ ├── SpringBootDemoApplication.java - @ConfigurationPropertiesScan
│ │ ├── config/
│ │ │ ├── AppProperties.java - @ConfigurationProperties(prefix="app")
│ │ │ │ nested FeatureFlags, @Validated
│ │ │ ├── DatabaseProperties.java - @ConfigurationProperties(prefix="app.database")
│ │ │ └── ConditionalConfig.java - @ConditionalOnProperty, @ConditionalOnMissingBean
│ │ ├── profile/
│ │ │ ├── EnvironmentInfo.java - interface
│ │ │ ├── DevEnvironmentInfo.java - @Profile("dev")
│ │ │ └── ProdEnvironmentInfo.java - @Profile("prod")
│ │ └── actuator/
│ │ ├── AppHealthIndicator.java - custom HealthIndicator
│ │ └── BuildInfoContributor.java - custom InfoContributor
│ └── resources/
│ ├── application.properties - base config
│ ├── application-dev.properties - dev overrides
│ └── application-prod.properties - prod overrides
└── test/java/com/javatraining/springboot/
├── ConfigurationPropertiesTest.java 10 tests - binding, nested,
│ @ConditionalOnProperty, @ConditionalOnMissingBean
├── ProfileDevTest.java 6 tests - dev bean, debug, URL override
├── ProfileProdTest.java 5 tests - prod bean, no debug, URL override
└── ActuatorTest.java 6 tests - /health UP, custom indicator,
/info env + custom contributor
All tests: 27 passing.
Key Takeaways
Auto-configuration @Conditional* annotations decide whether each @Bean is registered
@ConditionalOnMissing "Provide a default unless the user defines their own" pattern
Starters One dependency = all libraries + auto-config for a feature
@ConfigurationProperties Type-safe property binding; prefix groups related config
@Validated Fail fast with clear validation errors at startup
Relaxed binding max-connections, maxConnections, MAX_CONNECTIONS all map to same field
@ConfigurationPropertiesScan Auto-discovers all @ConfigurationProperties in the package tree
Profiles @Profile("dev") / @Profile("prod") swap beans per environment
Profile properties application-{profile}.properties merges on top of base; profile wins
@ActiveProfiles Test annotation to activate profiles in @SpringBootTest
Actuator /health UP/DOWN aggregated from all HealthIndicator beans
Actuator /info Merged from InfoContributor beans + info.* properties
Custom indicators Implement HealthIndicator / InfoContributor → auto-discovered
Stale test resources target/test-classes/application.properties shadows main properties.
Always mvn clean test after deleting resource files.