Effective Advice on Spring Async: Part 1

As per the current trend, I see developers from Juniors to Seniors all using Spring Boot as their weapon of choice to build software. The fact that it is developer friendly and its "convention over configuration" style helps the developer to only focus on business logic. If they are unsure as to how Springs works, just reviewing a Spring Boot tutorial can allow one to start using Spring Boot — it's that easy. Review a related tutorial covering how to implement OAuth 2.0 with Spring Boot.

Another part of Spring Boot that I like is that developers do not have to know Spring's inner details — just put some annotations in, write the business code, and voila! With that said, sometimes, you have to know how it works. What I am trying to say is that you need to know your tool better so you can utilize it like a pro. DZone's previously covered how to implement Spring Boot Security for Swagger.

In this article, I will attempt to give you a better idea as to how you can use asynchronous processing in Spring.

Any pieces of logic that are not directly associated with business logic (cross-cutting concerns) or logic that has a response not needed in the invoker context to determine the next flow or any business computation are ideal candidates for asyncronization. Also, when integrating to a distributed system, the asyncronization technique is being used to make them decoupled.

In Spring, we can use asynchronization using the @Async annotation. But if you use randomly @Async on top of a method and think your method will be invoked as asynchronous in a separate thread, you are wrong. You need to know how @Async works and it's limitations. Without that, you can't understand Async behavior.

How Does @Async Work?

When you put an Async annotation on a method underlying it, it creates a proxy of that object where Async is defined (JDK Proxy/CGlib) based on the proxyTargetClass property. Then, Spring tries to find a thread pool associated with the context to submit this method's logic as a separate path of execution. To be exact, it searches a unique TaskExecutor bean or a bean named as taskExecutor. If it is not found, then use the default SimpleAsyncTaskExecutor.

Now, as it creates a proxy and submits the job to the TaskExecutor thread pool, it has a few limitations that have to know. Otherwise, you will scratch your head as to why your Async did not work or create a new thread! Let's take a look.

Limitations of @Async 

1. Suppose you write a class and identify a method that will act as Async and put @Async on top of that method. Now, if you want to use that class from another class by creating local instance, then it will not fire the async. It has to be picked up by Spring @ComponentScan annotation or created inside a class marked  @Configuration.

Async Annotation Uses in a Class

package com.example.ask2shamik.springAsync.demo;

import java.util.Map;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class AsyncMailTrigger {

@Async
public void senMail(Map<String,String> properties) {
System.out.println("Trigger mail in a New Thread :: "  + Thread.currentThread().getName());
properties.forEach((K,V)->System.out.println("Key::" + K + " Value ::" + V));
}

}


Caller Class

package com.example.ask2shamik.springAsync.demo;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AsyncCaller {

@Autowired
AsyncMailTrigger asyncMailTriggerObject;

public void rightWayToCall() {
System.out.println("Calling From rightWayToCall Thread " + Thread.currentThread().getName());
asyncMailTriggerObject.senMail(populateMap());

}

public void wrongWayToCall() {
System.out.println("Calling From wrongWayToCall Thread " + Thread.currentThread().getName());
AsyncMailTrigger asyncMailTriggerObject = new AsyncMailTrigger();
asyncMailTriggerObject.senMail(populateMap());
}

private Map<String,String> populateMap(){
Map<String,String> mailMap= new HashMap<String,String>();
mailMap.put("body", "A Ask2Shamik Article");
return mailMap;

}
}


Here, I created two methods — one used the @Autowired version of AsyncMailtrigger, which will be picked by @ComponentScan, but in a  WrongWayToCall method, I create the object in local, so it will not be picked up by @ComponentScan, and hence, it will not spawn a new thread and will be executed inside the main thread.

Outcome

Calling From rightWayToCall Thread main
2019-03-09 14:08:28.893  INFO 8468 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
Trigger mail in a New Thread :: task-1
Key::body Value ::A Ask2Shamik Article
++++++++++++++++
Calling From wrongWayToCall Thread main
Trigger mail in a New Thread :: main
Key::body Value ::A Ask2Shamik Article


2. Never use @Async on top of a private method. In runtime, it will not able to create a proxy and, therefore, not work.

@Async
private void senMail() {
System.out.println("A proxy on Private method "  + Thread.currentThread().getName());

}


3. Never write an Async method in the same class where the caller method invokes the same Async methodAsync method in the same class where the caller method invokes the same Async method. So, always remember that when using this reference, Async does not work because, in this case, although it creates a proxy, the call bypasses the proxy and directly call the method so that Thread will not be spawned. This will prevent the developer from having the wrong assumption that it will work in an Async fashion. Most developers carelessly implement Async in this way, so be very careful when writing the Async caller method. It should be in a different class when calling the Async method.

Ask2shamik Async Invocation

Example

package com.example.ask2shamik.springAsync.demo;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class AsyncCaller {

@Autowired
AsyncMailTrigger asyncMailTriggerObject;

public void rightWayToCall() {
System.out.println("Calling From rightWayToCall Thread " + Thread.currentThread().getName());
asyncMailTriggerObject.senMail(populateMap());

}

public void wrongWayToCall() {
System.out.println("Calling From wrongWayToCall Thread " + Thread.currentThread().getName());
this.senMail(populateMap());
}

private Map<String,String> populateMap(){
Map<String,String> mailMap= new HashMap<String,String>();
mailMap.put("body", "A Ask2Shamik Article");
return mailMap;

}

@Async
public void senMail(Map<String,String> properties) {
System.out.println("Trigger mail in a New Thread :: "  + Thread.currentThread().getName());
properties.forEach((K,V)->System.out.println("Key::" + K + " Value ::" + V));
}

}


The last piece of advice is to execute the application. Please note that we use the @EnableAsync annotation. With this, Spring submits @Async methods in a background thread pool. This class can customize the used Executor by defining a new bean. I will explore this later with an example of how to do that in part two.

package com.example.ask2shamik.springAsync;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;

import com.example.ask2shamik.springAsync.demo.AsyncCaller;

@SpringBootApplication
@EnableAsync
public class DemoApplication {

@Autowired
AsyncCaller caller;

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);

}
 @Bean
 public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
        return args -> {
        caller.rightWayToCall();
        Thread.sleep(1000);
        System.out.println("++++++++++++++++");
        Thread.sleep(1000);
        caller.wrongWayToCall();

        };
    }
}


Conclusion

Well, I hope you are now able to understand how Async works internally and some of its limitation. In my next article, I will discuss how an exception handler works in Async. Stay tuned!

 

 

 

 

Top