diff --git a/spring-boot-modules/spring-boot-4/pom.xml b/spring-boot-modules/spring-boot-4/pom.xml
index 1463a385000f..1fa59f2fb97e 100644
--- a/spring-boot-modules/spring-boot-4/pom.xml
+++ b/spring-boot-modules/spring-boot-4/pom.xml
@@ -14,7 +14,7 @@
1.0.0-SNAPSHOT
-
+
org.springframework.boot
spring-boot-devtools
@@ -26,6 +26,18 @@
spring-boot-configuration-processor
true
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
org.projectlombok
lombok
@@ -37,9 +49,18 @@
${mapstruct.version}
true
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ com.h2database
+ h2
+
org.springframework.boot
spring-boot-starter-test
+ test
@@ -128,42 +149,9 @@
-
-
- repository.spring.milestone
- Spring Snapshot Repository
- https://repo.spring.io/milestone
-
-
- repository.spring.snapshot
- Spring Snapshot Repository
- https://repo.spring.io/snapshot
-
- true
- daily
-
-
-
-
-
- repository.spring.milestone.plugins
- Spring Snapshot Repository
- https://repo.spring.io/milestone
-
-
- repository.spring.snapshot.plugins
- Spring Snapshot Repository
- https://repo.spring.io/snapshot
-
- true
- daily
-
-
-
-
1.6.3
- 4.0.0-SNAPSHOT
+ 4.0.0-M2
1.5.18
0.2.0
diff --git a/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/beanregistrar/SampleApplication.java b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/SampleApplication.java
similarity index 59%
rename from spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/beanregistrar/SampleApplication.java
rename to spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/SampleApplication.java
index 07c2684e3e22..44e5bb9be9f9 100644
--- a/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/beanregistrar/SampleApplication.java
+++ b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/SampleApplication.java
@@ -1,9 +1,13 @@
-package com.baeldung.spring.beanregistrar;
+package com.baeldung.spring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.resilience.annotation.EnableResilientMethods;
+import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
+@EnableResilientMethods
+@EnableAsync
public class SampleApplication {
public static void main(String[] args) {
diff --git a/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/ApiConfig.java b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/ApiConfig.java
new file mode 100644
index 000000000000..50a3ab95109b
--- /dev/null
+++ b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/ApiConfig.java
@@ -0,0 +1,24 @@
+package com.baeldung.spring.mvc;
+
+import org.jspecify.annotations.NonNull;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.method.HandlerTypePredicate;
+import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;
+import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class ApiConfig implements WebMvcConfigurer {
+
+ @Override
+ public void configureApiVersioning(@NonNull ApiVersionConfigurer configurer) {
+ configurer.usePathSegment(1);
+ }
+
+ @Override
+ public void configurePathMatch(PathMatchConfigurer configurer) {
+ configurer.addPathPrefix("/api/v{version}", HandlerTypePredicate.forAnnotation(RestController.class));
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/ChristmasJoyClient.java b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/ChristmasJoyClient.java
new file mode 100644
index 000000000000..0e8cab8a6d5a
--- /dev/null
+++ b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/ChristmasJoyClient.java
@@ -0,0 +1,16 @@
+package com.baeldung.spring.mvc;
+
+import org.springframework.resilience.annotation.ConcurrencyLimit;
+import org.springframework.resilience.annotation.Retryable;
+import org.springframework.web.service.annotation.GetExchange;
+import org.springframework.web.service.registry.HttpServiceClient;
+
+@HttpServiceClient("christmasJoy")
+public interface ChristmasJoyClient {
+
+ @GetExchange("/greetings?random")
+ @Retryable(maxAttempts = 3, delay = 100, multiplier = 2, maxDelay = 1000)
+ @ConcurrencyLimit(3)
+ String getRandomGreeting();
+
+}
diff --git a/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldController.java b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldController.java
new file mode 100644
index 000000000000..28b0c1b2d482
--- /dev/null
+++ b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldController.java
@@ -0,0 +1,22 @@
+package com.baeldung.spring.mvc;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/hello")
+public class HelloWorldController {
+
+ @GetMapping(version = "1", produces = MediaType.TEXT_PLAIN_VALUE)
+ public String sayHelloV1() {
+ return "Hello World";
+ }
+
+ @GetMapping(version = "2", produces = MediaType.TEXT_PLAIN_VALUE)
+ public String sayHelloV2() {
+ return "Hi World";
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldEvent.java b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldEvent.java
new file mode 100644
index 000000000000..3f723af959ff
--- /dev/null
+++ b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldEvent.java
@@ -0,0 +1,5 @@
+package com.baeldung.spring.mvc;
+
+public record HelloWorldEvent(String message) {
+
+}
diff --git a/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldEventLogger.java b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldEventLogger.java
new file mode 100644
index 000000000000..aac9c9601cc5
--- /dev/null
+++ b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldEventLogger.java
@@ -0,0 +1,19 @@
+package com.baeldung.spring.mvc;
+
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Component
+@Slf4j
+public class HelloWorldEventLogger {
+
+ @Async
+ @EventListener
+ void logHelloWorldEvent(HelloWorldEvent event) {
+ log.info("Hello World Event: {}", event.message());
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldV3Controller.java b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldV3Controller.java
new file mode 100644
index 000000000000..7f175fae55bb
--- /dev/null
+++ b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldV3Controller.java
@@ -0,0 +1,17 @@
+package com.baeldung.spring.mvc;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping(path = "/hello", version = "3")
+public class HelloWorldV3Controller {
+
+ @GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
+ public String sayHello() {
+ return "Hey World";
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldV4Controller.java b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldV4Controller.java
new file mode 100644
index 000000000000..2d492527d0bb
--- /dev/null
+++ b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HelloWorldV4Controller.java
@@ -0,0 +1,26 @@
+package com.baeldung.spring.mvc;
+
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import lombok.RequiredArgsConstructor;
+
+@RestController
+@RequestMapping(path = "/hello", version = "4")
+@RequiredArgsConstructor
+public class HelloWorldV4Controller {
+
+ private final ChristmasJoyClient christmasJoy;
+ private final ApplicationEventPublisher applicationEventPublisher;
+
+ @GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
+ public String sayHello() {
+ final var result = this.christmasJoy.getRandomGreeting();
+ applicationEventPublisher.publishEvent(new HelloWorldEvent(result));
+ return result;
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HttpClientConfig.java b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HttpClientConfig.java
new file mode 100644
index 000000000000..4f70285e9152
--- /dev/null
+++ b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/HttpClientConfig.java
@@ -0,0 +1,36 @@
+package com.baeldung.spring.mvc;
+
+import java.util.List;
+
+import org.jspecify.annotations.NonNull;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
+import org.springframework.web.service.registry.AbstractClientHttpServiceRegistrar;
+
+@Configuration
+@Import(HttpClientConfig.HelloWorldClientHttpServiceRegistrar.class)
+public class HttpClientConfig {
+
+ static class HelloWorldClientHttpServiceRegistrar extends AbstractClientHttpServiceRegistrar {
+
+ @Override
+ protected void registerHttpServices(@NonNull GroupRegistry registry, @NonNull AnnotationMetadata metadata) {
+ findAndRegisterHttpServiceClients(registry, List.of("com.baeldung.spring.mvc"));
+ }
+ }
+
+ @Bean
+ RestClientHttpServiceGroupConfigurer christmasJoyServiceGroupConfigurer(@Value("${application.rest.services.christmasJoy.baseUrl}") String baseUrl) {
+ return groups -> {
+ groups.filterByName("christmasJoy")
+ .forEachClient((group, clientBuilder) -> {
+ clientBuilder.baseUrl(baseUrl);
+ });
+ };
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/TaskDecoratorConfiguration.java b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/TaskDecoratorConfiguration.java
new file mode 100644
index 000000000000..7ee3346a6d27
--- /dev/null
+++ b/spring-boot-modules/spring-boot-4/src/main/java/com/baeldung/spring/mvc/TaskDecoratorConfiguration.java
@@ -0,0 +1,41 @@
+package com.baeldung.spring.mvc;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.core.task.TaskDecorator;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Configuration
+@Slf4j
+public class TaskDecoratorConfiguration {
+
+ @Bean
+ @Order(2)
+ TaskDecorator loggingTaskConfigurator() {
+ return runnable -> () -> {
+ log.info("Running Task: {}", runnable);
+ try {
+ runnable.run();
+ } finally {
+ log.info("Finished Task: {}", runnable);
+ }
+ };
+ }
+
+ @Bean
+ @Order(1)
+ TaskDecorator measuringTaskConfigurator() {
+ return runnable -> () -> {
+ final var ts1 = System.currentTimeMillis();
+ try {
+ runnable.run();
+ } finally {
+ final var ts2 = System.currentTimeMillis();
+ log.info("Finished within {}ms (Task: {})", ts2 - ts1, runnable);
+ }
+ };
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-4/src/main/resources/application.properties b/spring-boot-modules/spring-boot-4/src/main/resources/application.properties
new file mode 100644
index 000000000000..007c1e5d5fcf
--- /dev/null
+++ b/spring-boot-modules/spring-boot-4/src/main/resources/application.properties
@@ -0,0 +1 @@
+application.rest.services.christmasJoy.baseUrl=https://christmasjoy.dev/api
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-4/src/test/java/com/baeldung/spring/mvc/HelloWorldApiIntegrationTest.java b/spring-boot-modules/spring-boot-4/src/test/java/com/baeldung/spring/mvc/HelloWorldApiIntegrationTest.java
new file mode 100644
index 000000000000..ffbf4cfceb1e
--- /dev/null
+++ b/spring-boot-modules/spring-boot-4/src/test/java/com/baeldung/spring/mvc/HelloWorldApiIntegrationTest.java
@@ -0,0 +1,62 @@
+package com.baeldung.spring.mvc;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.client.RestTestClient;
+import org.springframework.web.context.WebApplicationContext;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class HelloWorldApiIntegrationTest {
+
+ RestTestClient client;
+
+ @BeforeEach
+ void setUp(WebApplicationContext context) {
+ client = RestTestClient.bindToApplicationContext(context)
+ .build();
+ }
+
+ @Test
+ void shouldFetchHelloV1() {
+ client.get()
+ .uri("/api/v1/hello")
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectHeader()
+ .contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
+ .expectBody(String.class)
+ .consumeWith(message -> assertThat(message.getResponseBody()).containsIgnoringCase("hello"));
+ }
+
+ @Test
+ void shouldFetchHelloV2() {
+ client.get()
+ .uri("/api/v2/hello")
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectHeader()
+ .contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
+ .expectBody(String.class)
+ .consumeWith(message -> assertThat(message.getResponseBody()).containsIgnoringCase("hi"));
+ }
+
+ @Test
+ void shouldFetchHelloV3() {
+ client.get()
+ .uri("/api/v3/hello")
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectHeader()
+ .contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
+ .expectBody(String.class)
+ .consumeWith(message -> assertThat(message.getResponseBody()).containsIgnoringCase("hey"));
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-4/src/test/java/com/baeldung/spring/mvc/HelloWorldApiV4IntegrationTest.java b/spring-boot-modules/spring-boot-4/src/test/java/com/baeldung/spring/mvc/HelloWorldApiV4IntegrationTest.java
new file mode 100644
index 000000000000..412884fdbda6
--- /dev/null
+++ b/spring-boot-modules/spring-boot-4/src/test/java/com/baeldung/spring/mvc/HelloWorldApiV4IntegrationTest.java
@@ -0,0 +1,45 @@
+package com.baeldung.spring.mvc;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.client.RestTestClient;
+import org.springframework.web.context.WebApplicationContext;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@MockitoBean(types = ChristmasJoyClient.class)
+class HelloWorldApiV4IntegrationTest {
+
+ RestTestClient client;
+
+ @Autowired
+ ChristmasJoyClient christmasJoy;
+
+ @BeforeEach
+ void setUp(WebApplicationContext context) {
+ client = RestTestClient.bindToApplicationContext(context)
+ .build();
+ }
+
+ @Test
+ void shouldFetchHello() {
+ Mockito.when(christmasJoy.getRandomGreeting())
+ .thenReturn("Joy to the World");
+ client.get()
+ .uri("/api/v4/hello")
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectHeader()
+ .contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
+ .expectBody(String.class)
+ .consumeWith(message -> assertThat(message.getResponseBody()).isEqualTo("Joy to the World"));
+ }
+
+}