diff --git a/patterns-modules/design-patterns-architectural/pom.xml b/patterns-modules/design-patterns-architectural/pom.xml index f18976747743..eb61a36c9abf 100644 --- a/patterns-modules/design-patterns-architectural/pom.xml +++ b/patterns-modules/design-patterns-architectural/pom.xml @@ -72,8 +72,6 @@ 5.5.14 3.20.4 3.14.0 - 1.7.32 - 1.2.7 \ No newline at end of file diff --git a/patterns-modules/design-patterns-architectural/src/main/java/com/baeldung/ambassadorpattern/AmbassadorPatternApplication.java b/patterns-modules/design-patterns-architectural/src/main/java/com/baeldung/ambassadorpattern/AmbassadorPatternApplication.java new file mode 100644 index 000000000000..4bf29907d2e7 --- /dev/null +++ b/patterns-modules/design-patterns-architectural/src/main/java/com/baeldung/ambassadorpattern/AmbassadorPatternApplication.java @@ -0,0 +1,16 @@ +package com.baeldung.ambassadorpattern; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.retry.annotation.EnableRetry; + +@EnableRetry +@EnableCaching +@SpringBootApplication +public class AmbassadorPatternApplication { + + public static void main(String[] args) { + SpringApplication.run(AmbassadorPatternApplication.class, args); + } +} \ No newline at end of file diff --git a/patterns-modules/design-patterns-architectural/src/main/java/com/baeldung/ambassadorpattern/HttpAmbassadorController.java b/patterns-modules/design-patterns-architectural/src/main/java/com/baeldung/ambassadorpattern/HttpAmbassadorController.java new file mode 100644 index 000000000000..1c2b349764b8 --- /dev/null +++ b/patterns-modules/design-patterns-architectural/src/main/java/com/baeldung/ambassadorpattern/HttpAmbassadorController.java @@ -0,0 +1,21 @@ +package com.baeldung.ambassadorpattern; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v1/http-ambassador/names") +public class HttpAmbassadorController { + + private final HttpAmbassadorNamesApiClient httpAmbassadorNamesApiClient; + + public HttpAmbassadorController(HttpAmbassadorNamesApiClient httpAmbassadorNamesApiClient) { + this.httpAmbassadorNamesApiClient = httpAmbassadorNamesApiClient; + } + + @GetMapping + public String get() { + return httpAmbassadorNamesApiClient.getResponse(); + } +} diff --git a/patterns-modules/design-patterns-architectural/src/main/java/com/baeldung/ambassadorpattern/HttpAmbassadorNamesApiClient.java b/patterns-modules/design-patterns-architectural/src/main/java/com/baeldung/ambassadorpattern/HttpAmbassadorNamesApiClient.java new file mode 100644 index 000000000000..4b33e1297c3c --- /dev/null +++ b/patterns-modules/design-patterns-architectural/src/main/java/com/baeldung/ambassadorpattern/HttpAmbassadorNamesApiClient.java @@ -0,0 +1,46 @@ +package com.baeldung.ambassadorpattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +@Component +public class HttpAmbassadorNamesApiClient { + + private final RestTemplate restTemplate; + private final Logger logger = LoggerFactory.getLogger(HttpAmbassadorNamesApiClient.class); + public final String apiUrl; + + public HttpAmbassadorNamesApiClient(RestTemplate restTemplate, @Value("${names-api-url}") String apiUrl) { + this.restTemplate = restTemplate; + this.apiUrl = apiUrl; + } + + @Cacheable(value = "httpResponses", key = "#root.target.apiUrl", unless = "#result == null") + @Retryable(value = { HttpServerErrorException.class }, maxAttempts = 5, backoff = @Backoff(delay = 1000)) + public String getResponse() { + try { + String result = restTemplate.getForObject(apiUrl, String.class); + logger.info("HTTP call completed successfully to url={}", apiUrl); + return result; + } catch (HttpClientErrorException e) { + logger.error("HTTP Client Error error_code={} message={}", e.getStatusCode(), e.getMessage()); + throw e; + } + } + + @Recover + public String recover(Exception e) { + final String defaultResponse = "default"; + logger.error("Too many retry attempts. Falling back to default. error={} default={}", e.getMessage(), defaultResponse); + return defaultResponse; + } +} diff --git a/patterns-modules/design-patterns-architectural/src/main/java/com/baeldung/ambassadorpattern/RestTemplateConfig.java b/patterns-modules/design-patterns-architectural/src/main/java/com/baeldung/ambassadorpattern/RestTemplateConfig.java new file mode 100644 index 000000000000..cd84fb9c7e00 --- /dev/null +++ b/patterns-modules/design-patterns-architectural/src/main/java/com/baeldung/ambassadorpattern/RestTemplateConfig.java @@ -0,0 +1,31 @@ +package com.baeldung.ambassadorpattern; + +import java.time.Duration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + private final int connectTimeoutSeconds; + private final int readTimeoutSeconds; + private final RestTemplateBuilder restTemplateBuilder; + + public RestTemplateConfig(RestTemplateBuilder restTemplateBuilder, @Value("${http.client.read-timeout-seconds}") int readTimeoutSeconds, + @Value("${http.client.connect-timeout-seconds}") int connectTimeoutSeconds) { + this.restTemplateBuilder = restTemplateBuilder; + this.readTimeoutSeconds = readTimeoutSeconds; + this.connectTimeoutSeconds = connectTimeoutSeconds; + } + + @Bean + public RestTemplate restTemplate() { + return restTemplateBuilder.setConnectTimeout(Duration.ofMillis(connectTimeoutSeconds)) + .setReadTimeout(Duration.ofMillis(readTimeoutSeconds)) + .build(); + } +} diff --git a/patterns-modules/design-patterns-architectural/src/main/resources/application.properties b/patterns-modules/design-patterns-architectural/src/main/resources/application.properties new file mode 100644 index 000000000000..a38f6d9937bd --- /dev/null +++ b/patterns-modules/design-patterns-architectural/src/main/resources/application.properties @@ -0,0 +1,3 @@ +http.client.connect-timeout-seconds=2000 +http.client.read-timeout-seconds=3000 +names-api-url=https://domain.com/names/api \ No newline at end of file diff --git a/patterns-modules/design-patterns-architectural/src/test/java/com/baeldung/ambassadorpattern/HttpAmbassadorControllerIntegrationTest.java b/patterns-modules/design-patterns-architectural/src/test/java/com/baeldung/ambassadorpattern/HttpAmbassadorControllerIntegrationTest.java new file mode 100644 index 000000000000..394e5f1ec6d8 --- /dev/null +++ b/patterns-modules/design-patterns-architectural/src/test/java/com/baeldung/ambassadorpattern/HttpAmbassadorControllerIntegrationTest.java @@ -0,0 +1,38 @@ +package com.baeldung.ambassadorpattern; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.client.RestTemplate; + +@WebMvcTest(HttpAmbassadorController.class) +@Import({ HttpAmbassadorNamesApiClient.class, TestConfig.class }) +@AutoConfigureMockMvc(addFilters = false) +class HttpAmbassadorControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private RestTemplate restTemplate; + + @Test + void givenExternalCallMock_whenGetNames_thenReturnExpectedName() throws Exception { + String expectedResponse = "{'name': 'Baeldung'}"; + when(restTemplate.getForObject(eq("https://domain.com/names/api"), eq(String.class))).thenReturn(expectedResponse); + + mockMvc.perform(get("/v1/http-ambassador/names")) + .andExpect(status().isOk()) + .andExpect(content().string(expectedResponse)); + } +} \ No newline at end of file diff --git a/patterns-modules/design-patterns-architectural/src/test/java/com/baeldung/ambassadorpattern/TestConfig.java b/patterns-modules/design-patterns-architectural/src/test/java/com/baeldung/ambassadorpattern/TestConfig.java new file mode 100644 index 000000000000..927035c8d1c5 --- /dev/null +++ b/patterns-modules/design-patterns-architectural/src/test/java/com/baeldung/ambassadorpattern/TestConfig.java @@ -0,0 +1,11 @@ +package com.baeldung.ambassadorpattern; + + +import org.springframework.context.annotation.Configuration; +import org.springframework.retry.annotation.EnableRetry; + +@Configuration +@EnableRetry +public class TestConfig { + +}