Spring Boot¶
What is Spring Boot?¶
Spring Boot is an opinionated framework that simplifies Spring application development through: - Auto-configuration: Automatically configures beans based on classpath - Starter dependencies: Curated dependency sets for common use cases - Embedded servers: No external server deployment needed - Production-ready features: Metrics, health checks, externalized config
Application Bootstrap¶
Main Class¶
@SpringBootApplication // Combines 3 annotations
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// @SpringBootApplication is equivalent to:
@Configuration // This class provides beans
@EnableAutoConfiguration // Enable auto-configuration
@ComponentScan // Scan this package and subpackages
public class Application { }
Customizing Startup¶
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
app.setBannerMode(Banner.Mode.OFF);
app.setWebApplicationType(WebApplicationType.SERVLET);
app.setAdditionalProfiles("production");
app.setDefaultProperties(Map.of(
"server.port", "8080",
"spring.application.name", "my-app"
));
app.run(args);
}
// Fluent API
new SpringApplicationBuilder(Application.class)
.bannerMode(Banner.Mode.OFF)
.profiles("production")
.run(args);
Application Events¶
@Component
public class StartupListener {
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
log.info("Application is ready to serve requests");
}
@EventListener(ContextRefreshedEvent.class)
public void onContextRefreshed() {
log.info("Context refreshed");
}
}
// Event order:
// ApplicationStartingEvent
// ApplicationEnvironmentPreparedEvent
// ApplicationContextInitializedEvent
// ApplicationPreparedEvent
// ContextRefreshedEvent
// ApplicationStartedEvent
// ApplicationReadyEvent
CommandLineRunner & ApplicationRunner¶
@Component
public class DataLoader implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// Runs after context is loaded
loadInitialData();
}
}
@Component
@Order(1) // Run first
public class CacheWarmer implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// Access to parsed command line args
if (args.containsOption("warm-cache")) {
warmCache();
}
}
}
Configuration¶
application.properties / application.yml¶
# application.yml
server:
port: 8080
servlet:
context-path: /api
spring:
application:
name: my-service
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USER:postgres}
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
show-sql: false
logging:
level:
root: INFO
com.example: DEBUG
org.hibernate.SQL: DEBUG
# Custom properties
app:
feature:
new-checkout: true
payment:
timeout: 30s
retry:
max-attempts: 3
delay: 1000
@ConfigurationProperties¶
@ConfigurationProperties(prefix = "app.payment")
@Validated
public class PaymentProperties {
@NotNull
private Duration timeout;
@Valid
private Retry retry = new Retry();
// Getters and setters
public static class Retry {
private int maxAttempts = 3;
private long delay = 1000;
// Getters and setters
}
}
// Enable scanning
@Configuration
@EnableConfigurationProperties(PaymentProperties.class)
public class AppConfig { }
// Or use component scan
@ConfigurationProperties(prefix = "app.payment")
@Component
public class PaymentProperties { }
// Usage
@Service
public class PaymentService {
private final PaymentProperties properties;
public PaymentService(PaymentProperties properties) {
this.properties = properties;
}
public void process() {
Duration timeout = properties.getTimeout();
int maxRetries = properties.getRetry().getMaxAttempts();
}
}
@Value Injection¶
@Service
public class NotificationService {
@Value("${app.notification.enabled:true}")
private boolean enabled;
@Value("${app.notification.email.from}")
private String fromEmail;
@Value("${SMTP_PASSWORD}") // Environment variable
private String smtpPassword;
@Value("#{${app.feature.flags}}") // SpEL - Map
private Map<String, Boolean> featureFlags;
@Value("${app.allowed-origins}") // Comma-separated to List
private List<String> allowedOrigins;
}
Profiles¶
# application.yml (default)
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:development}
---
# application-development.yml
spring:
config:
activate:
on-profile: development
datasource:
url: jdbc:h2:mem:testdb
jpa:
hibernate:
ddl-auto: create-drop
---
# application-production.yml
spring:
config:
activate:
on-profile: production
datasource:
url: jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
jpa:
hibernate:
ddl-auto: validate
Starters¶
Common Starters¶
Dependency Management¶
// build.gradle
plugins {
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
id 'java'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Version managed by Spring Boot BOM
implementation 'com.fasterxml.jackson.core:jackson-databind'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Auto-Configuration¶
How It Works¶
Conditional Annotations¶
@Configuration
@ConditionalOnClass(DataSource.class) // Class on classpath
@ConditionalOnMissingBean(DataSource.class) // No bean defined
@ConditionalOnProperty(
prefix = "spring.datasource",
name = "enabled",
havingValue = "true",
matchIfMissing = true
)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DataSourceProperties properties) {
return DataSourceBuilder.create()
.url(properties.getUrl())
.username(properties.getUsername())
.password(properties.getPassword())
.build();
}
}
Excluding Auto-Configuration¶
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
SecurityAutoConfiguration.class
})
public class Application { }
// Or in properties
spring.autoconfigure.exclude=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
Debug Auto-Configuration¶
# See what auto-configs were applied
java -jar app.jar --debug
# Or
logging.level.org.springframework.boot.autoconfigure=DEBUG
Web Development¶
REST Controllers¶
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping
public List<OrderDTO> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return orderService.findAll(PageRequest.of(page, size));
}
@GetMapping("/{id}")
public ResponseEntity<OrderDTO> get(@PathVariable Long id) {
return orderService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public OrderDTO create(@Valid @RequestBody CreateOrderRequest request) {
return orderService.create(request);
}
@PutMapping("/{id}")
public OrderDTO update(@PathVariable Long id,
@Valid @RequestBody UpdateOrderRequest request) {
return orderService.update(id, request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
orderService.delete(id);
}
}
Exception Handling¶
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
return new ErrorResponse("NOT_FOUND", ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.toList();
return new ErrorResponse("VALIDATION_ERROR", errors);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGeneral(Exception ex) {
log.error("Unhandled exception", ex);
return new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred");
}
}
public record ErrorResponse(String code, Object message) {}
Request Validation¶
public record CreateOrderRequest(
@NotNull @Size(min = 1)
List<@Valid OrderItem> items,
@NotBlank
String customerId,
@Valid
ShippingAddress shippingAddress
) {}
public record OrderItem(
@NotBlank String productId,
@Min(1) int quantity,
@Positive BigDecimal price
) {}
public record ShippingAddress(
@NotBlank String street,
@NotBlank String city,
@Pattern(regexp = "\\d{5}") String zipCode
) {}
Actuator¶
Setup¶
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
base-path: /actuator
endpoint:
health:
show-details: when_authorized
health:
diskspace:
enabled: true
db:
enabled: true
info:
env:
enabled: true
Common Endpoints¶
Custom Health Indicator¶
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
private final RestTemplate restTemplate;
@Override
public Health health() {
try {
ResponseEntity<String> response = restTemplate.getForEntity(
"https://api.external.com/health", String.class);
if (response.getStatusCode().is2xxSuccessful()) {
return Health.up()
.withDetail("status", "External API is reachable")
.build();
}
return Health.down()
.withDetail("status", "External API returned: " + response.getStatusCode())
.build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}
Custom Metrics¶
@Service
public class OrderService {
private final Counter orderCounter;
private final Timer orderProcessingTimer;
public OrderService(MeterRegistry meterRegistry) {
this.orderCounter = meterRegistry.counter("orders.created");
this.orderProcessingTimer = meterRegistry.timer("orders.processing.time");
}
public Order create(CreateOrderRequest request) {
return orderProcessingTimer.record(() -> {
Order order = processOrder(request);
orderCounter.increment();
return order;
});
}
}
Testing¶
Unit Tests¶
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentService paymentService;
@InjectMocks
private OrderService orderService;
@Test
void shouldCreateOrder() {
// Given
CreateOrderRequest request = new CreateOrderRequest(...);
when(paymentService.charge(any())).thenReturn(PaymentResult.success());
when(orderRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
// When
Order result = orderService.create(request);
// Then
assertThat(result.getStatus()).isEqualTo(OrderStatus.CREATED);
verify(orderRepository).save(any(Order.class));
}
}
Integration Tests¶
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldCreateOrder() {
CreateOrderRequest request = new CreateOrderRequest(...);
ResponseEntity<OrderDTO> response = restTemplate.postForEntity(
"/api/v1/orders", request, OrderDTO.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().id()).isNotNull();
assertThat(orderRepository.findById(response.getBody().id())).isPresent();
}
}
Slice Tests¶
// Web layer only
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Test
void shouldReturnOrder() throws Exception {
when(orderService.findById(1L))
.thenReturn(Optional.of(new OrderDTO(1L, "CREATED")));
mockMvc.perform(get("/api/v1/orders/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.status").value("CREATED"));
}
}
// Repository layer only
@DataJpaTest
class OrderRepositoryTest {
@Autowired
private OrderRepository orderRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindByCustomerId() {
Order order = new Order();
order.setCustomerId("cust-123");
entityManager.persist(order);
List<Order> orders = orderRepository.findByCustomerId("cust-123");
assertThat(orders).hasSize(1);
}
}
Test Configuration¶
@TestConfiguration
public class TestConfig {
@Bean
@Primary
public PaymentService mockPaymentService() {
PaymentService mock = mock(PaymentService.class);
when(mock.charge(any())).thenReturn(PaymentResult.success());
return mock;
}
}
// Use Testcontainers for real dependencies
@SpringBootTest
@Testcontainers
class OrderServiceIT {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}
Common Interview Questions¶
- What is @SpringBootApplication?
-
Combines @Configuration, @EnableAutoConfiguration, @ComponentScan
-
How does auto-configuration work?
- Scans META-INF for auto-config classes
- Uses @Conditional annotations to decide what to configure
-
Your beans take precedence
-
Externalized configuration priority?
-
Command line args > Profile-specific > application.properties > @PropertySource
-
How to create custom starter?
- Create auto-configuration class with @Conditional
- Add META-INF/spring.factories
-
Create starter module with dependencies
-
Embedded server vs external?
- Embedded: Simpler deployment, app owns server
- External: Traditional deployment, shared server
- *