Spring @Async for Asynchronous Processing

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:

  1. 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.

  1. @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.
  2. Calling the async method from within the same class. It won’t work (Method calling like this will bypass proxy).
  3. 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.

  1. 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.
  2. Spring will search for TaskExecutor bean or a bean named as taskExecutor else it will fall back to the SimpleAsyncTaskExecutor.

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.

  1. Override the executor at method level.
  2. 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:

  1. @Async annotation active the asynchronous execution.
  2. 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.

  1. If the return type is CompletableFuture or Future, Future.get() method will throw the exception.
  2. 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.

  1. How to run long running processes in a separate thread pool using @Aync annotation.
  2. When to use the asynchronous execution support in Spring 
  3. Custom executor for the custom thread pool.
  4. How to handle the exceptions.

As always, the source code for this article is available on the GitHub.