Master Spring Boot Development
Spring Boot
Spring Boot is a powerful framework that simplifies Spring application development by providing auto-configuration, embedded servers, and production-ready features out of the box. It follows the "convention over configuration" principle, allowing developers to create stand-alone, production-grade Spring applications with minimal setup.
What is Spring Boot?
Spring Boot builds on top of the Spring Framework to provide:
Key Features:
- Auto-Configuration: Automatically configures Spring beans based on classpath dependencies
- Starter Dependencies: Pre-configured dependency bundles for common use cases
- Embedded Servers: Built-in Tomcat, Jetty, or Undertow servers
- Production-Ready: Built-in metrics, health checks, and externalized configuration
- No XML Configuration: Convention-based configuration with annotations
Benefits:
- Rapid Development: Get applications running quickly with minimal configuration
- Microservices Ready: Perfect for building microservices architectures
- Production Features: Monitoring, security, and deployment capabilities built-in
- Developer Experience: Excellent tooling support and hot reloading
- Community Ecosystem: Large ecosystem of starters and extensions
// Traditional Spring application setup (verbose)
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.example")
public class WebConfig implements WebMvcConfigurer {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:h2:mem:testdb");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
// Many more configuration beans...
}
// Spring Boot application (simplified)
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
Getting Started with Spring Boot
Creating a Spring Boot application is straightforward:
Project Structure
my-spring-boot-app/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/
│ │ │ ├── MyApplication.java
│ │ │ ├── controller/
│ │ │ │ └── UserController.java
│ │ │ ├── service/
│ │ │ │ └── UserService.java
│ │ │ └── model/
│ │ │ └── User.java
│ │ └── resources/
│ │ ├── application.properties
│ │ ├── static/
│ │ └── templates/
│ └── test/
│ └── java/
├── target/
└── pom.xml
Main Application Class
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication // Combines @Configuration, @EnableAutoConfiguration, @ComponentScan
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
Basic REST Controller
package com.example.controller;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
// Constructor injection (recommended)
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}
@PostMapping
public User createUser(@RequestBody User user) {
return userService.save(user);
}
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
return userService.update(id, user);
}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
userService.deleteById(id);
}
}
Service Layer
package com.example.service;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> findAll() {
return userRepository.findAll();
}
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
public User save(User user) {
validateUser(user);
return userRepository.save(user);
}
public User update(Long id, User user) {
User existing = findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
existing.setName(user.getName());
existing.setEmail(user.getEmail());
return userRepository.save(existing);
}
public void deleteById(Long id) {
if (!userRepository.existsById(id)) {
throw new UserNotFoundException("User not found: " + id);
}
userRepository.deleteById(id);
}
private void validateUser(User user) {
if (user.getName() == null || user.getName().trim().isEmpty()) {
throw new IllegalArgumentException("User name is required");
}
if (user.getEmail() == null || !user.getEmail().contains("@")) {
throw new IllegalArgumentException("Valid email is required");
}
}
}
Spring Boot Starters
Starters are dependency descriptors that bring in all necessary dependencies for specific functionality:
Common Starters
<!-- pom.xml -->
<dependencies>
<!-- Web applications -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Database access with JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Actuator for monitoring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
Database Integration Example
// Entity
package com.example.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;
@Email(message = "Invalid email format")
@NotBlank(message = "Email is required")
@Column(unique = true)
private String email;
@Min(value = 0, message = "Age must be non-negative")
@Max(value = 150, message = "Age must be realistic")
private Integer age;
// Constructors
public User() {}
public User(String name, String email, Integer age) {
this.name = name;
this.email = email;
this.age = age;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', email='" + email + "', age=" + age + "}";
}
}
// Repository
package com.example.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Spring Data JPA provides basic CRUD operations automatically
// Custom query methods
Optional<User> findByEmail(String email);
List<User> findByNameContainingIgnoreCase(String name);
List<User> findByAgeBetween(Integer minAge, Integer maxAge);
// Custom JPQL query
@Query("SELECT u FROM User u WHERE u.age > ?1 ORDER BY u.name")
List<User> findUsersOlderThan(Integer age);
// Native SQL query
@Query(value = "SELECT * FROM users WHERE email LIKE %?1%", nativeQuery = true)
List<User> findByEmailContaining(String emailPart);
}
Configuration and Properties
Spring Boot provides flexible configuration options:
Application Properties
# application.properties
# Server configuration
server.port=8080
server.servlet.context-path=/api
# Database configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# JPA configuration
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# H2 Console (for development)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# Logging configuration
logging.level.com.example=DEBUG
logging.level.org.springframework.web=INFO
logging.pattern.console=%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
# Actuator endpoints
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
Configuration Classes
package com.example.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppConfig {
private String name = "My Application";
private String version = "1.0.0";
private Security security = new Security();
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public Security getSecurity() { return security; }
public void setSecurity(Security security) { this.security = security; }
public static class Security {
private boolean enabled = true;
private String secretKey = "default-secret";
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getSecretKey() { return secretKey; }
public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
}
}
// Usage in properties:
// app.name=My Awesome App
// app.version=2.0.0
// app.security.enabled=true
// app.security.secret-key=my-secret-key
Environment-Specific Configuration
# application.yml (common configuration)
spring:
application:
name: my-app
---
# application-dev.yml (development)
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:devdb
jpa:
show-sql: true
logging:
level:
com.example: DEBUG
---
# application-prod.yml (production)
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:postgresql://localhost:5432/proddb
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
jpa:
show-sql: false
logging:
level:
com.example: WARN
Auto-Configuration
Spring Boot's auto-configuration automatically configures beans based on the classpath:
Custom Auto-Configuration
package com.example.autoconfigure;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyServiceProperties.class)
public class MyServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "myservice", name = "enabled", havingValue = "true", matchIfMissing = true)
public MyService myService(MyServiceProperties properties) {
return new MyService(properties.getApiKey(), properties.getTimeout());
}
@Bean
@ConditionalOnBean(MyService.class)
public MyServiceHealthIndicator myServiceHealthIndicator(MyService myService) {
return new MyServiceHealthIndicator(myService);
}
}
// Properties
@ConfigurationProperties(prefix = "myservice")
public class MyServiceProperties {
private boolean enabled = true;
private String apiKey;
private int timeout = 5000;
// Getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getApiKey() { return apiKey; }
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
public int getTimeout() { return timeout; }
public void setTimeout(int timeout) { this.timeout = timeout; }
}
Testing Spring Boot Applications
Spring Boot provides excellent testing support:
package com.example;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest
@ActiveProfiles("test")
class MyApplicationTests {
@Test
void contextLoads() {
// This test ensures the application context loads successfully
}
}
// Controller tests
package com.example.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
public void shouldCreateUser() throws Exception {
User user = new User("John Doe", "[email protected]", 30);
User savedUser = new User("John Doe", "[email protected]", 30);
savedUser.setId(1L);
given(userService.save(any(User.class))).willReturn(savedUser);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$.email").value("[email protected]"));
}
@Test
public void shouldReturnUserById() throws Exception {
User user = new User("John Doe", "[email protected]", 30);
user.setId(1L);
given(userService.findById(1L)).willReturn(Optional.of(user));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"));
}
}
// Repository tests
package com.example.repository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
@DataJpaTest
public class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
public void shouldFindUserByEmail() {
// Given
User user = new User("John Doe", "[email protected]", 30);
entityManager.persistAndFlush(user);
// When
Optional<User> found = userRepository.findByEmail("[email protected]");
// Then
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("John Doe");
}
@Test
public void shouldReturnEmptyWhenEmailNotFound() {
Optional<User> found = userRepository.findByEmail("[email protected]");
assertThat(found).isNotPresent();
}
}
Production Features
Spring Boot includes many production-ready features:
Actuator Endpoints
package com.example.actuator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final UserRepository userRepository;
public DatabaseHealthIndicator(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public Health health() {
try {
long userCount = userRepository.count();
return Health.up()
.withDetail("userCount", userCount)
.withDetail("status", "Database is accessible")
.build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}
// Custom info endpoint
package com.example.actuator;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;
@Component
public class AppInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("app", Map.of(
"name", "My Spring Boot App",
"description", "A sample Spring Boot application",
"version", "1.0.0",
"features", List.of("REST API", "JPA", "Security")
));
}
}
Metrics and Monitoring
package com.example.metrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
@Component
public class UserMetrics {
private final Counter userCreatedCounter;
private final Counter userDeletedCounter;
public UserMetrics(MeterRegistry meterRegistry) {
this.userCreatedCounter = Counter.builder("users.created")
.description("Number of users created")
.register(meterRegistry);
this.userDeletedCounter = Counter.builder("users.deleted")
.description("Number of users deleted")
.register(meterRegistry);
}
public void incrementUserCreated() {
userCreatedCounter.increment();
}
public void incrementUserDeleted() {
userDeletedCounter.increment();
}
}
// Updated service with metrics
@Service
public class UserService {
private final UserRepository userRepository;
private final UserMetrics userMetrics;
public UserService(UserRepository userRepository, UserMetrics userMetrics) {
this.userRepository = userRepository;
this.userMetrics = userMetrics;
}
public User save(User user) {
validateUser(user);
User saved = userRepository.save(user);
userMetrics.incrementUserCreated();
return saved;
}
public void deleteById(Long id) {
if (!userRepository.existsById(id)) {
throw new UserNotFoundException("User not found: " + id);
}
userRepository.deleteById(id);
userMetrics.incrementUserDeleted();
}
// Other methods...
}
Summary
Spring Boot revolutionizes Spring development:
Key Benefits:
- Rapid Development: Get applications running quickly with minimal setup
- Auto-Configuration: Intelligent defaults based on classpath scanning
- Production-Ready: Built-in monitoring, metrics, and health checks
- Microservices: Perfect foundation for microservices architecture
- Ecosystem: Rich ecosystem of starters and third-party integrations
Core Features:
- Starters: Dependency management made simple
- Auto-Configuration: Convention over configuration
- Embedded Servers: No need for external application servers
- Configuration: Flexible property-based configuration
- Testing: Comprehensive testing support with specialized annotations
Best Practices:
- Use Starters: Leverage Spring Boot starters for functionality
- Constructor Injection: Prefer constructor over field injection
- Profile-Specific Config: Use profiles for different environments
- Actuator: Enable monitoring and health checks
- Testing: Write comprehensive tests at all layers
Production Considerations:
- Externalized Config: Use environment variables and config servers
- Monitoring: Implement comprehensive logging and metrics
- Security: Follow security best practices
- Performance: Monitor and optimize application performance
- Deployment: Use container-based deployment strategies
Spring Boot is the de facto standard for building modern Java applications, providing a powerful yet simple foundation for enterprise development.