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