Spring Boot
The de facto standard framework for Java enterprise application development. Enables rapid development through auto-configuration and rich ecosystem.
GitHub Overview
spring-projects/spring-boot
Spring Boot helps you to create Spring-powered, production-grade applications and services with absolute minimum fuss.
Topics
Star History
Framework
Spring Boot
Overview
Spring Boot is the most popular web application framework in the Java ecosystem, designed for rapid and easy development of production-grade Spring applications.
Details
Spring Boot is a framework for creating standalone, production-grade Spring-based applications quickly and easily, built on top of the Spring Framework. Since its release in 2014, it has established itself as the de facto standard for Java development. The auto-configuration feature significantly reduces the complex XML configuration required in traditional Spring, allowing web applications to be built with minimal code. With embedded Tomcat and Jetty servers, it creates executable applications as single JAR files, simplifying deployment. Dependency Injection (DI) and Aspect-Oriented Programming (AOP) enable the development of highly maintainable and extensible applications. Currently widely used in microservices architecture and cloud-native application development, it's utilized for enterprise system construction in combination with a rich ecosystem including Spring Cloud, Spring Security, and other related technologies. Spring Boot 3.x now supports Java 17+, GraalVM native images, and improved observability features.
Pros and Cons
Pros
- Auto-configuration: Automates complex configurations, dramatically improving development efficiency
- Embedded Servers: Built-in Tomcat and Jetty for easy web application execution
- Starter Dependencies: Packages necessary features together, simplifying dependency management
- Production-Ready Features: Actuator provides monitoring and management capabilities for operations
- Rich Ecosystem: Diverse related technologies like Spring Cloud and Spring Security
- Microservices Ready: Lightweight and fast startup, ideal for microservices
- Enterprise Support: Long-term support and continuous development by VMware Tanzu
- High Market Value: High demand in enterprise systems with strong career prospects
Cons
- Learning Curve: Understanding the entire Spring ecosystem requires significant time investment
- Memory Usage: Framework overhead tends to consume more memory
- Startup Time: Longer startup times due to DI container initialization and AOP processing
- Feature Overkill: May have excessive functionality for simple applications
- Black Box Effect: Auto-configuration can make internal operations less transparent
- Java Dependency: Tied to Java language and JVM, cannot be used with other languages
Key Links
- Spring Boot Official Site
- Spring Boot Documentation
- Spring Initializr
- Spring Boot GitHub
- Spring Guides
- Spring Boot Actuator
Code Examples
Hello World (Basic Web Application)
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class DemoApplication {
@GetMapping("/")
public String hello() {
return "Hello, Spring Boot World!";
}
@GetMapping("/api/status")
public String status() {
return "Application is running!";
}
@GetMapping("/greet/{name}")
public String greet(@PathVariable String name) {
return "Hello, " + name + "!";
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
// Project creation commands:
// Visit https://start.spring.io/ or use Spring CLI:
// spring init --dependencies=web --name=demo demo
// cd demo
// ./mvnw spring-boot:run
REST API Development
package com.example.demo.controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.demo.service.UserService;
import com.example.demo.model.User;
import java.util.List;
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "*") // Allow CORS for frontend integration
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.findAll();
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(user -> ResponseEntity.ok(user))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
try {
User savedUser = userService.save(user);
return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User userDetails) {
return userService.findById(id)
.map(user -> {
user.setName(userDetails.getName());
user.setEmail(userDetails.getEmail());
return ResponseEntity.ok(userService.save(user));
})
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
if (userService.existsById(id)) {
userService.deleteById(id);
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
@GetMapping("/search")
public ResponseEntity<List<User>> searchUsers(@RequestParam String name) {
List<User> users = userService.findByNameContaining(name);
return ResponseEntity.ok(users);
}
}
// User model class
package com.example.demo.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Name is required")
@Column(nullable = false)
private String name;
@Email(message = "Email should be valid")
@NotBlank(message = "Email is required")
@Column(nullable = false, unique = true)
private String email;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
// Constructors
public User() {}
public User(String name, String email) {
this.name = name;
this.email = email;
}
// 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 LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
Database Operations (Spring Data JPA)
package com.example.demo.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.DecimalMin;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Product name is required")
@Column(nullable = false)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@DecimalMin(value = "0.0", message = "Price must be positive")
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(name = "created_at")
private LocalDateTime createdAt;
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Review> reviews;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
// Constructors, getters, and setters
public Product() {}
public Product(String name, String description, BigDecimal price) {
this.name = name;
this.description = description;
this.price = price;
}
// Getters and setters omitted for brevity
}
// Repository interface with custom queries
package com.example.demo.repository;
import com.example.demo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Query methods using method naming
List<Product> findByNameContainingIgnoreCase(String name);
List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
Optional<Product> findByNameIgnoreCase(String name);
// Custom JPQL queries
@Query("SELECT p FROM Product p WHERE p.price > :price")
List<Product> findExpensiveProducts(@Param("price") BigDecimal price);
@Query("SELECT p FROM Product p WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%')) " +
"OR LOWER(p.description) LIKE LOWER(CONCAT('%', :keyword, '%'))")
Page<Product> searchProducts(@Param("keyword") String keyword, Pageable pageable);
// Native SQL query
@Query(value = "SELECT * FROM products p WHERE p.price >= ?1 ORDER BY p.created_at DESC LIMIT ?2",
nativeQuery = true)
List<Product> findTopExpensiveProducts(BigDecimal minPrice, int limit);
}
// Service layer with business logic
package com.example.demo.service;
import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class ProductService {
@Autowired
private ProductRepository productRepository;
public List<Product> findAll() {
return productRepository.findAll();
}
public Page<Product> findAllPaginated(int page, int size, String sortBy) {
PageRequest pageRequest = PageRequest.of(page, size, Sort.by(sortBy));
return productRepository.findAll(pageRequest);
}
public Optional<Product> findById(Long id) {
return productRepository.findById(id);
}
@Transactional
public Product save(Product product) {
return productRepository.save(product);
}
@Transactional
public void deleteById(Long id) {
productRepository.deleteById(id);
}
public boolean existsById(Long id) {
return productRepository.existsById(id);
}
public List<Product> searchByName(String name) {
return productRepository.findByNameContainingIgnoreCase(name);
}
public List<Product> findByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
return productRepository.findByPriceBetween(minPrice, maxPrice);
}
public Page<Product> searchProducts(String keyword, int page, int size) {
PageRequest pageRequest = PageRequest.of(page, size, Sort.by("createdAt").descending());
return productRepository.searchProducts(keyword, pageRequest);
}
}
Dependency Injection and Bean Management
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@Configuration
public class AppConfig {
@Bean
@Primary
public NotificationService emailNotificationService() {
return new EmailNotificationService();
}
@Bean
@Profile("production")
public NotificationService smsNotificationService() {
return new SmsNotificationService();
}
@Bean
@ConditionalOnProperty(name = "app.slack.enabled", havingValue = "true")
public NotificationService slackNotificationService() {
return new SlackNotificationService();
}
}
// Service interfaces and implementations
package com.example.demo.service;
public interface NotificationService {
void sendNotification(String message, String recipient);
boolean isAvailable();
}
// Email implementation
package com.example.demo.service.impl;
import com.example.demo.service.NotificationService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class EmailNotificationService implements NotificationService {
private static final Logger logger = LoggerFactory.getLogger(EmailNotificationService.class);
@Value("${app.email.enabled:true}")
private boolean emailEnabled;
@Override
public void sendNotification(String message, String recipient) {
if (emailEnabled) {
logger.info("Sending email to {}: {}", recipient, message);
// Email sending logic here
} else {
logger.warn("Email service is disabled");
}
}
@Override
public boolean isAvailable() {
return emailEnabled;
}
}
// SMS implementation
@Service
public class SmsNotificationService implements NotificationService {
private static final Logger logger = LoggerFactory.getLogger(SmsNotificationService.class);
@Override
public void sendNotification(String message, String recipient) {
logger.info("Sending SMS to {}: {}", recipient, message);
// SMS sending logic here
}
@Override
public boolean isAvailable() {
return true; // Assume SMS is always available
}
}
// Usage in controller with dependency injection
package com.example.demo.controller;
import com.example.demo.service.NotificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import java.util.List;
@RestController
@RequestMapping("/api/notifications")
public class NotificationController {
private final NotificationService primaryNotificationService;
private final List<NotificationService> allNotificationServices;
// Constructor injection (recommended approach)
public NotificationController(@Qualifier("emailNotificationService") NotificationService primaryService,
List<NotificationService> allServices) {
this.primaryNotificationService = primaryService;
this.allNotificationServices = allServices;
}
@PostMapping("/send")
public ResponseEntity<String> sendNotification(@RequestParam String message,
@RequestParam String recipient) {
primaryNotificationService.sendNotification(message, recipient);
return ResponseEntity.ok("Notification sent via primary service");
}
@PostMapping("/broadcast")
public ResponseEntity<String> broadcastNotification(@RequestParam String message,
@RequestParam String recipient) {
allNotificationServices.forEach(service -> {
if (service.isAvailable()) {
service.sendNotification(message, recipient);
}
});
return ResponseEntity.ok("Notification broadcasted to all available services");
}
@GetMapping("/services")
public ResponseEntity<List<String>> getAvailableServices() {
List<String> availableServices = allNotificationServices.stream()
.filter(NotificationService::isAvailable)
.map(service -> service.getClass().getSimpleName())
.toList();
return ResponseEntity.ok(availableServices);
}
}
Configuration Properties and Validation
package com.example.demo.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.*;
import java.time.Duration;
import java.util.List;
import java.util.Map;
@Configuration
@ConfigurationProperties(prefix = "app")
@Validated
public class ApplicationProperties {
@NotBlank(message = "Application name is required")
private String name;
@NotBlank(message = "Version is required")
@Pattern(regexp = "\\d+\\.\\d+\\.\\d+", message = "Version must follow semantic versioning")
private String version;
private Database database = new Database();
private Security security = new Security();
private Cache cache = new Cache();
// Feature flags
private Map<String, Boolean> features;
// 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 Database getDatabase() { return database; }
public void setDatabase(Database database) { this.database = database; }
public Security getSecurity() { return security; }
public void setSecurity(Security security) { this.security = security; }
public Cache getCache() { return cache; }
public void setCache(Cache cache) { this.cache = cache; }
public Map<String, Boolean> getFeatures() { return features; }
public void setFeatures(Map<String, Boolean> features) { this.features = features; }
public static class Database {
@NotBlank(message = "Database host is required")
private String host = "localhost";
@Min(value = 1, message = "Port must be greater than 0")
@Max(value = 65535, message = "Port must be less than 65536")
private int port = 5432;
@NotBlank(message = "Database name is required")
private String name;
@Min(value = 1, message = "Pool size must be at least 1")
private int maxPoolSize = 20;
private Duration connectionTimeout = Duration.ofSeconds(30);
// Getters and setters
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getMaxPoolSize() { return maxPoolSize; }
public void setMaxPoolSize(int maxPoolSize) { this.maxPoolSize = maxPoolSize; }
public Duration getConnectionTimeout() { return connectionTimeout; }
public void setConnectionTimeout(Duration connectionTimeout) { this.connectionTimeout = connectionTimeout; }
}
public static class Security {
private boolean enabled = true;
@NotEmpty(message = "At least one allowed origin is required")
private List<String> allowedOrigins = List.of("http://localhost:3000");
private Jwt jwt = new Jwt();
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public List<String> getAllowedOrigins() { return allowedOrigins; }
public void setAllowedOrigins(List<String> allowedOrigins) { this.allowedOrigins = allowedOrigins; }
public Jwt getJwt() { return jwt; }
public void setJwt(Jwt jwt) { this.jwt = jwt; }
public static class Jwt {
@NotBlank(message = "JWT secret key is required")
private String secretKey;
private Duration expiration = Duration.ofHours(24);
public String getSecretKey() { return secretKey; }
public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
public Duration getExpiration() { return expiration; }
public void setExpiration(Duration expiration) { this.expiration = expiration; }
}
}
public static class Cache {
private boolean enabled = true;
private String type = "memory";
private Duration ttl = Duration.ofMinutes(30);
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public Duration getTtl() { return ttl; }
public void setTtl(Duration ttl) { this.ttl = ttl; }
}
}
// application.yml configuration example
/*
app:
name: My Spring Boot Application
version: 1.0.0
database:
host: localhost
port: 5432
name: myapp_db
max-pool-size: 20
connection-timeout: PT30S
security:
enabled: true
allowed-origins:
- http://localhost:3000
- https://myapp.com
jwt:
secret-key: ${JWT_SECRET_KEY:fallback-secret-key}
expiration: PT24H
cache:
enabled: true
type: redis
ttl: PT30M
features:
new-dashboard: true
beta-api: false
analytics: true
*/
// Using configuration properties in a component
package com.example.demo.component;
import com.example.demo.config.ApplicationProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component
public class AppInfoComponent {
private static final Logger logger = LoggerFactory.getLogger(AppInfoComponent.class);
private final ApplicationProperties appProperties;
public AppInfoComponent(ApplicationProperties appProperties) {
this.appProperties = appProperties;
}
@PostConstruct
public void logApplicationInfo() {
logger.info("Starting application: {}", appProperties.getName());
logger.info("Version: {}", appProperties.getVersion());
logger.info("Database: {}:{}/{}",
appProperties.getDatabase().getHost(),
appProperties.getDatabase().getPort(),
appProperties.getDatabase().getName());
logger.info("Security enabled: {}", appProperties.getSecurity().isEnabled());
logger.info("Cache enabled: {}", appProperties.getCache().isEnabled());
if (appProperties.getFeatures() != null) {
appProperties.getFeatures().forEach((feature, enabled) -> {
logger.info("Feature '{}': {}", feature, enabled ? "ENABLED" : "DISABLED");
});
}
}
public boolean isFeatureEnabled(String featureName) {
return appProperties.getFeatures() != null &&
Boolean.TRUE.equals(appProperties.getFeatures().get(featureName));
}
}
Testing (JUnit 5, Mockito, and TestContainers)
package com.example.demo.controller;
import com.example.demo.service.ProductService;
import com.example.demo.entity.Product;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
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 java.math.BigDecimal;
import java.util.Arrays;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
@WebMvcTest(ProductController.class)
@DisplayName("Product Controller Tests")
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductService productService;
@Autowired
private ObjectMapper objectMapper;
@Test
@DisplayName("Should return all products")
void testGetAllProducts() throws Exception {
// Given
Product product1 = new Product("Laptop", "Gaming laptop", new BigDecimal("999.99"));
Product product2 = new Product("Mouse", "Wireless mouse", new BigDecimal("29.99"));
when(productService.findAll()).thenReturn(Arrays.asList(product1, product2));
// When & Then
mockMvc.perform(get("/api/products"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$.length()").value(2))
.andExpect(jsonPath("$[0].name").value("Laptop"))
.andExpected(jsonPath("$[0].price").value(999.99))
.andExpect(jsonPath("$[1].name").value("Mouse"));
verify(productService).findAll();
}
@Test
@DisplayName("Should return product by ID")
void testGetProductById() throws Exception {
// Given
Product product = new Product("Keyboard", "Mechanical keyboard", new BigDecimal("129.99"));
product.setId(1L);
when(productService.findById(1L)).thenReturn(Optional.of(product));
// When & Then
mockMvc.perform(get("/api/products/1"))
.andExpect(status().isOk())
.andExpected(jsonPath("$.id").value(1))
.andExpected(jsonPath("$.name").value("Keyboard"))
.andExpected(jsonPath("$.price").value(129.99));
verify(productService).findById(1L);
}
@Test
@DisplayName("Should return 404 when product not found")
void testGetProductByIdNotFound() throws Exception {
// Given
when(productService.findById(999L)).thenReturn(Optional.empty());
// When & Then
mockMvc.perform(get("/api/products/999"))
.andExpect(status().isNotFound());
verify(productService).findById(999L);
}
@Test
@DisplayName("Should create new product")
void testCreateProduct() throws Exception {
// Given
Product newProduct = new Product("Monitor", "4K Monitor", new BigDecimal("399.99"));
Product savedProduct = new Product("Monitor", "4K Monitor", new BigDecimal("399.99"));
savedProduct.setId(1L);
when(productService.save(any(Product.class))).thenReturn(savedProduct);
// When & Then
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(newProduct)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Monitor"))
.andExpected(jsonPath("$.price").value(399.99));
verify(productService).save(any(Product.class));
}
@Test
@DisplayName("Should update existing product")
void testUpdateProduct() throws Exception {
// Given
Product existingProduct = new Product("Old Name", "Old Description", new BigDecimal("100.00"));
existingProduct.setId(1L);
Product updateRequest = new Product("New Name", "New Description", new BigDecimal("150.00"));
Product updatedProduct = new Product("New Name", "New Description", new BigDecimal("150.00"));
updatedProduct.setId(1L);
when(productService.findById(1L)).thenReturn(Optional.of(existingProduct));
when(productService.save(any(Product.class))).thenReturn(updatedProduct);
// When & Then
mockMvc.perform(put("/api/products/1")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updateRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("New Name"))
.andExpected(jsonPath("$.price").value(150.00));
verify(productService).findById(1L);
verify(productService).save(any(Product.class));
}
}
// Integration test with TestContainers
package com.example.demo;
import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.math.BigDecimal;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@DisplayName("Product Integration Tests")
class ProductIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@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);
}
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private ProductRepository productRepository;
@Test
@DisplayName("Should create and retrieve product end-to-end")
void testCreateAndRetrieveProduct() {
// Given
Product product = new Product("Integration Test Product", "Test Description", new BigDecimal("99.99"));
// When - Create product
ResponseEntity<Product> createResponse = restTemplate.postForEntity(
"http://localhost:" + port + "/api/products",
product,
Product.class
);
// Then - Verify creation
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody()).isNotNull();
assertThat(createResponse.getBody().getName()).isEqualTo("Integration Test Product");
assertThat(createResponse.getBody().getId()).isNotNull();
// When - Retrieve product
Long productId = createResponse.getBody().getId();
ResponseEntity<Product> getResponse = restTemplate.getForEntity(
"http://localhost:" + port + "/api/products/" + productId,
Product.class
);
// Then - Verify retrieval
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody()).isNotNull();
assertThat(getResponse.getBody().getName()).isEqualTo("Integration Test Product");
assertThat(getResponse.getBody().getPrice()).isEqualByComparingTo(new BigDecimal("99.99"));
// Verify database state
assertThat(productRepository.findById(productId)).isPresent();
}
@Test
@DisplayName("Should handle product search functionality")
void testProductSearch() {
// Given - Setup test data
productRepository.save(new Product("Gaming Laptop", "High-end gaming laptop", new BigDecimal("1299.99")));
productRepository.save(new Product("Office Laptop", "Business laptop", new BigDecimal("799.99")));
productRepository.save(new Product("Gaming Mouse", "RGB gaming mouse", new BigDecimal("49.99")));
// When - Search for gaming products
ResponseEntity<Product[]> response = restTemplate.getForEntity(
"http://localhost:" + port + "/api/products/search?name=gaming",
Product[].class
);
// Then - Verify search results
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody()).hasSize(2);
assertThat(response.getBody()[0].getName()).containsIgnoringCase("gaming");
assertThat(response.getBody()[1].getName()).containsIgnoringCase("gaming");
}
}
// Service layer unit test
package com.example.demo.service;
import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("Product Service Tests")
class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@InjectMocks
private ProductService productService;
@Test
@DisplayName("Should save product successfully")
void testSaveProduct() {
// Given
Product product = new Product("Test Product", "Test Description", new BigDecimal("199.99"));
Product savedProduct = new Product("Test Product", "Test Description", new BigDecimal("199.99"));
savedProduct.setId(1L);
when(productRepository.save(any(Product.class))).thenReturn(savedProduct);
// When
Product result = productService.save(product);
// Then
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getName()).isEqualTo("Test Product");
verify(productRepository).save(product);
}
@Test
@DisplayName("Should find product by ID")
void testFindById() {
// Given
Product product = new Product("Test Product", "Description", new BigDecimal("99.99"));
product.setId(1L);
when(productRepository.findById(1L)).thenReturn(Optional.of(product));
// When
Optional<Product> result = productService.findById(1L);
// Then
assertThat(result).isPresent();
assertThat(result.get().getName()).isEqualTo("Test Product");
verify(productRepository).findById(1L);
}
@Test
@DisplayName("Should return empty when product not found")
void testFindByIdNotFound() {
// Given
when(productRepository.findById(999L)).thenReturn(Optional.empty());
// When
Optional<Product> result = productService.findById(999L);
// Then
assertThat(result).isEmpty();
verify(productRepository).findById(999L);
}
}
Spring Boot Actuator and Monitoring
// Custom health indicator
package com.example.demo.health;
import org.springframework.boot.actuator.health.Health;
import org.springframework.boot.actuator.health.HealthIndicator;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Health health() {
try {
// Test database connectivity
jdbcTemplate.queryForObject("SELECT 1", Integer.class);
return Health.up()
.withDetail("database", "PostgreSQL")
.withDetail("status", "Connected")
.build();
} catch (Exception e) {
return Health.down()
.withDetail("database", "PostgreSQL")
.withDetail("error", e.getMessage())
.build();
}
}
}
// Custom metrics
package com.example.demo.metrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class ProductMetrics {
private final Counter productCreatedCounter;
private final Timer productSearchTimer;
private final MeterRegistry meterRegistry;
public ProductMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.productCreatedCounter = Counter.builder("products.created")
.description("Number of products created")
.register(meterRegistry);
this.productSearchTimer = Timer.builder("products.search.duration")
.description("Product search duration")
.register(meterRegistry);
}
public void incrementProductCreated() {
productCreatedCounter.increment();
}
public Timer.Sample startSearchTimer() {
return Timer.start(meterRegistry);
}
public void recordSearchTime(Timer.Sample sample) {
sample.stop(productSearchTimer);
}
public void recordSearchTime(long duration, TimeUnit unit) {
productSearchTimer.record(duration, unit);
}
}
// application.yml for Actuator configuration
/*
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,env,beans
base-path: /actuator
endpoint:
health:
show-details: always
show-components: always
metrics:
export:
prometheus:
enabled: true
info:
env:
enabled: true
info:
app:
name: ${app.name}
version: ${app.version}
description: Spring Boot Demo Application
build:
artifact: ${project.artifactId}
version: ${project.version}
*/