Effective Advice on Spring @Async: Final Part
Learn more about how Spring @Async works with HttpRequest.
Join the DZone community and get the full member experience.
Join For FreeIn my previous articles, I discussed the Spring Async concept and how to use it effectively. If you want to revisit these posts, check out the links below:
Part 1: How Spring Async works internally and how to use wisely so that it assigns tasks to new threads.
Part 2: How to handle exceptions, particularly if something goes wrong while executing a task.
In this part, we will discuss how Spring Async works with the web.
I am very excited to share an experience with you about Spring Async
and HttpRequest
, as an interesting incident happened in one of my projects. I believe that by sharing my experience, I can save some valuable time of yours in the future.
Let me try to depict the scenario:
Objective
My objective was to pass information from the UI to a back-end controller, which will do some work and eventually calls an async mail service that triggers a mail.
One of my juniors did the following code. I tried to replicate the code intention with the below code snippet, but this is not the actual code. Can you spot where the problem lies?
The Controller
The controller collects information from the UI as a form HTTP Servelet request, completes some operations, and passes it to the Async Mail Service.
package com.example.demo;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetController {
@Autowired
private AsyncMailTrigger greeter;
@RequestMapping(value = "/greet", method = RequestMethod.GET)
public String greet(HttpServletRequest request) throws Exception {
String name = request.getParameter("name");
greeter.asyncGreet(request);
System.out.println(Thread.currentThread() + " Says Name is " + name);
System.out.println(Thread.currentThread().getName() + " Hashcode" + request.hashCode());
return name;
}
}
For the Async Mail Service, I marked it as @Component
; you can easily change it to @Service
. Here, I have one method called asyncGreet
, which takes the HttpRequest
, fetches the information from there, and triggers the mail (this part is omitted for simplicity). Notice I put a Thread.sleep()
here. I will discuss the reasoning behind that later.
package com.example.demo;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class AsyncMailTrigger {
@Async
public void asyncGreet(HttpServletRequest request) throws Exception {
System.out.println("Trigger mail in a New Thread :: " + Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + " greets before sleep" + request.getParameter("name"));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " greets" + request.getParameter("name"));
System.out.println(Thread.currentThread().getName() + " Hashcode" + request.hashCode());
}
}
Now, the main class:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class SpringAsyncWebApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAsyncWebApplication.class, args);
}
}
If I run this program, the output will be very similar to below:
Thread[http-nio-8080-exec-1,5,main] Says Name is Shamik
http-nio-8080-exec-1 Hashcode 821691136
Trigger mail in a New Thread:: task-1
task-1 greets before sleep Shamik
task-1 greets null task-1 Hashcode 821691136
Pay attention to the output. The request still has the information before sleep, but after that, it magically disappears? Strange, isn't? But it is the same request object that hashcode proves the same.
What happened ? what is the reason behind the disappearance of the information from request? That was happening to my junior, the mail recipients, recipients name disappear from the request and mail is not triggered.
Let's Put Our Sherlock Hats On and Investigate the Problem
It is very common to have problems with the request. To understand the problem, let's have a look at how the request lifecycle works.
The request is created by the servlet container right before the call to servlet service method. In Spring, the request passes through a dispatcher servlet. In the Dispatcher servlet, it identifies the controller by request mapping and calls the desired method in the controlle. And when the request has been served, the servlet container either deletes the request object or resets the state of the request object. (This totally depends on the container implementation — it actually maintains a pool of requests). However, I am not going to dive into how the container maintains the request object in this post.
But keep one thing in mind: Once the request has been served and the response is committed, the container resets its state or destroys the request object.
Now, let's put the Spring Async part into the consideration. What async does is it picks one thread from the thread pool and assigns the task to it. In our case, we pass the request object to the async thread, and in the asyncGreet
method, we are trying to extract info directly from the request.
But as this is async, our main thread (the Controller part) will be not waiting for this thread to complete, so it prints the statement, commits the response, and refreshes the state of the request object.
Ironically, we pass the request object directly to the async thread. Still, at the point where the response is not committed in the main thread, the request holds the data. I explicitly put in a sleep statement, so in the main thread, the response can be committed and refreshes the request state. After sleep, we experience that there is no data in the request; it vanishes, which is a great experiment that proves the incident.
What We Will Learn From This Experiment?
Never pass a Request
object or any object related to Request
/Response
(headers) directly while using async; you never know when your response will be committed and refresh the state. If you do, you will face an intermittent error.
What Can Be Done?
If you need to pass a bunch of information from the request, you can create a value object, set the information, and pass the value object to Spring Async. In this manner, you can create a concrete solution:
RequestVO Object
package com.example.demo;
public class RequestVO {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Async Mail Service
package com.example.demo;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class AsyncMailTrigger {
@Async
public void asyncGreet(RequestVO reqVO) throws Exception {
System.out.println("Trigger mail in a New Thread :: " + Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + " greets before sleep" + reqVO.getName());
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " greets" + reqVO.getName());
}
}
Greet Controller
package com.example.demo;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetController {
@Autowired
private AsyncMailTrigger greeter;
@RequestMapping(value = "/greet", method = RequestMethod.GET)
public String greet(HttpServletRequest request) throws Exception {
String name = request.getParameter("name");
RequestVO vo = new RequestVO();
vo.setName(name);
//greeter.asyncGreet(request);
greeter.asyncGreet(vo);
System.out.println(Thread.currentThread() + " Says Name is " + name);
System.out.println(Thread.currentThread().getName() + " Hashcode" + request.hashCode());
return name;
}
}
Output
Thread[http-nio-8080-exec-1,5,main] Says Name is Shamik
http-nio-8080-exec-1 Hashcode 1669579896
Trigger mail in a New Thread:: task-1
task-1 greets before sleep Shamik
task-1 greets Shamik
Conclusion
Hope you enjoyed the article If you have questions, please leave any questions in the comment box.
Published at DZone with permission of Shamik Mitra, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments