Spring Boot 3.4: All New Features Explained

Explore Spring Boot 3.4 new features: structured logging, virtual threads, graceful shutdown by default, improved RestClient and MockMvcTester.

Spring Boot 3.4 new features and improvements

Spring Boot 3.4, released in November 2024, delivers significant improvements for developer productivity and application performance. This version introduces native structured logging, enables graceful shutdown by default, and expands virtual thread support across the framework.

Prerequisites

Spring Boot 3.4 requires Java 17 minimum and supports Java 21 for virtual threads. This version uses Spring Framework 6.2.

Native Structured Logging

Structured logging represents a major advancement for application observability. Instead of text-based logs that are difficult to parse, Spring Boot 3.4 generates JSON logs consumable by tools like Elasticsearch, Grafana Loki, or Datadog.

Three formats are natively supported: Elastic Common Schema (ECS), Logstash, and Graylog Extended Log Format (GELF).

properties
# application.properties
# Enable structured logging in console
logging.structured.format.console=ecs

# Or for log files
logging.structured.format.file=logstash

This simple configuration produces automatically formatted structured JSON logs.

LoggingController.javajava
@RestController
@RequestMapping("/api/demo")
public class LoggingController {

    // Logger injection via SLF4J
    private static final Logger logger = LoggerFactory.getLogger(LoggingController.class);

    @GetMapping("/action")
    public ResponseEntity<String> performAction(@RequestParam String userId) {
        // Log will be automatically formatted as structured JSON
        logger.info("Action performed by user: {}", userId);
        return ResponseEntity.ok("Action completed");
    }
}

With ECS format enabled, this log becomes a JSON object containing timestamp, level, message, class name, thread, and application metadata. This structure simplifies searching and aggregation in monitoring platforms.

Graceful Shutdown Enabled by Default

Important change: graceful shutdown is now enabled by default. In-flight HTTP requests are processed before server shutdown, preventing 502 errors during deployments.

properties
# application.properties
# Graceful shutdown is now ON by default
# To restore previous behavior (immediate shutdown):
server.shutdown=immediate

# Configure maximum wait timeout (30s default)
spring.lifecycle.timeout-per-shutdown-phase=45s

This behavior applies to all embedded servers: Tomcat, Jetty, Undertow, and Reactor Netty.

LifecycleConfig.javajava
@Configuration
public class LifecycleConfig {

    private static final Logger logger = LoggerFactory.getLogger(LifecycleConfig.class);

    @Bean
    public ApplicationListener<ContextClosedEvent> gracefulShutdownListener() {
        // This listener executes at shutdown start
        return event -> {
            logger.info("Graceful shutdown initiated - completing in-flight requests");
            // Custom logic: close connections, save state, etc.
        };
    }
}

This improvement makes zero-downtime deployments simpler to implement without additional configuration in most cases.

Extended Virtual Threads Support

Spring Boot 3.4 extends virtual thread support (Java 21+) to more components. OtlpMeterRegistry and the Undertow server now use virtual threads when enabled.

properties
# application.properties
# Enable virtual threads globally
spring.threads.virtual.enabled=true

This single property transforms the application's threading model. Each HTTP request gets its own virtual thread, enabling thousands of concurrent connections without exhausting the thread pool.

AsyncService.javajava
@Service
public class AsyncService {

    private static final Logger logger = LoggerFactory.getLogger(AsyncService.class);

    // With virtual threads enabled, each blocking call
    // no longer consumes an OS thread
    public String fetchExternalData() {
        logger.info("Executing on thread: {}", Thread.currentThread());

        // Blocking HTTP call - with virtual threads,
        // the OS thread is released during I/O wait
        return restClient.get()
            .uri("https://api.external.com/data")
            .retrieve()
            .body(String.class);
    }
}

Virtual threads shine for I/O-bound applications: HTTP calls, database queries, file operations. The OS thread is released during wait time, then reassigned upon resumption.

Compatibility

Virtual threads require Java 21 minimum. On Java 17, this property is ignored and classic behavior applies.

Improved RestClient and RestTemplate

Spring Boot 3.4 normalizes HTTP client configuration. HttpRequestFactory selection now follows a clear precedence based on classpath.

HttpClientConfig.javajava
@Configuration
public class HttpClientConfig {

    @Bean
    public RestClient restClient(RestClient.Builder builder) {
        // Spring Boot automatically chooses implementation:
        // 1. Apache HTTP Components (if present)
        // 2. Jetty Client
        // 3. Reactor Netty
        // 4. JDK HttpClient (Java 11+)
        // 5. SimpleClientHttpRequestFactory (fallback)
        return builder
            .baseUrl("https://api.example.com")
            .defaultHeader("Accept", "application/json")
            .build();
    }
}

Redirect behavior configuration is also simplified.

properties
# application.properties
# Force specific implementation
spring.http.client.factory=jdk

# Configure redirect behavior
spring.http.client.redirects=dont-follow

For fine-grained control, the new ClientHttpRequestFactoryBuilder enables complete programmatic configuration.

CustomHttpClientConfig.javajava
@Configuration
public class CustomHttpClientConfig {

    @Bean
    public RestClient customRestClient(ClientHttpRequestFactoryBuilder factoryBuilder) {
        // Advanced factory configuration
        ClientHttpRequestFactory factory = factoryBuilder
            .httpComponents()
            .connectTimeout(Duration.ofSeconds(5))
            .readTimeout(Duration.ofSeconds(30))
            .build();

        return RestClient.builder()
            .requestFactory(factory)
            .baseUrl("https://api.example.com")
            .build();
    }
}

This approach offers maximum flexibility while maintaining a consistent API regardless of the underlying HTTP implementation.

Ready to ace your Spring Boot interviews?

Practice with our interactive simulators, flashcards, and technical tests.

MockMvcTester for Fluent Testing

Spring Boot 3.4 introduces MockMvcTester, an AssertJ-based alternative to MockMvc. This new approach makes tests more readable and expressive.

UserControllerTest.javajava
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvcTester mockMvc; // New class!

    @Test
    void shouldReturnUserById() {
        // Fluent API with AssertJ
        mockMvc.get().uri("/api/users/{id}", 1)
            .assertThat()
            .hasStatusOk()
            .hasContentType(MediaType.APPLICATION_JSON)
            .bodyJson()
            .extractingPath("$.name")
            .isEqualTo("John Doe");
    }

    @Test
    void shouldReturn404ForUnknownUser() {
        mockMvc.get().uri("/api/users/{id}", 999)
            .assertThat()
            .hasStatus(HttpStatus.NOT_FOUND)
            .bodyJson()
            .extractingPath("$.error")
            .isEqualTo("User not found");
    }
}

Compared to the old approach with MockMvc, assertions are more concise and chaining feels more natural.

ComparisonTest.javajava
@WebMvcTest(UserController.class)
class ComparisonTest {

    @Autowired
    private MockMvcTester mockMvcTester;

    @Autowired
    private MockMvc mockMvc;

    @Test
    void withMockMvcTester() {
        // New approach: fluent and concise
        mockMvcTester.post().uri("/api/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"name\": \"Jane\"}")
            .assertThat()
            .hasStatus(HttpStatus.CREATED)
            .hasHeader("Location", "/api/users/2");
    }

    @Test
    void withClassicMockMvc() throws Exception {
        // Old approach: more verbose
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"name\": \"Jane\"}"))
            .andExpect(status().isCreated())
            .andExpect(header().string("Location", "/api/users/2"));
    }
}

MockMvcTester is auto-configured when AssertJ is present on classpath (included by default with spring-boot-starter-test).

Enhanced Docker Compose and Testcontainers

Docker Compose support gains flexibility with multiple configuration files and custom arguments.

properties
# application.properties
# Use multiple Docker Compose files
spring.docker.compose.file=compose.yaml,compose-dev.yaml

# Pass arguments at startup
spring.docker.compose.start.arguments=--scale redis=2

# Arguments at shutdown
spring.docker.compose.stop.arguments=--timeout 60

New services are automatically detected and configured.

yaml
# compose.yaml
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"

  redis-stack:
    image: redis/redis-stack:latest
    ports:
      - "6379:6379"
      - "8001:8001" # RedisInsight UI

  grafana-lgtm:
    image: grafana/otel-lgtm:latest
    ports:
      - "3000:3000"  # Grafana
      - "4317:4317"  # OTLP gRPC

Spring Boot 3.4 automatically detects these services and configures corresponding connection properties.

For testing with Testcontainers, new containers are supported.

IntegrationTestConfig.javajava
@TestConfiguration(proxyBeanMethods = false)
public class IntegrationTestConfig {

    @Bean
    @ServiceConnection
    public PostgreSQLContainer<?> postgresContainer() {
        return new PostgreSQLContainer<>("postgres:16");
    }

    @Bean
    @ServiceConnection
    public RedisStackContainer redisStackContainer() {
        // New Redis Stack support
        return new RedisStackContainer("redis/redis-stack:latest");
    }

    @Bean
    @ServiceConnection
    public LgtmStackContainer observabilityContainer() {
        // New Grafana LGTM support (Loki, Grafana, Tempo, Mimir)
        return new LgtmStackContainer("grafana/otel-lgtm:latest");
    }
}

The @ServiceConnection annotation automatically configures connection properties, eliminating the need for @DynamicPropertySource in most cases.

Actuator SSL and Observability

The new /actuator/info endpoint now exposes information about configured SSL certificates: validity dates, issuer, and subject.

properties
# application.properties
# Enable SSL information in actuator
management.info.ssl.enabled=true

# Configure warning threshold for expiring certificates
management.health.ssl.certificate-validity-warning-threshold=30d

This feature enables certificate expiration monitoring directly via actuator, facilitating renewal automation.

SslMonitoringConfig.javajava
@Configuration
public class SslMonitoringConfig {

    @Bean
    public HealthIndicator sslCertificateHealth(SslInfo sslInfo) {
        return () -> {
            // Custom certificate validity check
            boolean allValid = sslInfo.getBundles().values().stream()
                .flatMap(bundle -> bundle.getCertificates().stream())
                .allMatch(cert -> cert.getValidityEnds().isAfter(Instant.now()));

            return allValid
                ? Health.up().build()
                : Health.down().withDetail("reason", "Certificate expiring soon").build();
        };
    }
}

For observability, OTLP transport now supports gRPC in addition to HTTP.

properties
# application.properties
# Use gRPC for OTLP (traces and metrics)
management.otlp.tracing.transport=grpc
management.otlp.tracing.endpoint=http://localhost:4317

# New: group applications together
spring.application.group=payment-services

Application group (spring.application.group) allows logical grouping of multiple services in observability dashboards.

Lighter OCI Images

The default OCI image builder changes from paketobuildpacks/builder-jammy-base to paketobuildpacks/builder-jammy-java-tiny, producing significantly smaller images.

build.gradlegroovy
tasks.named("bootBuildImage") {
    // Native ARM support (new)
    imagePlatform = "linux/arm64"

    // New security flag
    trustBuilder = false

    // Image configuration
    imageName = "myregistry.com/myapp:${version}"
}

The new imagePlatform parameter simplifies cross-platform builds for ARM and x64.

xml
<!-- pom.xml -->
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <image>
            <!-- Native ARM support -->
            <platform>linux/arm64</platform>
            <!-- Builder optimized for minimal images -->
            <builder>paketobuildpacks/builder-jammy-java-tiny</builder>
        </image>
    </configuration>
</plugin>

These optimized images start faster and consume fewer resources, particularly beneficial for Kubernetes deployments.

Migration Note

The builder change may affect applications depending on certain system tools. Test new images before deploying to production.

Bean Validation Changes

Spring Boot 3.4 aligns validation behavior with the Bean Validation specification. Validation no longer automatically cascades to nested properties.

ConfigProperties.javajava
@ConfigurationProperties(prefix = "app")
@Validated
public class AppConfig {

    @NotBlank
    private String name;

    // IMPORTANT: @Valid required to cascade validation
    @Valid
    private DatabaseConfig database;

    // Without @Valid, ServerConfig constraints will NOT be checked
    private ServerConfig server;

    // Getters and setters
}

public class DatabaseConfig {
    @NotBlank
    private String url;

    @Min(1)
    private int poolSize;

    // Getters and setters
}

public class ServerConfig {
    @NotNull // This constraint will NOT be checked without @Valid on parent
    private Integer port;

    // Getters and setters
}

This change may break existing applications. Audit @ConfigurationProperties classes and add @Valid where validation should cascade.

@MockBean and @SpyBean Deprecation

Spring Boot's @MockBean and @SpyBean annotations are deprecated in favor of new Mockito annotations.

UserServiceTest.java (new approach)java
@SpringBootTest
class UserServiceTest {

    // New: native Mockito annotations
    @MockitoBean
    private UserRepository userRepository;

    @MockitoSpyBean
    private EmailService emailService;

    @Autowired
    private UserService userService;

    @Test
    void shouldCreateUser() {
        // Mock configuration
        when(userRepository.save(any())).thenReturn(new User(1L, "test@example.com"));

        userService.createUser("test@example.com");

        // Spy verification
        verify(emailService).sendWelcomeEmail("test@example.com");
    }
}

Old annotations still work but display deprecation warnings. Plan migration to @MockitoBean and @MockitoSpyBean.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Conclusion

Spring Boot 3.4 delivers substantial improvements for productivity and observability:

Migration checklist:

  • ✅ Enable structured logging for improved observability
  • ✅ Verify graceful shutdown behavior (now enabled by default)
  • ✅ Consider virtual threads for I/O-bound applications (Java 21+)
  • ✅ Migrate to MockMvcTester for more readable tests
  • ✅ Add @Valid on nested @ConfigurationProperties properties
  • ✅ Replace @MockBean/@SpyBean with @MockitoBean/@MockitoSpyBean
  • ✅ Test new OCI images before production deployment

Spring Boot 3.4 consolidates the framework's position as the reference for modern Java development, with particular attention to observability standards and performance.

Sources:

Tags

#spring boot
#java
#spring boot 3.4
#spring framework
#backend

Share

Related articles