Spring Boot: asynchronous processing with @Async annotation
If you are following my recent articles about Spring Boot ( here, here and here), you should have noticed that it simplifies application development a lot. In this article we’ll cover how the framework supports asynchronous processing.
Introduction
When it comes to scaling services, one good approach is to implement asynchronous processing. And we can achieve that in Spring Boot by creating asynchronous methods, annotating them with @Async.
Annotating a method of a bean with @Async will make it execute in a separate thread, so the caller will not have to wait for the completion of the called method.
The test project
Suppose a RESTful API to calculate exchange quotation from Dollar to Real. It exposes an endpoint that takes the desired amount of dollars to be quoted in various exchange companies, so the user can decide where to buy it.
To achieve that, our API will make REST calls to the exchange companies in order to get the quotations, and then the endpoint will aggregate all the quotations in the final response to the user.
Since every exchange company has different response times, it would be nice to run those REST calls in parallel, so we can optimize the general response time.
Interesting things that we’ll see in this test project:
- how to enable asynchronous processing;
- how to create asynchronous methods;
- how to create bean classes that reads from application.yml configuration file;
- how to make REST calls using RestTemplate.
Let’s do it.
Creating the project
Spring Initializr is our start point:
We’ve choose the following dependency:
- Web: Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container.
The classes
First, let’s see the configuration that enables asynchronous processing. This is our configuration class:
package com.tiago.configuration;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.client.RestTemplate;
/**
* Configuration class that makes possible to inject the beans listed here.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@Configuration
@EnableAsync
public class AsyncMethodsExampleApplicationConfiguration {
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(3);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("AsynchThread-");
executor.initialize();
return executor;
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
In order to enable asynchronous processing in our Spring Boot application, we use @EnableAsync annotation.
We set up our ThreadPoolTaskExecutor as a bean named ‘asyncExecutor’ that will be referenced with the asynchronous methods that we will see later. Our threadpool will have the size of 3, and we are naming his threads by ‘AsynchThread’ so we can see on the server log.
Then we register RestTemplate as a bean, so we can use it in our service class.
Now let’s see our ‘ application.yml’ file:
company:
one:
name: Company One
url: http://localhost:8080/api/companyOne/getQuotation?value={value}
quotation: 3.73
two:
name: Company Two
url: http://localhost:8080/api/companyTwo/getQuotation?value={value}
quotation: 3.70
three:
name: Company Three
url: http://localhost:8080/api/companyThree/getQuotation?value={value}
quotation: 3.75
So we have three fictitious exchange companies, with their respective URLs and quotations. For the sake of test, we are pointing to another controller from our API to emulate it.
How can we read those properties?
Simple. We could define a bean class representing each company, reading their properties accordingly.
Company One’s properties:
package com.tiago.configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Company One's properties.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@Component
@ConfigurationProperties(prefix="company.one")
public class CompanyOneProperties {
private String name;
private String url;
private Double quotation;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Double getQuotation() {
return quotation;
}
public void setQuotation(Double quotation) {
this.quotation = quotation;
}
}
Company Two’s properties:
package com.tiago.configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Company Two's properties.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@Component
@ConfigurationProperties(prefix="company.two")
public class CompanyTwoProperties {
private String name;
private String url;
private Double quotation;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Double getQuotation() {
return quotation;
}
public void setQuotation(Double quotation) {
this.quotation = quotation;
}
}
Company Three’s properties:
package com.tiago.configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Company Three's properties.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@Component
@ConfigurationProperties(prefix="company.three")
public class CompanyThreeProperties {
private String name;
private String url;
private Double quotation;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Double getQuotation() {
return quotation;
}
public void setQuotation(Double quotation) {
this.quotation = quotation;
}
}
By using @ConfigurationProperties we can map the desired set of properties. And as long as our bean classes have the same properties names from the ones defined in ‘ application.yml’ file, they will be set through reflection.
This is our ‘ DollarExchangeQuotationController’ that the user calls to get quotations:
package com.tiago.controller;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.tiago.payload.ExchangeQuotationResponse;
import com.tiago.service.ExchangeQuotationService;
/**
* Restful controller responsible for getting exchange quotations.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@RestController
@RequestMapping("/dollar")
public class DollarExchangeQuotationController {
private static Logger LOGGER = LoggerFactory.getLogger(DollarExchangeQuotationController.class);
@Autowired
ExchangeQuotationService service;
/**
* Get exchange quotations from a given amount of dollars.
*
* @param value
* @return a list of {@link ExchangeQuotationResponse}
* @throws InterruptedException
* @throws ExecutionException
*/
@GetMapping("/exchangeQuotationsInBRL")
public List<ExchangeQuotationResponse> getExchangeQuotations(
@RequestParam(value = "value") Double value) throws InterruptedException, ExecutionException {
List<ExchangeQuotationResponse> exchangeQuotationResponses = new ArrayList<ExchangeQuotationResponse>();
LOGGER.info("GET \"/exchangeQuotations\" starting");
CompletableFuture<ExchangeQuotationResponse> quotationFromCompanyOne = service.getExchangeQuotationFromCompanyOne(value);
CompletableFuture<ExchangeQuotationResponse> quotationFromCompanyTwo = service.getExchangeQuotationFromCompanyTwo(value);
CompletableFuture<ExchangeQuotationResponse> quotationFromCompanyThree = service.getExchangeQuotationFromCompanyThree(value);
// Wait until they are all done
CompletableFuture.allOf(quotationFromCompanyOne, quotationFromCompanyTwo, quotationFromCompanyThree).join();
exchangeQuotationResponses.add(quotationFromCompanyOne.get());
exchangeQuotationResponses.add(quotationFromCompanyTwo.get());
exchangeQuotationResponses.add(quotationFromCompanyThree.get());
LOGGER.info("GET \"/exchangeQuotations\" finished");
return exchangeQuotationResponses;
}
}
It calls asynchronous methods in our service class that returns CompletableFuture objects. By calling CompletableFutures.allOf() we are firing them simultaneously and each call will run in a separated thread. When all threads are finished, we build the final response with all exchange quotations.
This is our interface ‘ ExchangeQuotationService’:
package com.tiago.service;
import java.util.concurrent.CompletableFuture;
/**
* Service class used to get exchange quotations from different companies.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
import com.tiago.payload.ExchangeQuotationResponse;
/**
* Service class to emulate exchange quotations.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
public interface ExchangeQuotationService {
/**
* Emulates quotation calculation for a exchange company.
*
* @param value
* @return {@link ExchangeQuotationResponse}
*/
CompletableFuture<ExchangeQuotationResponse> getExchangeQuotationFromCompanyOne(Double value);
/**
* Emulates quotation calculation for a exchange company.
*
* @param value
* @return {@link ExchangeQuotationResponse}
*/
CompletableFuture<ExchangeQuotationResponse> getExchangeQuotationFromCompanyTwo(Double value);
/**
* Emulates quotation calculation for a exchange company.
*
* @param value
* @return {@link ExchangeQuotationResponse}
*/
CompletableFuture<ExchangeQuotationResponse> getExchangeQuotationFromCompanyThree(Double value);
}
And its implementation class:
package com.tiago.service.impl;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.tiago.configuration.CompanyOneProperties;
import com.tiago.configuration.CompanyThreeProperties;
import com.tiago.configuration.CompanyTwoProperties;
import com.tiago.payload.ExchangeQuotationResponse;
import com.tiago.service.ExchangeQuotationService;
/**
* Implementation of {@link ExchangeQuotationService} interface.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@Service
public class ExchangeQuotationServiceImpl implements ExchangeQuotationService {
private static Logger LOGGER = LoggerFactory.getLogger(ExchangeQuotationService.class);
@Autowired
private RestTemplate restTemplate;
@Autowired
private CompanyOneProperties companyOneProperties;
@Autowired
private CompanyTwoProperties companyTwoProperties;
@Autowired
private CompanyThreeProperties companyThreeProperties;
/* (non-Javadoc)
* @see com.tiago.service.ExchangeQuotationService#getExchangeQuotationFromCompanyOne(java.lang.Double)
*/
@Override
@Async("asyncExecutor")
public CompletableFuture<ExchangeQuotationResponse> getExchangeQuotationFromCompanyOne(Double value) {
LOGGER.info("getExchangeQuotationFromCompanyOne() starting");
Double quotation = restTemplate.getForObject(companyOneProperties.getUrl(), Double.class, value);
LOGGER.info("getExchangeQuotationFromCompanyOne() finished");
return CompletableFuture.completedFuture(buildExchangeQuotationResponse(companyOneProperties.getName(), value, quotation));
}
/* (non-Javadoc)
* @see com.tiago.service.ExchangeQuotationService#getExchangeQuotationFromCompanyTwo(java.lang.Double)
*/
@Override
@Async("asyncExecutor")
public CompletableFuture<ExchangeQuotationResponse> getExchangeQuotationFromCompanyTwo(Double value) {
LOGGER.info("getExchangeQuotationFromCompanyTwo() starting");
Double quotation = restTemplate.getForObject(companyTwoProperties.getUrl(), Double.class, value);
LOGGER.info("getExchangeQuotationFromCompanyTwo() finished");
return CompletableFuture.completedFuture(buildExchangeQuotationResponse(companyTwoProperties.getName(), value, quotation));
}
/* (non-Javadoc)
* @see com.tiago.service.ExchangeQuotationService#getExchangeQuotationFromCompanyThree(java.lang.Double)
*/
@Override
@Async("asyncExecutor")
public CompletableFuture<ExchangeQuotationResponse> getExchangeQuotationFromCompanyThree(Double value) {
LOGGER.info("getExchangeQuotationFromCompanyThree() starting");
Double quotation = restTemplate.getForObject(companyThreeProperties.getUrl(), Double.class, value);
LOGGER.info("getExchangeQuotationFromCompanyThree() finished");
return CompletableFuture.completedFuture(buildExchangeQuotationResponse(companyThreeProperties.getName(), value, quotation));
}
private ExchangeQuotationResponse buildExchangeQuotationResponse(String companyName, Double dollars, Double exchangeQuotation) {
return new ExchangeQuotationResponse(companyName, dollars, exchangeQuotation);
}
}
Now let’s take a closer look at what it does:
@Autowired
private RestTemplate restTemplate;
@Autowired
private CompanyOneProperties companyOneProperties;
@Autowired
private CompanyTwoProperties companyTwoProperties;
@Autowired
private CompanyThreeProperties companyThreeProperties;
Here we are injecting the RestTemplate and the bean classes representing each company configuration.
Next, let get one of the service methods to see what it does:
/* (non-Javadoc)
* @see com.tiago.service.ExchangeQuotationService#getExchangeQuotationFromCompanyOne(java.lang.Double)
*/
@Override
@Async("asyncExecutor")
public CompletableFuture<ExchangeQuotationResponse> getExchangeQuotationFromCompanyOne(Double value) {
LOGGER.info("getExchangeQuotationFromCompanyOne() starting");
Double quotation = restTemplate.getForObject(companyOneProperties.getUrl(), Double.class, value);
LOGGER.info("getExchangeQuotationFromCompanyOne() finished");
return CompletableFuture.completedFuture(buildExchangeQuotationResponse(companyOneProperties.getName(), value, quotation));
}
By annotating with ‘@Async(“asyncExecutor”)’ we are defining our method as an asynchronous one. Note that we are passing our ThreadPoolTaskExecutor configured earlier.
To make the REST call, we pass the desired URL, the type that this endpoint returns (in our case, Double) and the value to get a quotation for.
Then we return a CompletableFuture that is composed of ‘ ExchangeQuotationResponse’, thatencapsulates the response:
package com.tiago.payload;
/**
* Encapsulates quotation response data.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
public class ExchangeQuotationResponse {
private String companyName;
private Double dollars;
private Double exchangeQuotation;
public ExchangeQuotationResponse(String companyName, Double dollars, Double exchangeQuotation) {
this.companyName = companyName;
this.dollars = dollars;
this.exchangeQuotation = exchangeQuotation;
}
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
public Double getDollars() {
return dollars;
}
public void setDollars(Double dollars) {
this.dollars = dollars;
}
public Double getExchangeQuotation() {
return exchangeQuotation;
}
public void setExchangeQuotation(Double exchangeQuotation) {
this.exchangeQuotation = exchangeQuotation;
}
}
Finally, this is our ‘ ExchangeCompaniesController’ that emulates exchange companies:
package com.tiago.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.tiago.configuration.CompanyOneProperties;
import com.tiago.configuration.CompanyThreeProperties;
import com.tiago.configuration.CompanyTwoProperties;
import com.tiago.util.NumberUtils;
/**
* Restful controller that emulates different exchange companies.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@RestController
@RequestMapping("/api")
public class ExchangeCompaniesController {
@Autowired
private CompanyOneProperties companyOneProperties;
@Autowired
private CompanyTwoProperties companyTwoProperties;
@Autowired
private CompanyThreeProperties companyThreeProperties;
/**
* Emulates quotation calculation for a exchange company.
*
* @param value
* @return the quotation
* @throws InterruptedException
*/
@GetMapping("/companyOne/getQuotation")
public Double getExchangeQuotationFromCompanyOne( @RequestParam(value = "value") Double value) throws InterruptedException {
// simulates some processing time
Thread.sleep(3000L);
return NumberUtils.getRoundedDouble(value * companyOneProperties.getQuotation());
}
/**
* Emulates quotation calculation for a exchange company.
*
* @param value
* @return the quotation
* @throws InterruptedException
*/
@GetMapping("/companyTwo/getQuotation")
public Double getExchangeQuotationFromCompanyTwo ( @RequestParam(value = "value") Double value) throws InterruptedException {
// simulates some processing time
Thread.sleep(5000L);
return NumberUtils.getRoundedDouble(value * companyTwoProperties.getQuotation());
}
/**
* Emulates quotation calculation for a exchange company.
*
* @param value
* @return the quotation
* @throws InterruptedException
*/
@GetMapping("/companyThree/getQuotation")
public Double getExchangeQuotationFromCompanyThree( @RequestParam(value = "value") Double value) throws InterruptedException {
// simulates some processing time
Thread.sleep(4000L);
return NumberUtils.getRoundedDouble(value * companyThreeProperties.getQuotation());
}
}
So, we will simulate different response times for each exchange company:
- ‘Company One’ will take 3 seconds to respond;
- ‘Company Two’ will take 5 seconds to respond;
- ‘Company Three’ will take 4 seconds to respond.
It’s show time!
Let’s fire up the application:
$ mvn spring-boot:run
Let’s call the endpoint:
$ curl -v "http://localhost:8080/dollar/exchangeQuotationsInBRL?value=331.54"
This is the response:
[
{
"companyName": "Company One",
"dollars": 331.54,
"exchangeQuotation": 1236.64
},
{
"companyName": "Company Two",
"dollars": 331.54,
"exchangeQuotation": 1226.7
},
{
"companyName": "Company Three",
"dollars": 331.54,
"exchangeQuotation": 1243.28
}
]
Alright. Let’s see the server’s console:
2019-02-24 14:26:50.308 INFO 4405 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2019-02-24 14:26:50.308 INFO 4405 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2019-02-24 14:26:50.317 INFO 4405 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 9 ms
2019-02-24 14:26:50.361 INFO 4405 --- [nio-8080-exec-1] c.t.c.DollarExchangeQuotationController : GET "/exchangeQuotations" starting
2019-02-24 14:26:50.374 INFO 4405 --- [ AsynchThread-1] c.t.service.ExchangeQuotationService : getExchangeQuotationFromCompanyOne() starting
2019-02-24 14:26:50.378 INFO 4405 --- [ AsynchThread-2] c.t.service.ExchangeQuotationService : getExchangeQuotationFromCompanyTwo() starting
2019-02-24 14:26:50.385 INFO 4405 --- [ AsynchThread-3] c.t.service.ExchangeQuotationService : getExchangeQuotationFromCompanyThree() starting
2019-02-24 14:26:53.532 INFO 4405 --- [ AsynchThread-1] c.t.service.ExchangeQuotationService : getExchangeQuotationFromCompanyOne() finished
2019-02-24 14:26:54.456 INFO 4405 --- [ AsynchThread-3] c.t.service.ExchangeQuotationService : getExchangeQuotationFromCompanyThree() finished
2019-02-24 14:26:55.455 INFO 4405 --- [ AsynchThread-2] c.t.service.ExchangeQuotationService : getExchangeQuotationFromCompanyTwo() finished
2019-02-24 14:26:55.457 INFO 4405 --- [nio-8080-exec-1] c.t.c.DollarExchangeQuotationController : GET "/exchangeQuotations" finished
The three REST calls were run in parallel at 2019-02-24 14:26:50; notice the three different thread names:
- AsynchThread-1
- AsynchThread-2
- AsynchThread-3
The processing was finished at 2019-02-24 14:26:55, so the overall time was 5 seconds due to the slowest company to respond (‘Company Two’ takes exactly 5 seconds to respond).
Now let’s see the what happens if we turn off asynchronous processing. We can do it by commenting @EnableAsync annotation from our configuration class, like this:
package com.tiago.configuration;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.client.RestTemplate;
/**
* Configuration class that makes possible to inject the beans listed here.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@Configuration
//@EnableAsync
public class AsyncMethodsExampleApplicationConfiguration {
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(3);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("AsynchThread-");
executor.initialize();
return executor;
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Now if we call the endpoint in the same way we did earlier, this is what we have on server’s log:
2019-02-24 14:27:28.659 INFO 4482 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2019-02-24 14:27:28.659 INFO 4482 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2019-02-24 14:27:28.668 INFO 4482 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 9 ms
2019-02-24 14:27:28.701 INFO 4482 --- [nio-8080-exec-1] c.t.c.DollarExchangeQuotationController : GET "/exchangeQuotations" starting
2019-02-24 14:27:28.701 INFO 4482 --- [nio-8080-exec-1] c.t.service.ExchangeQuotationService : getExchangeQuotationFromCompanyOne() starting
2019-02-24 14:27:31.823 INFO 4482 --- [nio-8080-exec-1] c.t.service.ExchangeQuotationService : getExchangeQuotationFromCompanyOne() finished
2019-02-24 14:27:31.826 INFO 4482 --- [nio-8080-exec-1] c.t.service.ExchangeQuotationService : getExchangeQuotationFromCompanyTwo() starting
2019-02-24 14:27:36.839 INFO 4482 --- [nio-8080-exec-1] c.t.service.ExchangeQuotationService : getExchangeQuotationFromCompanyTwo() finished
2019-02-24 14:27:36.840 INFO 4482 --- [nio-8080-exec-1] c.t.service.ExchangeQuotationService : getExchangeQuotationFromCompanyThree() starting
2019-02-24 14:27:40.861 INFO 4482 --- [nio-8080-exec-1] c.t.service.ExchangeQuotationService : getExchangeQuotationFromCompanyThree() finished
2019-02-24 14:27:40.862 INFO 4482 --- [nio-8080-exec-1] c.t.c.DollarExchangeQuotationController : GET "/exchangeQuotations" finished
We can clearly see that the three REST calls were sequentially called; the processing began at 2019-02-24 14:27:28 and it finished at 2019-02-24 14:27:40, having an overall time of 12 seconds, which is the sum of all companies response times (3 + 5 + 4).
Conclusion
Through this simple example we learnt how to scale up services by enabling asynchronous processing in Spring Boot by enabling it using @EnableAsync annotation and by writing asynchronous methods by using @Async annotation.
We also saw how to map properties to bean classes and how to make REST calls using RestTemplate.
Download the source
Here: https://bitbucket.org/tiagoharris/async-methods-example/src/master/