Skip to content

Commit 9f4b9f8

Browse files
Merge pull request #17574 from pedrolopes9-7/BAEL-8300
BAEL-8300: Using CompletableFuture With Feign Client in Spring Boot
2 parents f45c898 + 18bfcd9 commit 9f4b9f8

File tree

6 files changed

+188
-1
lines changed

6 files changed

+188
-1
lines changed

spring-cloud-modules/spring-cloud-openfeign-2/pom.xml

+12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@
77
<artifactId>spring-cloud-openfeign-2</artifactId>
88
<name>spring-cloud-openfeign-2</name>
99
<description>OpenFeign project for Spring Boot</description>
10+
<build>
11+
<plugins>
12+
<plugin>
13+
<groupId>org.apache.maven.plugins</groupId>
14+
<artifactId>maven-compiler-plugin</artifactId>
15+
<configuration>
16+
<source>9</source>
17+
<target>9</target>
18+
</configuration>
19+
</plugin>
20+
</plugins>
21+
</build>
1022

1123
<parent>
1224
<groupId>com.baeldung.spring.cloud</groupId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.baeldung.cloud.openfeign.completablefuturefeignclient;
2+
3+
import org.springframework.cloud.openfeign.FeignClient;
4+
import org.springframework.web.bind.annotation.RequestMapping;
5+
import org.springframework.web.bind.annotation.RequestMethod;
6+
import org.springframework.web.bind.annotation.RequestParam;
7+
8+
@FeignClient(name = "paymentMethodClient", url = "http://localhost:8083")
9+
public interface PaymentMethodClient {
10+
11+
@RequestMapping(method = RequestMethod.GET, value = "/payment_methods")
12+
String getAvailablePaymentMethods(@RequestParam(name = "site_id") String siteId);
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.baeldung.cloud.openfeign.completablefuturefeignclient;
2+
3+
import java.util.concurrent.CompletableFuture;
4+
import java.util.concurrent.ExecutionException;
5+
import java.util.concurrent.TimeUnit;
6+
import java.util.concurrent.TimeoutException;
7+
8+
import org.springframework.stereotype.Service;
9+
10+
import feign.FeignException;
11+
import feign.RetryableException;
12+
13+
@Service
14+
public class PurchaseService {
15+
16+
private final PaymentMethodClient paymentMethodClient;
17+
private final ReportClient reportClient;
18+
19+
public PurchaseService(PaymentMethodClient paymentMethodClient, ReportClient reportClient) {
20+
this.paymentMethodClient = paymentMethodClient;
21+
this.reportClient = reportClient;
22+
}
23+
24+
public String executePurchase(String siteId) throws ExecutionException, InterruptedException {
25+
CompletableFuture<String> paymentMethodsFuture = CompletableFuture.supplyAsync(() -> paymentMethodClient.getAvailablePaymentMethods(siteId))
26+
.orTimeout(400, TimeUnit.MILLISECONDS)
27+
.exceptionally(ex -> {
28+
if (ex.getCause() instanceof FeignException && ((FeignException) ex.getCause()).status() == 404) {
29+
return "cash";
30+
}
31+
32+
if (ex.getCause() instanceof RetryableException) {
33+
// handle REST timeout
34+
throw new RuntimeException("REST call network timeout!");
35+
}
36+
37+
if (ex instanceof TimeoutException) {
38+
// handle thread timeout
39+
throw new RuntimeException("Thread timeout!", ex);
40+
}
41+
42+
throw new RuntimeException("Unrecoverable error!", ex);
43+
});
44+
45+
CompletableFuture.runAsync(() -> reportClient.sendReport("Purchase Order Report"))
46+
.orTimeout(400, TimeUnit.MILLISECONDS);
47+
48+
return String.format("Purchase executed with payment method %s", paymentMethodsFuture.get());
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.baeldung.cloud.openfeign.completablefuturefeignclient;
2+
3+
import org.springframework.cloud.openfeign.FeignClient;
4+
import org.springframework.web.bind.annotation.RequestBody;
5+
import org.springframework.web.bind.annotation.RequestMapping;
6+
import org.springframework.web.bind.annotation.RequestMethod;
7+
8+
@FeignClient(name = "reportClient", url = "http://localhost:8083")
9+
public interface ReportClient {
10+
11+
@RequestMapping(method = RequestMethod.POST, value = "/reports")
12+
void sendReport(@RequestBody String reportRequest);
13+
}

spring-cloud-modules/spring-cloud-openfeign-2/src/main/resources/application.properties

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,9 @@ spring.main.allow-bean-definition-overriding=true
77
logging.level.com.baeldung.cloud.openfeign.client=INFO
88
feign.hystrix.enabled=true
99

10-
spring.cloud.openfeign.client.config.postClient.url=https://jsonplaceholder.typicode.com/posts/
10+
spring.cloud.openfeign.client.config.postClient.url=https://jsonplaceholder.typicode.com/posts/
11+
12+
feign.client.config.paymentMethodClient.readTimeout: 200
13+
feign.client.config.paymentMethodClient.connectTimeout: 100
14+
feign.client.config.reportClient.readTimeout: 200
15+
feign.client.config.reportClient.connectTimeout: 100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.baeldung.cloud.openfeign.completablefuturefeignclient;
2+
3+
import com.baeldung.cloud.openfeign.ExampleApplication;
4+
import com.github.tomakehurst.wiremock.WireMockServer;
5+
6+
import org.junit.jupiter.api.AfterEach;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Disabled;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.extension.ExtendWith;
11+
import org.springframework.beans.factory.annotation.Autowired;
12+
import org.springframework.boot.test.context.SpringBootTest;
13+
import org.springframework.http.HttpStatus;
14+
import org.springframework.test.context.junit.jupiter.SpringExtension;
15+
16+
import java.util.concurrent.ExecutionException;
17+
18+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
19+
import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.get;
21+
import static com.github.tomakehurst.wiremock.client.WireMock.post;
22+
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
23+
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
24+
import static org.junit.Assert.*;
25+
26+
@ExtendWith(SpringExtension.class)
27+
@SpringBootTest(classes = ExampleApplication.class)
28+
class PurchaseServiceIntegrationTest {
29+
30+
@Autowired
31+
private PurchaseService purchaseService;
32+
33+
@Autowired
34+
private PaymentMethodClient paymentMethodClient;
35+
36+
@Autowired
37+
private ReportClient reportClient;
38+
39+
private WireMockServer wireMockServer;
40+
41+
@BeforeEach
42+
public void startWireMockServer() {
43+
wireMockServer = new WireMockServer(8083);
44+
configureFor("localhost", 8083);
45+
wireMockServer.start();
46+
47+
stubFor(post(urlEqualTo("/reports")).willReturn(aResponse().withStatus(HttpStatus.OK.value())));
48+
}
49+
50+
@AfterEach
51+
public void stopWireMockServer() {
52+
wireMockServer.stop();
53+
}
54+
55+
@Test
56+
void givenRestCalls_whenBothReturnsOk_thenReturnCorrectResult() throws ExecutionException, InterruptedException {
57+
stubFor(get(urlEqualTo("/payment_methods?site_id=BR")).willReturn(aResponse().withStatus(HttpStatus.OK.value())
58+
.withBody("credit_card")));
59+
60+
String result = purchaseService.executePurchase("BR");
61+
62+
assertNotNull(result);
63+
assertEquals("Purchase executed with payment method credit_card", result);
64+
}
65+
66+
@Test
67+
void givenRestCalls_whenPurchaseReturns404_thenReturnDefault() throws ExecutionException, InterruptedException {
68+
stubFor(get(urlEqualTo("/payment_methods?site_id=BR")).willReturn(aResponse().withStatus(HttpStatus.NOT_FOUND.value())));
69+
70+
String result = purchaseService.executePurchase("BR");
71+
72+
assertNotNull(result);
73+
assertEquals("Purchase executed with payment method cash", result);
74+
}
75+
76+
@Test
77+
@Disabled
78+
void givenRestCalls_whenPurchaseCompletableFutureTimeout_thenThrowNewException() {
79+
stubFor(get(urlEqualTo("/payment_methods?site_id=BR")).willReturn(aResponse().withFixedDelay(550)));
80+
81+
Throwable error = assertThrows(ExecutionException.class, () -> purchaseService.executePurchase("BR"));
82+
83+
assertEquals("java.lang.RuntimeException: Thread timeout!", error.getMessage());
84+
}
85+
86+
@Test
87+
void givenRestCalls_whenPurchaseRequestWebTimeout_thenThrowNewException() {
88+
stubFor(get(urlEqualTo("/payment_methods?site_id=BR")).willReturn(aResponse().withFixedDelay(250)));
89+
90+
Throwable error = assertThrows(ExecutionException.class, () -> purchaseService.executePurchase("BR"));
91+
92+
assertEquals("java.lang.RuntimeException: REST call network timeout!", error.getMessage());
93+
}
94+
}

0 commit comments

Comments
 (0)