DevToolBoxKOSTENLOS
Blog

Spring Boot Komplett-Leitfaden: REST APIs, Spring Data JPA, Security, Actuator und Docker

16 Min. Lesezeitvon DevToolBox
TL;DR — Spring Boot Quick Reference

Spring Boot is an opinionated Java framework that eliminates configuration boilerplate. Add starters to pom.xml, annotate your main class with @SpringBootApplication, and run. Use @RestController + @GetMapping/@PostMapping for REST APIs. Use JpaRepository for CRUD database access. Secure endpoints with spring-boot-starter-security. Monitor with Actuator (/actuator/health). Test with @SpringBootTest + MockMvc. Package as a fat JAR and run with java -jar app.jar.

What Is Spring Boot?

Spring Boot is an opinionated, convention-over-configuration framework built on top of the Spring Framework that makes it easy to create stand-alone, production-grade Java applications. Released in 2014 by Pivotal (now VMware Tanzu), Spring Boot dramatically reduces the boilerplate configuration that made classic Spring development verbose.

The core philosophy of Spring Boot is: sensible defaults. If you add spring-boot-starter-web to your project, Spring Boot automatically configures an embedded Tomcat server, sets up Spring MVC, configures JSON serialization with Jackson, and handles all the bean wiring — with zero XML configuration required.

As of 2026, Spring Boot 3.x (requiring Java 17+) is the current major version, bringing native compilation support via GraalVM, improved observability, and full Jakarta EE 10 compatibility. It powers applications at Netflix, Airbnb, LinkedIn, and thousands of enterprises worldwide.

Key Takeaways
  • Spring Boot auto-configures beans based on classpath dependencies — no XML required.
  • Starters bundle compatible library versions: spring-boot-starter-web, spring-boot-starter-data-jpa, etc.
  • @SpringBootApplication combines @Configuration, @EnableAutoConfiguration, and @ComponentScan.
  • Dependency injection uses constructor injection by default; @Autowired is optional for single-constructor beans.
  • Spring Data JPA repositories eliminate CRUD boilerplate — extend JpaRepository to get 15+ methods for free.
  • Spring Security integrates JWT and OAuth2; configure with SecurityFilterChain beans.
  • Actuator exposes health, metrics, and management endpoints at /actuator/*.
  • Package as a fat JAR and deploy anywhere Java runs — or containerize with Docker.

Spring Boot vs Spring Framework

Understanding the difference between Spring Framework and Spring Boot is essential before diving in.

Spring Framework (2002):
  - Core container: IoC / DI
  - Spring MVC: web layer
  - Spring Data: data access
  - Spring Security: authentication/authorization
  - Spring AOP: aspect-oriented programming
  - Requires manual configuration of beans, XML or @Configuration classes
  - You must choose and wire compatible library versions yourself

Spring Boot (2014):
  - Built ON TOP of Spring Framework
  - Auto-configuration: detects classpath and configures beans automatically
  - Starter dependencies: curated, version-compatible bundles
  - Embedded servers: Tomcat/Jetty/Undertow — no WAR deployment needed
  - Production-ready: Actuator metrics, health, info endpoints
  - Opinionated defaults you can override when needed

Getting Started: Project Setup

The easiest way to bootstrap a Spring Boot project is Spring Initializr at start.spring.io. Choose your build tool (Maven or Gradle), Java version, and add dependencies.

<!-- pom.xml — Maven Spring Boot project -->
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.2.3</version>
</parent>

<dependencies>
  <!-- Web: Spring MVC + Embedded Tomcat + Jackson -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <!-- JPA: Hibernate + Spring Data JPA -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>

  <!-- Security: Spring Security -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
  </dependency>

  <!-- Actuator: health, metrics, info -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>

  <!-- Database driver (PostgreSQL) -->
  <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
  </dependency>

  <!-- Validation -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
  </dependency>

  <!-- Testing -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

The main application class:

// src/main/java/com/example/demo/DemoApplication.java
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
// Equivalent to:
//   @Configuration          — marks this as a bean source
//   @EnableAutoConfiguration — enables Spring Boot auto-config
//   @ComponentScan          — scans current package and sub-packages
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
# Run the application
./mvnw spring-boot:run

# Or build and run the JAR
./mvnw package
java -jar target/demo-0.0.1-SNAPSHOT.jar

# Application starts on http://localhost:8080

Dependency Injection: @Component, @Service, @Repository, @Autowired

Spring's IoC (Inversion of Control) container manages object creation and wiring. You mark classes with stereotype annotations so Spring can discover and inject them.

Stereotype Annotations

// @Component — generic Spring-managed bean
@Component
public class EmailValidator {
    public boolean isValid(String email) {
        return email.contains("@");
    }
}

// @Service — business logic layer
@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailValidator emailValidator;

    // Constructor injection (recommended — no @Autowired needed)
    public UserService(UserRepository userRepository,
                       EmailValidator emailValidator) {
        this.userRepository = userRepository;
        this.emailValidator = emailValidator;
    }

    public User createUser(String email, String name) {
        if (!emailValidator.isValid(email)) {
            throw new IllegalArgumentException("Invalid email: " + email);
        }
        User user = new User(email, name);
        return userRepository.save(user);
    }
}

// @Repository — data access layer (adds exception translation)
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

Field Injection vs Constructor Injection

// ❌ Field injection — harder to test, hides dependencies
@Service
public class BadService {
    @Autowired
    private UserRepository userRepository; // avoid this
}

// ✅ Constructor injection — explicit, testable, immutable
@Service
public class GoodService {
    private final UserRepository userRepository;

    public GoodService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

// ✅ Lombok + constructor injection (recommended in modern projects)
@Service
@RequiredArgsConstructor  // generates constructor for all final fields
public class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
}

@Bean and @Configuration

// Define beans programmatically in @Configuration classes
@Configuration
public class AppConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    @Bean
    @Profile("production")  // only active in production profile
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("users", "products");
    }
}

Spring MVC: Controllers, RequestMapping, and REST APIs

Spring MVC provides the web layer. @RestController combines @Controller and @ResponseBody, meaning every method returns data serialized directly to the response body (JSON by default) rather than rendering a view template.

Basic REST Controller

package com.example.demo.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;

@RestController
@RequestMapping("/api/v1/users")  // base path for all methods
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    // GET /api/v1/users
    @GetMapping
    public ResponseEntity<List<UserDto>> getAllUsers() {
        return ResponseEntity.ok(userService.findAll());
    }

    // GET /api/v1/users/{id}
    @GetMapping("/{id}")
    public ResponseEntity<UserDto> getUserById(@PathVariable Long id) {
        return ResponseEntity.ok(userService.findById(id));
    }

    // POST /api/v1/users
    @PostMapping
    public ResponseEntity<UserDto> createUser(
            @Valid @RequestBody CreateUserRequest request) {
        UserDto created = userService.create(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }

    // PUT /api/v1/users/{id}
    @PutMapping("/{id}")
    public ResponseEntity<UserDto> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody UpdateUserRequest request) {
        return ResponseEntity.ok(userService.update(id, request));
    }

    // DELETE /api/v1/users/{id}
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }

    // GET /api/v1/users/search?email=foo@bar.com&active=true
    @GetMapping("/search")
    public ResponseEntity<List<UserDto>> searchUsers(
            @RequestParam String email,
            @RequestParam(defaultValue = "true") boolean active) {
        return ResponseEntity.ok(userService.search(email, active));
    }
}

Request and Response DTOs with Validation

// Request DTO with Bean Validation
public class CreateUserRequest {
    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 100)
    private String name;

    @NotBlank
    @Email(message = "Must be a valid email address")
    private String email;

    @NotBlank
    @Size(min = 8, message = "Password must be at least 8 characters")
    private String password;

    // Lombok: @Getter @Setter @NoArgsConstructor or use Java records
    // Getters, setters...
}

// Response DTO (never expose entity directly)
public record UserDto(Long id, String name, String email, LocalDateTime createdAt) {}

// Validation error response — handled globally by @ControllerAdvice
// Returns HTTP 400 with field-level error messages

Configuration: application.yml and Profiles

Spring Boot reads configuration from application.properties or application.yml. YAML is preferred for its readability and support for nested properties. Profiles allow environment-specific configuration.

# src/main/resources/application.yml
spring:
  application:
    name: my-spring-app

  datasource:
    url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/mydb}
    username: ${DB_USERNAME:postgres}
    password: ${DB_PASSWORD:secret}
    driver-class-name: org.postgresql.Driver
    hikari:
      maximum-pool-size: 10
      minimum-idle: 2

  jpa:
    hibernate:
      ddl-auto: validate         # never use create/create-drop in production
    show-sql: false
    open-in-view: false          # recommended: disable OSIV

  security:
    jwt:
      secret: ${JWT_SECRET:change-this-in-production}
      expiration: 86400000       # 24 hours in ms

server:
  port: 8080
  servlet:
    context-path: /

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when-authorized

logging:
  level:
    com.example.demo: DEBUG
    org.springframework.security: INFO
# src/main/resources/application-dev.yml
# Active with: spring.profiles.active=dev
spring:
  datasource:
    url: jdbc:h2:mem:testdb    # in-memory H2 for development
  jpa:
    hibernate:
      ddl-auto: create-drop    # recreate schema on restart
    show-sql: true

logging:
  level:
    com.example.demo: TRACE
# src/main/resources/application-prod.yml
# Active with: SPRING_PROFILES_ACTIVE=prod
spring:
  jpa:
    hibernate:
      ddl-auto: validate
  datasource:
    hikari:
      maximum-pool-size: 20

management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
// Inject configuration values
@Service
public class JwtService {

    @Value("${spring.security.jwt.secret}")
    private String jwtSecret;

    @Value("${spring.security.jwt.expiration}")
    private long jwtExpiration;

    // Or use @ConfigurationProperties for grouped config
}

// Typed configuration with @ConfigurationProperties
@ConfigurationProperties(prefix = "spring.security.jwt")
@Component
public class JwtProperties {
    private String secret;
    private long expiration;
    // getters and setters
}

Spring Data JPA and Repositories

Spring Data JPA eliminates boilerplate database code. Define your entity with JPA annotations and extend JpaRepository to get dozens of CRUD and query operations automatically.

JPA Entity

package com.example.demo.entity;

import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;

@Entity
@Table(name = "users",
    uniqueConstraints = @UniqueConstraint(columnNames = "email"))
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String password;  // always store hashed!

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private UserRole role = UserRole.USER;

    @Column(nullable = false)
    private boolean active = true;

    @CreationTimestamp
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Post> posts = new ArrayList<>();

    // constructors, getters, setters...
}

public enum UserRole {
    USER, ADMIN, MODERATOR
}

Repository Interface

package com.example.demo.repository;

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.util.Optional;
import java.util.List;

// JpaRepository<Entity, PrimaryKey> provides:
// save(), findById(), findAll(), deleteById(), count(), existsById(), etc.
public interface UserRepository extends JpaRepository<User, Long> {

    // Query by convention — Spring generates SQL automatically
    Optional<User> findByEmail(String email);

    boolean existsByEmail(String email);

    List<User> findByActiveTrue();

    List<User> findByRoleOrderByCreatedAtDesc(UserRole role);

    // Pagination and sorting
    Page<User> findByActive(boolean active, Pageable pageable);

    // Custom JPQL query
    @Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true")
    Optional<User> findActiveUserByEmail(@Param("email") String email);

    // Native SQL query
    @Query(value = "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '7 days'",
           nativeQuery = true)
    List<User> findRecentUsers();

    // Modifying query
    @Modifying
    @Query("UPDATE User u SET u.active = false WHERE u.id = :id")
    int deactivateUser(@Param("id") Long id);
}

Service Layer with JPA

@Service
@Transactional(readOnly = true)  // default: read-only transactions
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserService(UserRepository userRepository,
                       PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    public Page<UserDto> findAll(int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        return userRepository.findAll(pageable).map(this::toDto);
    }

    public UserDto findById(Long id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("User not found: " + id));
        return toDto(user);
    }

    @Transactional  // write transaction
    public UserDto create(CreateUserRequest request) {
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new ConflictException("Email already registered: " + request.getEmail());
        }
        User user = new User();
        user.setName(request.getName());
        user.setEmail(request.getEmail().toLowerCase());
        user.setPassword(passwordEncoder.encode(request.getPassword()));
        return toDto(userRepository.save(user));
    }

    private UserDto toDto(User user) {
        return new UserDto(user.getId(), user.getName(), user.getEmail(), user.getCreatedAt());
    }
}

Exception Handling: @ControllerAdvice and @ExceptionHandler

Centralize exception handling with @RestControllerAdvice. This eliminates try-catch blocks in controllers and ensures consistent error response format across the entire API.

package com.example.demo.exception;

// Custom exception classes
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

public class ConflictException extends RuntimeException {
    public ConflictException(String message) {
        super(message);
    }
}

// Error response body
public record ApiError(
    int status,
    String error,
    String message,
    String path,
    LocalDateTime timestamp
) {}

// Global exception handler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiError> handleNotFound(
            ResourceNotFoundException ex, HttpServletRequest request) {
        log.warn("Resource not found: {}", ex.getMessage());
        ApiError error = new ApiError(
            404, "Not Found", ex.getMessage(),
            request.getRequestURI(), LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(ConflictException.class)
    public ResponseEntity<ApiError> handleConflict(
            ConflictException ex, HttpServletRequest request) {
        ApiError error = new ApiError(
            409, "Conflict", ex.getMessage(),
            request.getRequestURI(), LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
    }

    // Handle @Valid validation failures
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidation(
            MethodArgumentNotValidException ex) {
        Map<String, String> fieldErrors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
            fieldErrors.put(error.getField(), error.getDefaultMessage())
        );
        Map<String, Object> body = Map.of(
            "status", 400,
            "error", "Validation Failed",
            "fields", fieldErrors,
            "timestamp", LocalDateTime.now()
        );
        return ResponseEntity.badRequest().body(body);
    }

    // Catch-all for unexpected errors
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiError> handleGeneral(
            Exception ex, HttpServletRequest request) {
        log.error("Unhandled exception", ex);
        ApiError error = new ApiError(
            500, "Internal Server Error", "An unexpected error occurred",
            request.getRequestURI(), LocalDateTime.now()
        );
        return ResponseEntity.internalServerError().body(error);
    }
}

Spring Security: JWT Authentication

Spring Security 6 (Spring Boot 3) uses a lambda-based SecurityFilterChain configuration. The older WebSecurityConfigurerAdapter is removed. Here is a complete JWT setup:

JWT Utility Class

// Add to pom.xml: io.jsonwebtoken:jjwt-api:0.12.3 + jjwt-impl + jjwt-jackson
@Component
public class JwtUtil {

    @Value("${spring.security.jwt.secret}")
    private String secretKey;

    @Value("${spring.security.jwt.expiration}")
    private long expiration;

    private SecretKey getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }

    public String generateToken(UserDetails userDetails) {
        return Jwts.builder()
            .subject(userDetails.getUsername())
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + expiration))
            .signWith(getSigningKey())
            .compact();
    }

    public String extractUsername(String token) {
        return Jwts.parser()
            .verifyWith(getSigningKey())
            .build()
            .parseSignedClaims(token)
            .getPayload()
            .getSubject();
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        return Jwts.parser()
            .verifyWith(getSigningKey())
            .build()
            .parseSignedClaims(token)
            .getPayload()
            .getExpiration()
            .before(new Date());
    }
}

JWT Authentication Filter

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        final String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        final String jwt = authHeader.substring(7);  // remove "Bearer "
        final String username = jwtUtil.extractUsername(jwt);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtUtil.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities()
                    );
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

Security Configuration

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;
    private final UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())  // stateless API — no CSRF needed
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/v1/posts/**").permitAll()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}

// Auth Controller — login and register
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;

    @PostMapping("/register")
    public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
        return ResponseEntity.status(HttpStatus.CREATED).body(authService.register(request));
    }

    @PostMapping("/login")
    public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
        return ResponseEntity.ok(authService.login(request));
    }
}

Spring Boot Actuator and Monitoring

Spring Boot Actuator provides production-ready endpoints for health checks, metrics, environment info, and more. It integrates natively with Prometheus and Micrometer for cloud-native observability.

# application.yml — Actuator configuration
management:
  endpoints:
    web:
      base-path: /actuator
      exposure:
        include: health,info,metrics,prometheus,env,loggers,threaddump
  endpoint:
    health:
      show-details: when-authorized
      show-components: always
  info:
    env:
      enabled: true
    git:
      mode: full

info:
  app:
    name: ${spring.application.name}
    version: "@project.version@"
    java-version: "@java.version@"
# Available Actuator endpoints
GET /actuator/health        # {"status":"UP","components":{...}}
GET /actuator/info          # application info, git info
GET /actuator/metrics       # list available metrics
GET /actuator/metrics/jvm.memory.used  # specific metric
GET /actuator/prometheus    # Prometheus-format metrics
GET /actuator/env           # environment properties
GET /actuator/loggers       # current log levels
POST /actuator/loggers/com.example  # change log level at runtime
GET /actuator/threaddump    # current thread dump
// Custom health indicator
@Component
public class DatabaseHealthIndicator implements HealthIndicator {

    private final DataSource dataSource;

    public DatabaseHealthIndicator(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public Health health() {
        try (Connection conn = dataSource.getConnection()) {
            return Health.up()
                .withDetail("database", conn.getMetaData().getDatabaseProductName())
                .withDetail("url", conn.getMetaData().getURL())
                .build();
        } catch (SQLException e) {
            return Health.down().withException(e).build();
        }
    }
}

// Custom metrics with Micrometer
@Service
@RequiredArgsConstructor
public class OrderService {

    private final MeterRegistry meterRegistry;
    private final Counter orderCreatedCounter;

    public OrderService(MeterRegistry registry) {
        this.meterRegistry = registry;
        this.orderCreatedCounter = Counter.builder("orders.created")
            .description("Total orders created")
            .register(registry);
    }

    public Order createOrder(CreateOrderRequest request) {
        Order order = processOrder(request);
        orderCreatedCounter.increment();
        meterRegistry.gauge("orders.pending", getPendingOrderCount());
        return order;
    }
}

Testing with JUnit 5 and MockMvc

Spring Boot provides excellent testing support out of the box. Use @SpringBootTest for integration tests and @WebMvcTest for controller slice tests with MockMvc.

Unit Testing with Mockito

// Pure unit test — no Spring context, fast
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private PasswordEncoder passwordEncoder;

    @InjectMocks
    private UserService userService;

    @Test
    void createUser_Success() {
        // Arrange
        CreateUserRequest request = new CreateUserRequest("Alice", "alice@example.com", "password123");
        User savedUser = new User(1L, "Alice", "alice@example.com", "hashed");

        when(userRepository.existsByEmail(anyString())).thenReturn(false);
        when(passwordEncoder.encode(anyString())).thenReturn("hashed");
        when(userRepository.save(any(User.class))).thenReturn(savedUser);

        // Act
        UserDto result = userService.create(request);

        // Assert
        assertThat(result.name()).isEqualTo("Alice");
        assertThat(result.email()).isEqualTo("alice@example.com");
        verify(userRepository).save(any(User.class));
    }

    @Test
    void createUser_DuplicateEmail_ThrowsConflict() {
        // Arrange
        CreateUserRequest request = new CreateUserRequest("Alice", "alice@example.com", "password123");
        when(userRepository.existsByEmail("alice@example.com")).thenReturn(true);

        // Act + Assert
        assertThatThrownBy(() -> userService.create(request))
            .isInstanceOf(ConflictException.class)
            .hasMessageContaining("alice@example.com");
    }
}

Controller Tests with MockMvc

// Slice test — only loads web layer (fast)
@WebMvcTest(UserController.class)
@AutoConfigureMockMvc(addFilters = false)  // disable security for unit tests
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private UserService userService;

    @Test
    void getUser_ExistingId_Returns200() throws Exception {
        UserDto user = new UserDto(1L, "Alice", "alice@example.com", LocalDateTime.now());
        when(userService.findById(1L)).thenReturn(user);

        mockMvc.perform(get("/api/v1/users/1")
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("Alice"))
            .andExpect(jsonPath("$.email").value("alice@example.com"));
    }

    @Test
    void getUser_NotFound_Returns404() throws Exception {
        when(userService.findById(999L))
            .thenThrow(new ResourceNotFoundException("User not found: 999"));

        mockMvc.perform(get("/api/v1/users/999"))
            .andExpect(status().isNotFound())
            .andExpect(jsonPath("$.status").value(404));
    }

    @Test
    void createUser_ValidRequest_Returns201() throws Exception {
        CreateUserRequest request = new CreateUserRequest("Bob", "bob@example.com", "password123");
        UserDto created = new UserDto(2L, "Bob", "bob@example.com", LocalDateTime.now());
        when(userService.create(any())).thenReturn(created);

        mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id").value(2L));
    }

    @Test
    void createUser_InvalidEmail_Returns400() throws Exception {
        CreateUserRequest request = new CreateUserRequest("Bob", "not-an-email", "pass");

        mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.fields.email").exists());
    }
}

Integration Tests with @SpringBootTest

// Full Spring context + embedded H2 database
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
@Transactional  // rollback after each test
class UserIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private UserRepository userRepository;

    @Test
    void createAndFetchUser_FullFlow() {
        // Create
        CreateUserRequest request = new CreateUserRequest("Carol", "carol@test.com", "password123");
        ResponseEntity<UserDto> createResponse = restTemplate.postForEntity(
            "/api/v1/users", request, UserDto.class);

        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        Long userId = createResponse.getBody().id();

        // Fetch
        ResponseEntity<UserDto> getResponse = restTemplate.getForEntity(
            "/api/v1/users/" + userId, UserDto.class);

        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(getResponse.getBody().email()).isEqualTo("carol@test.com");
    }
}

// Run tests
// ./mvnw test                          — run all tests
// ./mvnw test -pl :users-service       — run specific module
// ./mvnw verify                        — run tests + integration tests
// ./mvnw test -Dtest=UserServiceTest   — run specific test class

Docker Deployment with Spring Boot

Spring Boot applications package as executable fat JARs, making Docker deployment straightforward. Spring Boot 3.1+ also supports buildpacks natively for creating optimized OCI images without a Dockerfile.

Multi-stage Dockerfile

# Multi-stage build: compile in full JDK, run in slim JRE
# Stage 1: Build
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app

# Cache Maven dependencies separately
COPY pom.xml .
RUN mvn dependency:go-offline -q

# Build the application
COPY src ./src
RUN mvn package -DskipTests -q

# Stage 2: Extract layers for better caching (Spring Boot layered JARs)
FROM eclipse-temurin:21-jre AS extractor
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

# Stage 3: Final runtime image
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app

# Copy extracted layers (most stable first for layer caching)
COPY --from=extractor /app/dependencies/ ./
COPY --from=extractor /app/spring-boot-loader/ ./
COPY --from=extractor /app/snapshot-dependencies/ ./
COPY --from=extractor /app/application/ ./

# Non-root user for security
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser

EXPOSE 8080

ENTRYPOINT ["java",   "-XX:+UseContainerSupport",   "-XX:MaxRAMPercentage=75.0",   "org.springframework.boot.loader.launch.JarLauncher"]

Docker Compose for Local Development

# docker-compose.yml
version: "3.9"

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: docker
      DATABASE_URL: jdbc:postgresql://db:5432/mydb
      DB_USERNAME: postgres
      DB_PASSWORD: secret
      JWT_SECRET: my-docker-jwt-secret-change-in-prod
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres-data:
# Build image without Dockerfile (Spring Boot Buildpacks)
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myapp:latest

# Run with Docker Compose
docker compose up -d

# View logs
docker compose logs -f app

# Scale the app service
docker compose up -d --scale app=3

Spring Boot vs NestJS vs Express: Framework Comparison

Choosing between Spring Boot, NestJS, and Express depends on your team, language preference, and application requirements. Here is a comprehensive side-by-side comparison:

FeatureSpring BootNestJSExpress.js
LanguageJava / KotlinTypeScript / JavaScriptJavaScript / TypeScript
ArchitectureOpinionated, annotation-drivenOpinionated, decorator-drivenMinimal, unopinionated
PerformanceHigh (JVM JIT); slow startup without GraalVMGood (Node.js event loop)Fast startup, high RPS for I/O
DI ContainerBuilt-in (mature, powerful)Built-in (inspired by Angular)None (use InversifyJS manually)
Database ORMSpring Data JPA / HibernateTypeORM, Prisma, MongoosePrisma, Sequelize, Knex
AuthSpring Security (JWT, OAuth2)@nestjs/passport + JWTPassport.js, custom middleware
TestingJUnit 5, MockMvc, MockitoJest, SupertestJest, Mocha, Supertest
Learning CurveSteep (Java + Spring ecosystem)Moderate (TypeScript + decorators)Low (minimal abstractions)
Enterprise AdoptionVery High (banking, fintech, large corps)Growing (startups to mid-size)High (microservices, BFFs)
MicroservicesSpring Cloud ecosystem (Eureka, Gateway, Config)Built-in microservice transportsManual setup with libraries
Best ForEnterprise apps, Java teams, complex domainsTypeScript full-stack teams, structured APIsSimple APIs, prototypes, microservices

Spring Boot Profiles and Environment-Specific Configuration

Spring Profiles allow beans and configuration to be activated conditionally based on the active environment. This is essential for managing dev, staging, and production configurations.

// @Profile — bean only active in specific profiles
@Configuration
@Profile("dev")
public class DevDataInitializer implements CommandLineRunner {

    private final UserRepository userRepository;

    @Override
    public void run(String... args) {
        // Seed test data in development only
        if (userRepository.count() == 0) {
            userRepository.save(new User("Admin", "admin@dev.local", "admin123", UserRole.ADMIN));
        }
    }
}

// @ConditionalOnProperty — activate based on property value
@Bean
@ConditionalOnProperty(name = "feature.email-notifications", havingValue = "true")
public EmailNotificationService emailNotificationService() {
    return new SmtpEmailService();
}

@Bean
@ConditionalOnMissingBean(EmailNotificationService.class)
public EmailNotificationService noopEmailService() {
    return new NoopEmailService();  // fallback when email disabled
}
# Activate profiles
# Option 1: application.yml
spring.profiles.active: prod

# Option 2: Environment variable (recommended for Docker/K8s)
SPRING_PROFILES_ACTIVE=prod java -jar app.jar

# Option 3: Command line argument
java -jar app.jar --spring.profiles.active=prod

# Option 4: Multiple profiles
java -jar app.jar --spring.profiles.active=prod,aws

Summary and Next Steps

Spring Boot remains the dominant Java backend framework in 2026, powering applications from simple REST APIs to complex distributed systems with Spring Cloud. Its combination of convention-over-configuration, a mature ecosystem, battle-tested security model, and first-class testing support make it the go-to choice for enterprise Java development.

From here, explore the Spring ecosystem further: Spring Cloud Gateway for API gateways, Spring Batch for bulk data processing, Spring WebFlux for reactive applications with Project Reactor, and Spring Cloud for microservices patterns like service discovery (Eureka), distributed configuration (Config Server), and circuit breakers (Resilience4j).

𝕏 Twitterin LinkedIn
War das hilfreich?

Bleiben Sie informiert

Wöchentliche Dev-Tipps und neue Tools.

Kein Spam. Jederzeit abbestellbar.

Verwandte Tools ausprobieren

{ }JSON FormatterJWTJWT DecoderB→Base64 Encoder

Verwandte Artikel

NestJS Komplett-Leitfaden: Module, Controller, Services, DI, TypeORM, JWT Auth und Testing

NestJS von Grund auf meistern. Module, Controller, Services, Provider, Dependency Injection, TypeORM/Prisma, JWT-Authentifizierung, Guards, Pipes, Interceptors, Exception Filter und Jest-Tests.

API Design Leitfaden: REST Best Practices, OpenAPI, Auth, Paginierung und Caching

API Design meistern. REST-Prinzipien, Versionierungsstrategien, JWT/OAuth 2.0, OpenAPI/Swagger, Rate Limiting, RFC 7807 Fehlerbehandlung, Paginierung, ETags und REST vs GraphQL vs gRPC vs tRPC.

Datenbankdesign Leitfaden: Normalisierung, ERD, Indizierung, SQL vs NoSQL und Performance

Datenbankdesign meistern. Normalisierung (1NF-BCNF), ER-Diagramme, Primär-/Fremdschlüssel, Indizierungsstrategien, SQL vs NoSQL, ACID-Transaktionen, reale Schemata und PostgreSQL-Optimierung.