diff --git a/spring-core-6/pom.xml b/spring-core-6/pom.xml
index 2df7167ca1..a3dda0374f 100644
--- a/spring-core-6/pom.xml
+++ b/spring-core-6/pom.xml
@@ -10,27 +10,39 @@
http://www.baeldung.com
- com.baeldung
- parent-modules
- 1.0.0-SNAPSHOT
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.0.1
+
org.springframework.boot
spring-boot-starter-web
- ${spring.boot.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.mock-server
+ mockserver-netty
+ ${mockserver.version}
+
+
+ org.mock-server
+ mockserver-client-java
+ ${mockserver.version}
org.springframework.boot
spring-boot-starter-test
- ${spring.boot.version}
test
- org.junit.jupiter
- junit-jupiter-api
- ${junit-jupiter.version}
+ io.projectreactor
+ reactor-test
test
@@ -76,13 +88,23 @@
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 17
+ 17
+
+
+
UTF-8
- 11
- 11
- 2.7.5
+ 17
+ 17
+ 5.14.0
\ No newline at end of file
diff --git a/spring-core-6/src/main/java/com/baeldung/httpinterface/Book.java b/spring-core-6/src/main/java/com/baeldung/httpinterface/Book.java
new file mode 100644
index 0000000000..a38085852e
--- /dev/null
+++ b/spring-core-6/src/main/java/com/baeldung/httpinterface/Book.java
@@ -0,0 +1,3 @@
+package com.baeldung.httpinterface;
+
+public record Book(long id, String title, String author, int year) {}
diff --git a/spring-core-6/src/main/java/com/baeldung/httpinterface/BooksClient.java b/spring-core-6/src/main/java/com/baeldung/httpinterface/BooksClient.java
new file mode 100644
index 0000000000..3034f4f528
--- /dev/null
+++ b/spring-core-6/src/main/java/com/baeldung/httpinterface/BooksClient.java
@@ -0,0 +1,23 @@
+package com.baeldung.httpinterface;
+
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.support.WebClientAdapter;
+import org.springframework.web.service.invoker.HttpServiceProxyFactory;
+
+@Component
+public class BooksClient {
+
+ private final BooksService booksService;
+
+ public BooksClient(WebClient webClient) {
+ HttpServiceProxyFactory httpServiceProxyFactory =
+ HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient))
+ .build();
+ booksService = httpServiceProxyFactory.createClient(BooksService.class);
+ }
+
+ public BooksService getBooksService() {
+ return booksService;
+ }
+}
diff --git a/spring-core-6/src/main/java/com/baeldung/httpinterface/BooksService.java b/spring-core-6/src/main/java/com/baeldung/httpinterface/BooksService.java
new file mode 100644
index 0000000000..a9cf6ec58a
--- /dev/null
+++ b/spring-core-6/src/main/java/com/baeldung/httpinterface/BooksService.java
@@ -0,0 +1,26 @@
+package com.baeldung.httpinterface;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.service.annotation.DeleteExchange;
+import org.springframework.web.service.annotation.GetExchange;
+import org.springframework.web.service.annotation.PostExchange;
+
+import java.util.List;
+
+interface BooksService {
+
+ @GetExchange("/books")
+ List getBooks();
+
+ @GetExchange("/books/{id}")
+ Book getBook(@PathVariable long id);
+
+ @PostExchange("/books")
+ Book saveBook(@RequestBody Book book);
+
+ @DeleteExchange("/books/{id}")
+ ResponseEntity deleteBook(@PathVariable long id);
+
+}
diff --git a/spring-core-6/src/main/java/com/baeldung/reinitializebean/cache/ConfigManager.java b/spring-core-6/src/main/java/com/baeldung/reinitializebean/cache/ConfigManager.java
index 1e4dee6cc4..240fb350c2 100644
--- a/spring-core-6/src/main/java/com/baeldung/reinitializebean/cache/ConfigManager.java
+++ b/spring-core-6/src/main/java/com/baeldung/reinitializebean/cache/ConfigManager.java
@@ -5,7 +5,6 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
-import javax.annotation.PostConstruct;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
diff --git a/spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockServerTest.java b/spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockServerTest.java
new file mode 100644
index 0000000000..22e00c16ae
--- /dev/null
+++ b/spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockServerTest.java
@@ -0,0 +1,217 @@
+package com.baeldung.httpinterface;
+
+import org.apache.http.HttpException;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mockserver.client.MockServerClient;
+import org.mockserver.integration.ClientAndServer;
+import org.mockserver.configuration.Configuration;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.List;
+
+import org.mockserver.model.HttpRequest;
+import org.mockserver.model.MediaType;
+import org.mockserver.verify.VerificationTimes;
+import org.slf4j.event.Level;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatusCode;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientResponseException;
+import reactor.core.publisher.Mono;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockserver.integration.ClientAndServer.startClientAndServer;
+import static org.mockserver.matchers.Times.exactly;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class BooksServiceMockServerTest {
+
+ private static final String SERVER_ADDRESS = "localhost";
+ private static final String PATH = "/books";
+
+ private static int serverPort;
+ private static ClientAndServer mockServer;
+ private static String serviceUrl;
+
+ @BeforeAll
+ static void startServer() throws IOException {
+ serverPort = getFreePort();
+ serviceUrl = "http://" + SERVER_ADDRESS + ":" + serverPort;
+
+ Configuration config = Configuration.configuration().logLevel(Level.WARN);
+ mockServer = startClientAndServer(config, serverPort);
+
+ mockAllBooksRequest();
+ mockBookByIdRequest();
+ mockSaveBookRequest();
+ mockDeleteBookRequest();
+ }
+
+ @AfterAll
+ static void stopServer() {
+ mockServer.stop();
+ }
+
+ @Test
+ void givenMockedGetResponse_whenGetBooksServiceMethodIsCalled_thenTwoBooksAreReturned() {
+ BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
+ BooksService booksService = booksClient.getBooksService();
+
+ List books = booksService.getBooks();
+ assertEquals(2, books.size());
+
+ mockServer.verify(
+ HttpRequest.request()
+ .withMethod(HttpMethod.GET.name())
+ .withPath(PATH),
+ VerificationTimes.exactly(1)
+ );
+ }
+
+ @Test
+ void givenMockedGetResponse_whenGetExistingBookServiceMethodIsCalled_thenCorrectBookIsReturned() {
+ BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
+ BooksService booksService = booksClient.getBooksService();
+
+ Book book = booksService.getBook(1);
+ assertEquals("Book_1", book.title());
+
+ mockServer.verify(
+ HttpRequest.request()
+ .withMethod(HttpMethod.GET.name())
+ .withPath(PATH + "/1"),
+ VerificationTimes.exactly(1)
+ );
+ }
+
+ @Test
+ void givenMockedGetResponse_whenGetNonExistingBookServiceMethodIsCalled_thenCorrectBookIsReturned() {
+ BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
+ BooksService booksService = booksClient.getBooksService();
+
+ assertThrows(WebClientResponseException.class, () -> booksService.getBook(9));
+ }
+
+ @Test
+ void givenCustomErrorHandlerIsSet_whenGetNonExistingBookServiceMethodIsCalled_thenCustomExceptionIsThrown() {
+ BooksClient booksClient = new BooksClient(WebClient.builder()
+ .defaultStatusHandler(HttpStatusCode::isError, resp ->
+ Mono.just(new MyServiceException("Custom exception")))
+ .baseUrl(serviceUrl)
+ .build());
+
+ BooksService booksService = booksClient.getBooksService();
+ assertThrows(MyServiceException.class, () -> booksService.getBook(9));
+ }
+
+ @Test
+ void givenMockedPostResponse_whenSaveBookServiceMethodIsCalled_thenCorrectBookIsReturned() {
+ BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
+ BooksService booksService = booksClient.getBooksService();
+
+ Book book = booksService.saveBook(new Book(3, "Book_3", "Author_3", 2000));
+ assertEquals("Book_3", book.title());
+
+ mockServer.verify(
+ HttpRequest.request()
+ .withMethod(HttpMethod.POST.name())
+ .withPath(PATH),
+ VerificationTimes.exactly(1)
+ );
+ }
+
+ @Test
+ void givenMockedDeleteResponse_whenDeleteBookServiceMethodIsCalled_thenCorrectCodeIsReturned() {
+ BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
+ BooksService booksService = booksClient.getBooksService();
+
+ ResponseEntity response = booksService.deleteBook(3);
+ assertEquals(HttpStatusCode.valueOf(200), response.getStatusCode());
+
+ mockServer.verify(
+ HttpRequest.request()
+ .withMethod(HttpMethod.DELETE.name())
+ .withPath(PATH + "/3"),
+ VerificationTimes.exactly(1)
+ );
+ }
+
+ private static int getFreePort () throws IOException {
+ try (ServerSocket serverSocket = new ServerSocket(0)) {
+ return serverSocket.getLocalPort();
+ }
+ }
+
+ private static void mockAllBooksRequest() {
+ new MockServerClient(SERVER_ADDRESS, serverPort)
+ .when(
+ request()
+ .withPath(PATH)
+ .withMethod(HttpMethod.GET.name()),
+ exactly(1)
+ )
+ .respond(
+ response()
+ .withStatusCode(HttpStatus.SC_OK)
+ .withContentType(MediaType.APPLICATION_JSON)
+ .withBody("[{\"id\":1,\"title\":\"Book_1\",\"author\":\"Author_1\",\"year\":1998},{\"id\":2,\"title\":\"Book_2\",\"author\":\"Author_2\",\"year\":1999}]")
+ );
+ }
+
+ private static void mockBookByIdRequest() {
+ new MockServerClient(SERVER_ADDRESS, serverPort)
+ .when(
+ request()
+ .withPath(PATH + "/1")
+ .withMethod(HttpMethod.GET.name()),
+ exactly(1)
+ )
+ .respond(
+ response()
+ .withStatusCode(HttpStatus.SC_OK)
+ .withContentType(MediaType.APPLICATION_JSON)
+ .withBody("{\"id\":1,\"title\":\"Book_1\",\"author\":\"Author_1\",\"year\":1998}")
+ );
+ }
+
+ private static void mockSaveBookRequest() {
+ new MockServerClient(SERVER_ADDRESS, serverPort)
+ .when(
+ request()
+ .withPath(PATH)
+ .withMethod(HttpMethod.POST.name())
+ .withContentType(MediaType.APPLICATION_JSON)
+ .withBody("{\"id\":3,\"title\":\"Book_3\",\"author\":\"Author_3\",\"year\":2000}"),
+ exactly(1)
+ )
+ .respond(
+ response()
+ .withStatusCode(HttpStatus.SC_OK)
+ .withContentType(MediaType.APPLICATION_JSON)
+ .withBody("{\"id\":3,\"title\":\"Book_3\",\"author\":\"Author_3\",\"year\":2000}")
+ );
+ }
+
+ private static void mockDeleteBookRequest() {
+ new MockServerClient(SERVER_ADDRESS, serverPort)
+ .when(
+ request()
+ .withPath(PATH + "/3")
+ .withMethod(HttpMethod.DELETE.name()),
+ exactly(1)
+ )
+ .respond(
+ response()
+ .withStatusCode(HttpStatus.SC_OK)
+ );
+ }
+
+}
diff --git a/spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockitoTest.java b/spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockitoTest.java
new file mode 100644
index 0000000000..7a82835ef3
--- /dev/null
+++ b/spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockitoTest.java
@@ -0,0 +1,88 @@
+package com.baeldung.httpinterface;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Answers;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatusCode;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+import static org.mockito.BDDMockito.*;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@ExtendWith(MockitoExtension.class)
+class BooksServiceMockitoTest {
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private WebClient webClient;
+
+ @InjectMocks
+ private BooksClient booksClient;
+
+ @Test
+ void givenMockedWebClientReturnsTwoBooks_whenGetBooksServiceMethodIsCalled_thenListOfTwoBooksIsReturned() {
+ given(webClient.method(HttpMethod.GET)
+ .uri(anyString(), anyMap())
+ .retrieve()
+ .bodyToMono(new ParameterizedTypeReference>(){}))
+ .willReturn(Mono.just(List.of(
+ new Book(1,"Book_1", "Author_1", 1998),
+ new Book(2, "Book_2", "Author_2", 1999)
+ )));
+
+ BooksService booksService = booksClient.getBooksService();
+ List books = booksService.getBooks();
+ assertEquals(2, books.size());
+ }
+
+ @Test
+ void givenMockedWebClientReturnsBook_whenGetBookServiceMethodIsCalled_thenBookIsReturned() {
+ given(webClient.method(HttpMethod.GET)
+ .uri(anyString(), anyMap())
+ .retrieve()
+ .bodyToMono(new ParameterizedTypeReference(){}))
+ .willReturn(Mono.just(new Book(1,"Book_1", "Author_1", 1998)));
+
+ BooksService booksService = booksClient.getBooksService();
+ Book book = booksService.getBook(1);
+ assertEquals("Book_1", book.title());
+ }
+
+ @Test
+ void givenMockedWebClientReturnsBook_whenSaveBookServiceMethodIsCalled_thenBookIsReturned() {
+ given(webClient.method(HttpMethod.POST)
+ .uri(anyString(), anyMap())
+ .retrieve()
+ .bodyToMono(new ParameterizedTypeReference(){}))
+ .willReturn(Mono.just(new Book(3, "Book_3", "Author_3", 2000)));
+
+ BooksService booksService = booksClient.getBooksService();
+ Book book = booksService.saveBook(new Book(3, "Book_3", "Author_3", 2000));
+ assertEquals("Book_3", book.title());
+ }
+
+ @Test
+ void givenMockedWebClientReturnsOk_whenDeleteBookServiceMethodIsCalled_thenOkCodeIsReturned() {
+ given(webClient.method(HttpMethod.DELETE)
+ .uri(anyString(), anyMap())
+ .retrieve()
+ .toBodilessEntity()
+ .block(any())
+ .getStatusCode())
+ .willReturn(HttpStatusCode.valueOf(200));
+
+ BooksService booksService = booksClient.getBooksService();
+ ResponseEntity response = booksService.deleteBook(3);
+ assertEquals(HttpStatusCode.valueOf(200), response.getStatusCode());
+ }
+
+}
diff --git a/spring-core-6/src/test/java/com/baeldung/httpinterface/MyServiceException.java b/spring-core-6/src/test/java/com/baeldung/httpinterface/MyServiceException.java
new file mode 100644
index 0000000000..e09335a211
--- /dev/null
+++ b/spring-core-6/src/test/java/com/baeldung/httpinterface/MyServiceException.java
@@ -0,0 +1,9 @@
+package com.baeldung.httpinterface;
+
+public class MyServiceException extends RuntimeException {
+
+ MyServiceException(String msg) {
+ super(msg);
+ }
+
+}