In this article, we will explore the Spring @Async annotation. We will look at the asynchronous execution support in Spring with the help of @Async and @EnableAsync annotations.
Introduction
Spring provides a feature to run a long-running process in a separate thread. This feature is helpful when scaling services. By using the @Async and @EnableAsync annotations, we can run the run expensive jobs in the background and wait for the results by using Java’s CompletableFuture
interface.
1. Enable Async Support by @EnableAsync
To enable the asynchronous processing, add the @EnableAsync annotation to the configuration class.
@Configuration
@EnableAsync
public class ApplicationConfiguration {
//additional configurations
}
The @EnableAsync
annotation switches on Spring’s ability to run @Async
methods in a background thread pool. In most cases, this is enough to enable the asynchronous processing but we should keep following things in mind:
- By default,
@EnableAsync
detects Spring’s@Async
annotation.
2. Spring @Async Annotation
We need to add the @Async annotation to the method where we like to enable the asynchronous processing in a separate thread.
@Async
public void updateCustomer(Customer customer) {
//long running background process.
}
There are few rules which we should remember while using this annotation.
@Async
annotation must be on the public method. Spring use a proxy for this annotation and it must be public for the proxy to work.- Calling the async method from within the same class. It won’t work (Method calling like this will bypass proxy).
- Method with a return type should be
CompletableFuture
or Future.
3. How @Async works
Once we add the @Async
on a method, spring framework creates a proxy based on the proxyTargetClass
property. For an incoming request to this method.
- Spring tries to find thread pool associated with the context. It uses this thread pool to submit the request in a separate thread and release the main thread.
- Spring will search for
TaskExecutor
bean or a bean named as taskExecutor else it will fall back to theSimpleAsyncTaskExecutor
.
Let’s look in to the 2 variation where we can apply the @Async annotation.
3.1. Method with Void Return
If our method return type is void, we need not perform any additional steps. Simple add the annotation.
@Async
public void updateCustomer(Customer customer) {
// run the background process
}
Spring will auto-start in a separate thread.
3.2. Method with Return Type
If the method has a return type, we must wrap it with the CompletableFuture
or Future. This is a requirement if we like to use the asynchronous service mode.
@Async
public CompletableFuture getCustomerByID(final String id) throws InterruptedException {
//run the process
return CompletableFuture.completedFuture(customer);
}
4. The Executor
Spring needs a thread pool to manage the thread for the background processes. It will search for TaskExecutor
bean or a bean named as taskExecutor. It will fall back to the SimpleAsyncTaskExecutor
. Sometimes, we may need to customize the thread pool behaviour as per our need, spring provides the following 2 options to customize the executor.
- Override the executor at method level.
- Application level
In most cases, we will end up using the custom executor at the method level. Before we look in to the two options let’s create a custom executor bean.
@Bean(name = "threadPoolTaskExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("AsynchThread::");
executor.initialize();
return executor;
}
We are defining the custom thread pool executor. Above configurations are for demo purpose. You should setup the thread pool as per your application need.
4.1 Method Level Executor
Use the custom executor bean name as an attribute to the @Async:
@Async("threadPoolTaskExecutor")
public CompletableFuture < Customer > getCustomerByID(final String id) throws InterruptedException {
//background or long running process
}
4.2 Override the Executor at the Application Level
Implement the AsyncConfigurer
interface in the configuration class to use the custom executor at the application level. The getAsyncExecutor()
method return the executor at the application level.
@Configuration
public class ServiceExecutorConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(4);
taskExecutor.setMaxPoolSize(4);
taskExecutor.setQueueCapacity(50);
taskExecutor.initialize();
return taskExecutor;
}
}
4.3 Multiple ThreadPoolTaskExecutors
You can define multiple executor beans in case you like to have different ThreadPoolTaskExecutors
for a different task.
@Configuration
@EnableAsync
public class ApplicationConfiguration {
@Bean(name = "threadPoolTaskExecutor1")
public Executor executor1() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("CustomExecutor1::");
executor.initialize();
return executor;
}
@Bean(name = "threadPoolTaskExecutor2")
public Executor executor2() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("CustomExecutor2::");
executor.initialize();
return executor;
}
}
This is how we can use these:
@Async("threadPoolTaskExecutor1")
public void methodA() {}
@Async("threadPoolTaskExecutor2")
public void methodB() {}
5. Application in Action
So far we saw the core concepts and configurations, let’s see the Spring @Async
annotation in action. We will start by setting up the application using Spring Initilizr. We can use the web version or can use IDE to build the application. This is how the pom.xml looks like:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath />
<!-- lookup parent from repository -->
</parent>
<groupId>com.javadevjournal</groupId>
<artifactId>spring-async</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Spring @Async for Asynchronous Processing</name>
<description>Spring @Async for Asynchronous Processing</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Let’s create our service class, which will simulate the long-running process:
package com.javadevjournal.customer.service;
import com.javadevjournal.data.customer.Customer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class DefaultCustomerService implements CustomerService {
private static final Logger LOG = LoggerFactory.getLogger(DefaultCustomerService.class);
@Override
@Async("threadPoolTaskExecutor")
public CompletableFuture < Customer > getCustomerByID(final String id) throws InterruptedException {
LOG.info("Filling the customer details for id {} ", id);
Customer customer = new Customer();
customer.setFirstName("Javadev");
customer.setLastName("Journal");
customer.setAge(34);
customer.setEmail("contact-us@javadevjournal");
// doing an artificial sleep
Thread.sleep(20000);
return CompletableFuture.completedFuture(customer);
}
@Override
@Async("threadPoolTaskExecutor")
public void updateCustomer(Customer customer) {
LOG.warn("Running method with thread {} :", Thread.currentThread().getName());
// do nothing
}
@Override
public Customer getCustomerByEmail(String email) throws InterruptedException {
LOG.info("Filling the customer details for email {}", email);
Customer customer = new Customer();
customer.setFirstName("New");
customer.setLastName("Customer");
customer.setAge(30);
customer.setEmail("contact-us@javadevjournal");
Thread.sleep(20000);
return customer;
}
}
We are delaying the response by adding Thread.sleep(2000)
. This is to simulate slow moving service. Let’s discuss few important points:
- @Async annotation active the asynchronous execution.
- We are using the custom executor to run the request in a separate thread pool.
5.1. Controller
Our controller is a simple class. This is how it looks like:
@RestController
@RequestMapping("/customers")
public class CustomerController {
@Autowired
CustomerService customerService;
@GetMapping("/customer/{id}")
public CompletableFuture < Customer > getCustomerById(@PathVariable String id) throws InterruptedException {
return customerService.getCustomerByID(id);
}
@PutMapping("/customer/update")
public void updateCustomer() {
customerService.updateCustomer(null);
}
@GetMapping("/customer/id/{email}")
public Customer getCustomerByEmail(@PathVariable String email) throws InterruptedException {
return customerService.getCustomerByEmail(email);
}
}
5.2. Build and Running the Application
Let’s run the application to see this in action. Once the application is up and running, hit the following URL http://localhost:8080/customers/customer/12
and check the server log. You will see a similar output:
2020-07-10 18:37:10.403 INFO 12056 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-07-10 18:37:10.418 INFO 12056 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 15 ms
2020-07-10 18:37:10.524 INFO 12056 --- [AsynchThread::1] c.j.c.service.DefaultCustomerService : Filling the customer details for id 12
If you look closely, the request is executing in a new thread [AsynchThread::1]
. This will help in long running processes as we can run the process in a separate thread and not blocking the main thread. To verify this in more details, hit the following URL http://localhost:8080/customers/customer/id/[email protected]
(The service method does not contain @Async annotation).
2020-07-10 18:37:10.418 INFO 12056 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 15 ms
2020-07-10 18:37:10.524 INFO 12056 --- [AsynchThread::1] c.j.c.service.DefaultCustomerService : Filling the customer details for id 12
2020-07-10 18:40:33.546 INFO 12056 --- [nio-8080-exec-4] c.j.c.service.DefaultCustomerService : Filling the customer details for email [email protected]
6. Exception Handling
To handle the exception with @Async
annotation, remember following key points.
- If the return type is
CompletableFuture
orFuture
,Future.get()
method will throw the exception. - For
void
return type, we need to add extra configuration as exceptions will not be propagated to the calling thread.
To handle exception for void return type, we need to create asynchronous exception handler by implementing the AsyncUncaughtExceptionHandler interface.
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(CustomAsyncExceptionHandler.class);
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
LOG.error("Exception while executing with message {} ", throwable.getMessage());
LOG.error("Exception happen in {} method ", method.getName());
}
}
The last step is to configure this AsyncUncaughtExceptionHandler
in our configuration class.
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new CustomAsyncExceptionHandler();
}
Summary
In this article, we talked about the Spring @Async annotation. We covered the following topics in this article.
- How to run long running processes in a separate thread pool using @Aync annotation.
- When to use the asynchronous execution support in Spring
- Custom executor for the custom thread pool.
- How to handle the exceptions.
As always, the source code for this article is available on the GitHub.