Skip to content

Commit b1a0a85

Browse files
BAEL-9085: Introduction to Ambassdor Design Pattern (#18602)
* adding ambassador pattern with retry, timeout, fallback, logging, and caching * adding tests. fix client class annotations * change naming * formatting files * adding back dependency versions * renaming test * switch from system out to slf4j loggers
1 parent 44bb68f commit b1a0a85

File tree

8 files changed

+166
-2
lines changed

8 files changed

+166
-2
lines changed

patterns-modules/design-patterns-architectural/pom.xml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,6 @@
7272
<spring-integration-test.version>5.5.14</spring-integration-test.version>
7373
<camel-core.version>3.20.4</camel-core.version>
7474
<camel-test-junit5.version>3.14.0</camel-test-junit5.version>
75-
<org.slf4j.version>1.7.32</org.slf4j.version>
76-
<logback.version>1.2.7</logback.version>
7775
</properties>
7876

7977
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.baeldung.ambassadorpattern;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.cache.annotation.EnableCaching;
6+
import org.springframework.retry.annotation.EnableRetry;
7+
8+
@EnableRetry
9+
@EnableCaching
10+
@SpringBootApplication
11+
public class AmbassadorPatternApplication {
12+
13+
public static void main(String[] args) {
14+
SpringApplication.run(AmbassadorPatternApplication.class, args);
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.baeldung.ambassadorpattern;
2+
3+
import org.springframework.web.bind.annotation.GetMapping;
4+
import org.springframework.web.bind.annotation.RequestMapping;
5+
import org.springframework.web.bind.annotation.RestController;
6+
7+
@RestController
8+
@RequestMapping("/v1/http-ambassador/names")
9+
public class HttpAmbassadorController {
10+
11+
private final HttpAmbassadorNamesApiClient httpAmbassadorNamesApiClient;
12+
13+
public HttpAmbassadorController(HttpAmbassadorNamesApiClient httpAmbassadorNamesApiClient) {
14+
this.httpAmbassadorNamesApiClient = httpAmbassadorNamesApiClient;
15+
}
16+
17+
@GetMapping
18+
public String get() {
19+
return httpAmbassadorNamesApiClient.getResponse();
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.baeldung.ambassadorpattern;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.cache.annotation.Cacheable;
7+
import org.springframework.retry.annotation.Backoff;
8+
import org.springframework.retry.annotation.Recover;
9+
import org.springframework.retry.annotation.Retryable;
10+
import org.springframework.stereotype.Component;
11+
import org.springframework.web.client.HttpClientErrorException;
12+
import org.springframework.web.client.HttpServerErrorException;
13+
import org.springframework.web.client.RestTemplate;
14+
15+
@Component
16+
public class HttpAmbassadorNamesApiClient {
17+
18+
private final RestTemplate restTemplate;
19+
private final Logger logger = LoggerFactory.getLogger(HttpAmbassadorNamesApiClient.class);
20+
public final String apiUrl;
21+
22+
public HttpAmbassadorNamesApiClient(RestTemplate restTemplate, @Value("${names-api-url}") String apiUrl) {
23+
this.restTemplate = restTemplate;
24+
this.apiUrl = apiUrl;
25+
}
26+
27+
@Cacheable(value = "httpResponses", key = "#root.target.apiUrl", unless = "#result == null")
28+
@Retryable(value = { HttpServerErrorException.class }, maxAttempts = 5, backoff = @Backoff(delay = 1000))
29+
public String getResponse() {
30+
try {
31+
String result = restTemplate.getForObject(apiUrl, String.class);
32+
logger.info("HTTP call completed successfully to url={}", apiUrl);
33+
return result;
34+
} catch (HttpClientErrorException e) {
35+
logger.error("HTTP Client Error error_code={} message={}", e.getStatusCode(), e.getMessage());
36+
throw e;
37+
}
38+
}
39+
40+
@Recover
41+
public String recover(Exception e) {
42+
final String defaultResponse = "default";
43+
logger.error("Too many retry attempts. Falling back to default. error={} default={}", e.getMessage(), defaultResponse);
44+
return defaultResponse;
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.baeldung.ambassadorpattern;
2+
3+
import java.time.Duration;
4+
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.boot.web.client.RestTemplateBuilder;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
import org.springframework.web.client.RestTemplate;
10+
11+
@Configuration
12+
public class RestTemplateConfig {
13+
14+
private final int connectTimeoutSeconds;
15+
private final int readTimeoutSeconds;
16+
private final RestTemplateBuilder restTemplateBuilder;
17+
18+
public RestTemplateConfig(RestTemplateBuilder restTemplateBuilder, @Value("${http.client.read-timeout-seconds}") int readTimeoutSeconds,
19+
@Value("${http.client.connect-timeout-seconds}") int connectTimeoutSeconds) {
20+
this.restTemplateBuilder = restTemplateBuilder;
21+
this.readTimeoutSeconds = readTimeoutSeconds;
22+
this.connectTimeoutSeconds = connectTimeoutSeconds;
23+
}
24+
25+
@Bean
26+
public RestTemplate restTemplate() {
27+
return restTemplateBuilder.setConnectTimeout(Duration.ofMillis(connectTimeoutSeconds))
28+
.setReadTimeout(Duration.ofMillis(readTimeoutSeconds))
29+
.build();
30+
}
31+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
http.client.connect-timeout-seconds=2000
2+
http.client.read-timeout-seconds=3000
3+
names-api-url=https://domain.com/names/api
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.baeldung.ambassadorpattern;
2+
3+
import static org.mockito.ArgumentMatchers.eq;
4+
import static org.mockito.Mockito.when;
5+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
6+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
7+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
8+
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
12+
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
13+
import org.springframework.boot.test.mock.mockito.MockBean;
14+
import org.springframework.context.annotation.Import;
15+
import org.springframework.test.web.servlet.MockMvc;
16+
import org.springframework.web.client.RestTemplate;
17+
18+
@WebMvcTest(HttpAmbassadorController.class)
19+
@Import({ HttpAmbassadorNamesApiClient.class, TestConfig.class })
20+
@AutoConfigureMockMvc(addFilters = false)
21+
class HttpAmbassadorControllerIntegrationTest {
22+
23+
@Autowired
24+
private MockMvc mockMvc;
25+
26+
@MockBean
27+
private RestTemplate restTemplate;
28+
29+
@Test
30+
void givenExternalCallMock_whenGetNames_thenReturnExpectedName() throws Exception {
31+
String expectedResponse = "{'name': 'Baeldung'}";
32+
when(restTemplate.getForObject(eq("https://domain.com/names/api"), eq(String.class))).thenReturn(expectedResponse);
33+
34+
mockMvc.perform(get("/v1/http-ambassador/names"))
35+
.andExpect(status().isOk())
36+
.andExpect(content().string(expectedResponse));
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.baeldung.ambassadorpattern;
2+
3+
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.retry.annotation.EnableRetry;
6+
7+
@Configuration
8+
@EnableRetry
9+
public class TestConfig {
10+
11+
}

0 commit comments

Comments
 (0)