Skip to content

Commit

Permalink
Merge pull request #17574 from pedrolopes9-7/BAEL-8300
Browse files Browse the repository at this point in the history
BAEL-8300: Using CompletableFuture With Feign Client in Spring Boot
  • Loading branch information
davidmartinezbarua authored Sep 17, 2024
2 parents f45c898 + 18bfcd9 commit 9f4b9f8
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 1 deletion.
12 changes: 12 additions & 0 deletions spring-cloud-modules/spring-cloud-openfeign-2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
<artifactId>spring-cloud-openfeign-2</artifactId>
<name>spring-cloud-openfeign-2</name>
<description>OpenFeign project for Spring Boot</description>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>9</source>
<target>9</target>
</configuration>
</plugin>
</plugins>
</build>

<parent>
<groupId>com.baeldung.spring.cloud</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.baeldung.cloud.openfeign.completablefuturefeignclient;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "paymentMethodClient", url = "http://localhost:8083")
public interface PaymentMethodClient {

@RequestMapping(method = RequestMethod.GET, value = "/payment_methods")
String getAvailablePaymentMethods(@RequestParam(name = "site_id") String siteId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.baeldung.cloud.openfeign.completablefuturefeignclient;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.springframework.stereotype.Service;

import feign.FeignException;
import feign.RetryableException;

@Service
public class PurchaseService {

private final PaymentMethodClient paymentMethodClient;
private final ReportClient reportClient;

public PurchaseService(PaymentMethodClient paymentMethodClient, ReportClient reportClient) {
this.paymentMethodClient = paymentMethodClient;
this.reportClient = reportClient;
}

public String executePurchase(String siteId) throws ExecutionException, InterruptedException {
CompletableFuture<String> paymentMethodsFuture = CompletableFuture.supplyAsync(() -> paymentMethodClient.getAvailablePaymentMethods(siteId))
.orTimeout(400, TimeUnit.MILLISECONDS)
.exceptionally(ex -> {
if (ex.getCause() instanceof FeignException && ((FeignException) ex.getCause()).status() == 404) {
return "cash";
}

if (ex.getCause() instanceof RetryableException) {
// handle REST timeout
throw new RuntimeException("REST call network timeout!");
}

if (ex instanceof TimeoutException) {
// handle thread timeout
throw new RuntimeException("Thread timeout!", ex);
}

throw new RuntimeException("Unrecoverable error!", ex);
});

CompletableFuture.runAsync(() -> reportClient.sendReport("Purchase Order Report"))
.orTimeout(400, TimeUnit.MILLISECONDS);

return String.format("Purchase executed with payment method %s", paymentMethodsFuture.get());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.baeldung.cloud.openfeign.completablefuturefeignclient;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@FeignClient(name = "reportClient", url = "http://localhost:8083")
public interface ReportClient {

@RequestMapping(method = RequestMethod.POST, value = "/reports")
void sendReport(@RequestBody String reportRequest);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ spring.main.allow-bean-definition-overriding=true
logging.level.com.baeldung.cloud.openfeign.client=INFO
feign.hystrix.enabled=true

spring.cloud.openfeign.client.config.postClient.url=https://jsonplaceholder.typicode.com/posts/
spring.cloud.openfeign.client.config.postClient.url=https://jsonplaceholder.typicode.com/posts/

feign.client.config.paymentMethodClient.readTimeout: 200
feign.client.config.paymentMethodClient.connectTimeout: 100
feign.client.config.reportClient.readTimeout: 200
feign.client.config.reportClient.connectTimeout: 100
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.baeldung.cloud.openfeign.completablefuturefeignclient;

import com.baeldung.cloud.openfeign.ExampleApplication;
import com.github.tomakehurst.wiremock.WireMockServer;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.concurrent.ExecutionException;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static org.junit.Assert.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = ExampleApplication.class)
class PurchaseServiceIntegrationTest {

@Autowired
private PurchaseService purchaseService;

@Autowired
private PaymentMethodClient paymentMethodClient;

@Autowired
private ReportClient reportClient;

private WireMockServer wireMockServer;

@BeforeEach
public void startWireMockServer() {
wireMockServer = new WireMockServer(8083);
configureFor("localhost", 8083);
wireMockServer.start();

stubFor(post(urlEqualTo("/reports")).willReturn(aResponse().withStatus(HttpStatus.OK.value())));
}

@AfterEach
public void stopWireMockServer() {
wireMockServer.stop();
}

@Test
void givenRestCalls_whenBothReturnsOk_thenReturnCorrectResult() throws ExecutionException, InterruptedException {
stubFor(get(urlEqualTo("/payment_methods?site_id=BR")).willReturn(aResponse().withStatus(HttpStatus.OK.value())
.withBody("credit_card")));

String result = purchaseService.executePurchase("BR");

assertNotNull(result);
assertEquals("Purchase executed with payment method credit_card", result);
}

@Test
void givenRestCalls_whenPurchaseReturns404_thenReturnDefault() throws ExecutionException, InterruptedException {
stubFor(get(urlEqualTo("/payment_methods?site_id=BR")).willReturn(aResponse().withStatus(HttpStatus.NOT_FOUND.value())));

String result = purchaseService.executePurchase("BR");

assertNotNull(result);
assertEquals("Purchase executed with payment method cash", result);
}

@Test
@Disabled
void givenRestCalls_whenPurchaseCompletableFutureTimeout_thenThrowNewException() {
stubFor(get(urlEqualTo("/payment_methods?site_id=BR")).willReturn(aResponse().withFixedDelay(550)));

Throwable error = assertThrows(ExecutionException.class, () -> purchaseService.executePurchase("BR"));

assertEquals("java.lang.RuntimeException: Thread timeout!", error.getMessage());
}

@Test
void givenRestCalls_whenPurchaseRequestWebTimeout_thenThrowNewException() {
stubFor(get(urlEqualTo("/payment_methods?site_id=BR")).willReturn(aResponse().withFixedDelay(250)));

Throwable error = assertThrows(ExecutionException.class, () -> purchaseService.executePurchase("BR"));

assertEquals("java.lang.RuntimeException: REST call network timeout!", error.getMessage());
}
}

0 comments on commit 9f4b9f8

Please sign in to comment.