Partner – Microsoft – NPI (cat= Spring)
announcement - icon

Azure Spring Apps is a fully managed service from Microsoft (built in collaboration with VMware), focused on building and deploying Spring Boot applications on Azure Cloud without worrying about Kubernetes.

And, the Enterprise plan comes with some interesting features, such as commercial Spring runtime support, a 99.95% SLA and some deep discounts (up to 47%) when you are ready for production.

>> Learn more and deploy your first Spring Boot app to Azure.

You can also ask questions and leave feedback on the Azure Spring Apps GitHub page.

1. Overview

In this tutorial, we’ll explore the asynchronous execution support in Spring and the @Async annotation.

Simply put, annotating a method of a bean with @Async will execute it in a separate thread. In other words, the caller will not wait to complete the called method.

One interesting aspect of Spring is that the event support in the framework also has support for async processing if necessary.

2. Enable Async Support

Let’s start by enabling asynchronous processing with Java configuration.

We’ll do this by adding the @EnableAsync to a configuration class:

@Configuration
@EnableAsync
public class SpringAsyncConfig { ... }

The enable annotation is enough. But there are also a few simple options for configuration as well:

  • annotationBy default, @EnableAsync detects Spring’s @Async annotation and the EJB 3.1 javax.ejb.Asynchronous. We can use this option to detect other, user-defined annotation types as well
  • mode indicates the type of advice that should be used — JDK proxy based or AspectJ weaving
  • proxyTargetClass indicates the type of proxy that should be used — CGLIB or JDK. This attribute has effect only if the mode is set to AdviceMode.PROXY
  • order sets the order in which AsyncAnnotationBeanPostProcessor should be applied. By default, it runs last so that it can take into account all existing proxies

We can also enable asynchronous processing with XML configuration by using the task namespace:

<task:executor id="myexecutor" pool-size="5"  />
<task:annotation-driven executor="myexecutor"/>

3. The @Async Annotation

First, let’s go over the rules. @Async has two limitations:

  • It must be applied to public methods only
  • Self-invocation — calling the async method from within the same class — won’t work

The reasons are simple: The method needs to be public so it can be proxied. Self-invocation doesn’t work because it bypasses the proxy and calls the underlying method directly.

3.1. Methods With Void Return Type

This is the simple way to configure a method with a void return type to run asynchronously:

@Async
public void asyncMethodWithVoidReturnType() {
    System.out.println("Execute method asynchronously. " 
      + Thread.currentThread().getName());
}

3.2. Methods With Return Type

We can also apply @Async to a method with a return type by wrapping the actual return in the Future:

@Async
public Future<String> asyncMethodWithReturnType() {
    System.out.println("Execute method asynchronously - " 
      + Thread.currentThread().getName());
    try {
        Thread.sleep(5000);
        return new AsyncResult<String>("hello world !!!!");
    } catch (InterruptedException e) {
        //
    }

    return null;
}

Spring also provides an AsyncResult class that implements Future. We can use this to track the result of the execution of the asynchronous method.

Now, let’s invoke the above method and retrieve the result of the asynchronous process using the Future object.

public void testAsyncAnnotationForMethodsWithReturnType()
  throws InterruptedException, ExecutionException {
    System.out.println("Invoking an asynchronous method. " 
      + Thread.currentThread().getName());
    Future<String> future = asyncAnnotationExample.asyncMethodWithReturnType();

    while (true) {
        if (future.isDone()) {
            System.out.println("Result from asynchronous process - " + future.get());
            break;
        }
        System.out.println("Continue doing something else. ");
        Thread.sleep(1000);
    }
}

3.3. Merging the Response of Two @Async Services

By using the completable method on the AsyncResult type of response, we wrap the actual method return into a CompletableFuture:

@Async
public CompletableFuture<String> asyncGetData() throws InterruptedException {
    System.out.println("Execute method asynchronously " + Thread.currentThread()
      .getName());
    Thread.sleep(4000);
    return new AsyncResult<>(super.getClass().getSimpleName() + " response !!! ").completable();
}

We’re now implementing the main service that we’ll use to merge the CompletableFuture responses of two @Async services:

@Service
public class AsyncService {

    @Autowired
    private FirstAsyncService fisrtService;
    @Autowired
    private SecondAsyncService secondService;

    public CompletableFuture<String> asyncMergeServicesResponse() throws InterruptedException {
        CompletableFuture<String> fisrtServiceResponse = fisrtService.asyncGetData();
        CompletableFuture<String> secondServiceResponse = secondService.asyncGetData();

        // Merge responses from FirstAsyncService and SecondAsyncService
        return fisrtServiceResponse.thenCompose(fisrtServiceValue -> secondServiceResponse.thenApply(secondServiceValue -> fisrtServiceValue + secondServiceValue));
    }
}

Let’s invoke the above service and retrieve the result of the asynchronous services using the CompletableFuture object.

public void testAsyncAnnotationForMergedServicesResponse() throws InterruptedException, ExecutionException {
    System.out.println("Invoking an asynchronous method. " + Thread.currentThread()
      .getName());
    CompletableFuture<String> completableFuture = asyncServiceExample.asyncMergeServicesResponse();

    while (true) {
        if (completableFuture.isDone()) {
            System.out.println("Result from asynchronous process - " + completableFuture.get());
            break;
        }
        System.out.println("Continue doing something else. ");
        Thread.sleep(1000);
    }
}

4. The Executor

By default, Spring uses a SimpleAsyncTaskExecutor to actually run these methods asynchronously. However, we can override the defaults at two levels: the application level or the individual method level.

4.1. Override the Executor at the Method Level

We need to declare the required executor in a configuration class:

@Configuration
@EnableAsync
public class SpringAsyncConfig {
    
    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor() {
        return new ThreadPoolTaskExecutor();
    }
}

Then, we should provide the executor name as an attribute in @Async:

@Async("threadPoolTaskExecutor")
public void asyncMethodWithConfiguredExecutor() {
    System.out.println("Execute method with configured executor - "
      + Thread.currentThread().getName());
}

4.2. Override the Executor at the Application Level

The configuration class should implement the AsyncConfigurer interface. So, it has to implement the getAsyncExecutor() method. Here, we will return the executor for the entire application. This now becomes the default executor to run methods annotated with @Async:

@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {
   @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }
}

5. Exception Handling

When a method return type is a Future, exception handling is easy. Future.get() method will throw the exception.

However, exceptions will not be propagated to the calling thread if the return type is void. So, we need to add extra configurations to handle exceptions.

We’ll create a custom async exception handler by implementing AsyncUncaughtExceptionHandler interface. The handleUncaughtException() method is invoked when there are any uncaught asynchronous exceptions:

public class CustomAsyncExceptionHandler
  implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(
      Throwable throwable, Method method, Object... obj) {
 
        System.out.println("Exception message - " + throwable.getMessage());
        System.out.println("Method name - " + method.getName());
        for (Object param : obj) {
            System.out.println("Parameter value - " + param);
        }
    }
    
}

In the previous section, we looked at the AsyncConfigurer interface implemented by the configuration class. As part of that, we also need to override the getAsyncUncaughtExceptionHandler() method to return our custom asynchronous exception handler:

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
    return new CustomAsyncExceptionHandler();
}

6. Conclusion

In this article, we looked at running asynchronous code with Spring.

We started with the very basic configuration and annotation to make it work. However, we also looked at more advanced configurations, such as providing our own executor or exception handling strategies.

As always, the full code presented in this article is available over on GitHub.

Course – LS (cat=Spring)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.