9
minutes
Mis à jour le
15/9/2019


Share this post

In the context of one of our finance projects, we faced performance issues. Some of these problems were due to multiple successive calls. Indeed, we made a lot of independent and synchronous calls…

#
Spring Boot
#
Testing

In the context of one of our finance projects, we faced performance issues. Some of these problems were due to multiple successive calls. Indeed, we made a lot of independent and synchronous calls. For example, we made three calls to get some information about: a client, his accounts, and his investment choices. In our case, after these calls, we used results, so we wanted to parallelize the three calls in order to improve our performance. This helped us to divide by two the execution time, which represented 600ms for each client and our sponsor enjoyed the improvements.

  • How to make parallel calls in java?
  • How to test asynchronous function?

This article will help you to implement parallel calls in a Spring Boot Java application and to test these asynchronous functions.

Prerequisites to implement asynchronous calls

Let’s start with the requirements to implement asynchronous calls. You need to have two or more independent calls to third-party API and that can be executed at the same time. Let’s take an example, you want to implement a Spring MVC resource whose goal is to provide the list of European countries whose official language is French. In this example, you need two independent calls: one to fetch all European countries and the other to fetch all countries whose official language is French. In order to have a nicer interface to use our resource, a swagger was implemented, you can find how in my code (available at the end of the article) or here. So you have the following resource, client and a country POJO (Plain Old Java Object):


@Component
@Api(value = "CountryResource")
@RestController
public class CountryResource {

    private final CountryClient countryClient;

    public CountryResource(
            CountryClient countryClient
    ) {
        this.countryClient = countryClient;
    }

    @ApiOperation(httpMethod = "GET", value = "Get all European and French speaking countries", response = String.class, responseContainer = "List")
    @ApiResponses(value = {
            @ApiResponse(code = 404, message = "Countries not found"),
            @ApiResponse(code = 500, message = "The countries could not be fetched")
    })    @GetMapping("")
    public List<String> getAllEuropeanFrenchSpeakingCountries() {
        List<Country> countriesByLanguage = countryClient.getCountriesByLanguage("fr");
        List<Country> countriesByRegion = countryClient.getCountriesByRegion("europe");

        List<String> europeanFrenchSpeakingCountries = new ArrayList<>(countriesByLanguage.stream().map(Country::getName).collect(Collectors.toList()));
        europeanFrenchSpeakingCountries.retainAll(countriesByRegion.stream().map(Country::getName).collect(Collectors.toList()));

        return europeanFrenchSpeakingCountries;
    }
}

The CountryClient.java (see below) client allows us to make HTTP requests in order to get countries by language and by region thanks to the API of RESTCountries.


@Service
public class CountryClient {
    RestTemplate restTemplate = new RestTemplate();

    public List<Country> getCountriesByLanguage(String language) {
        String url = "https://restcountries.eu/rest/v2/lang/" + language + "?fields=name";
        Country[] response = restTemplate.getForObject(url, Country[].class);

        return Arrays.asList(response);
    }

    public List<Country> getCountriesByRegion(String region) {
        String url = "https://restcountries.eu/rest/v2/region/" + region + "?fields=name";
        Country[] response = restTemplate.getForObject(url, Country[].class);

        return Arrays.asList(response);
    }
}

public class Country {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

You have an application that works but let’s suppose that the call to get all French-speaking countries is 2 seconds long and the call to get all European countries is 3 seconds long. You have to wait 5 seconds before being able to use the results instead of 3 seconds. So you want to parallelize these two independent calls. To do so, you have to do the following steps :

01- Add @Async  annotation to the function you want to parallelize getCountriesByLanguage and getCountriesByRegion

02- Change the return type of the function by CompletableFuture<List<Country>>

03- Change the return of getCountriesByLanguage and getCountriesByRegion by CompletableFuture.completedFuture(Arrays.asList(response))

04- Change the type of what return getCountriesByLanguage and Region by CompletableFuture<List<Country>>

05- Add a try-catch when you use the completableFuture in your resource

06- Add a .get() in order to use the elements of the list of countries

07- Add throws Throwable to the function getAllEuropeanFrenchSpeakingCountries

08- Add an AsyncConfiguration

The try-catch is not necessary but it is a good reflex to have.To recap, your new code should look like what follows :


@Service
public class CountryClient {
    RestTemplate restTemplate = new RestTemplate();

    @Async
    public CompletableFuture<List<Country>> getCountriesByLanguage(String language) {
        String url = "https://restcountries.eu/rest/v2/lang/" + language + "?fields=name";
        Country[] response = restTemplate.getForObject(url, Country[].class);

        return CompletableFuture.completedFuture(Arrays.asList(response));
    }

    @Async
    public CompletableFuture<List<Country>> getCountriesByRegion(String region) {
        String url = "https://restcountries.eu/rest/v2/region/" + region + "?fields=name";
        Country[] response = restTemplate.getForObject(url, Country[].class);

        return CompletableFuture.completedFuture(Arrays.asList(response));
    }
}

@Component
@Api(value = "CountryResource")
@RestController
public class CountryResource {

    private final CountryClient countryClient;

    public CountryResource(
            CountryClient countryClient
    ) {
        this.countryClient = countryClient;
    }

    @ApiOperation(httpMethod = "GET", value = "Get all European and French speaking countries", response = String.class, responseContainer = "List")
    @ApiResponses(value = {
            @ApiResponse(code = 404, message = "Countries not found"),
            @ApiResponse(code = 500, message = "The countries could not be fetched")
    })
    @GetMapping("")
    public List<String> getAllEuropeanFrenchSpeakingCountries() throws Throwable {
        CompletableFuture<List<Country>> countriesByLanguageFuture = countryClient.getCountriesByLanguage("fr");
        CompletableFuture<List<Country>> countriesByRegionFuture = countryClient.getCountriesByRegion("europe");
        List<String> europeanFrenchSpeakingCountries;
        try {
            europeanFrenchSpeakingCountries = new ArrayList<>>(countriesByLanguageFuture.get().stream().map(Country::getName).collect(Collectors.toList()));
            europeanFrenchSpeakingCountries.retainAll(countriesByRegionFuture.get().stream().map(Country::getName).collect(Collectors.toList()));
        } catch (Throwable e) {
            throw e.getCause();
        }

        return europeanFrenchSpeakingCountries;
    }
}

The AsyncConfiguration file allows us to use the use the asynchronous functions and the @Async annotation. If you want more details like how to increase the thread pool size, you can find some here.


@Configuration
@EnableAsync
public class AsyncConfiguration  {
    @Bean
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        return executor;
    }
}

Unit testing these functions?

Here we are, we have two parallel calls which work and we want to create some unit tests. So we need to test our client and our resource. First, to test our client, we act as if the function was not asynchronous. In this example, we use Mockito to mock the client and to get the response, we need to use .get() before testing the values.


public class CountryClientTest {
    private CountryClient countryClient;

    @Before
    public void setUp() {
        countryClient = Mockito.spy(new CountryClient());
    }

    @Test
    public void getCountryByLanguage() throws ExecutionException, InterruptedException {
        List<Country> countriesByLanguage = countryClient.getCountriesByLanguage("fr").get();
        assertNotNull(countriesByLanguage);
        assertEquals("Belgium", countriesByLanguage.get(0).getName());
    }

    @Test
    public void getCountryByRegion() throws ExecutionException, InterruptedException {
        List<Country> countriesByRegion = countryClient.getCountriesByRegion("europe").get();
        assertNotNull(countriesByRegion);
        assertEquals("Åland Islands", countriesByRegion.get(0).getName());
        assertEquals("Albania", countriesByRegion.get(1).getName());
    }
}

In order to test our resource, we can mock the response of our client, let’s return France and Belgium for the French spoken countries and France and Germany for the European countries, the result of the intersection should be France. We need to return a CompletableFuture, we do exactly as if the function was not async and then return a CompletableFuture.completedFuture .


public class CountryResourceTest {
    @InjectMocks
    private CountryResource countryResource;

    private CountryClient countryClient;

    @Before
    public void setup() {
        this.countryClient = mock(CountryClient.class);
        this.countryResource = new CountryResource(countryClient);
    }

    @Test
    public void getAllEuropeanFrenchSpeakingCountries() throws Throwable {
        //GIVEN
        Country country = new Country();
        country.setName("France");
        Country country2 = new Country();
        country2.setName("Belgium");
        Country country3 = new Country();
        country3.setName("Germany");
        List<Country> countriesByLanguage = new ArrayList<>();
        countriesByLanguage.add(country);
        countriesByLanguage.add(country2);
        when(countryClient.getCountriesByLanguage(anyString())).thenReturn(CompletableFuture.completedFuture(countriesByLanguage));
        List<Country> countriesByRegion = new ArrayList<>();
        countriesByRegion.add(country);
        countriesByRegion.add(country3);
        when(countryClient.getCountriesByRegion(anyString())).thenReturn(CompletableFuture.completedFuture(countriesByRegion));

        List<String> expectedResult = new ArrayList<>();
        expectedResult.add("France");

        //WHEN
        List<String> result = countryResource.getAllEuropeanFrenchSpeakingCountries();

        //THEN
        assertEquals(expectedResult, result);
    }
}

There you go! We transformed our two independent calls in two asynchronous ones.

Bibliography