Asynchronous code in Java using Spring Framework

werner
3 min readMay 18, 2019

--

A few lessons I learnt on implementing the @Async attribute in the Java Spring framework.

Photo by nousnou iwasaki on Unsplash

I was recently tasked with getting the open source DevOps dashboard — Hygieia — implemented in order for various teams to gain insights into the deployment flows of various projects.

If you have not heard of Hygieia, it’s a open source project from Capital One that collects data from various DevOps related sources (Git, Sonar, CI/CD pipelines and more) and makes it available via a dashboard in order for teams and management to get a visual representation of the states of these various integrations in project workflows.

The problem

Hygieia comes with an Atlassian Bamboo (A CI/CD service) data collector included. The problem is, when your organization has 5000+ Bamboo projects, each with various plans and each of those with various sub-plans / branches, it takes the collector 4–5 hours to crawl all this build data and prepare it for the Hygieia UI.

Thus data on the dashboard could be many hours old at any time — not ideal for projects multiple daily releases.

Upon digging into the source code, I could see why. HTTP requests to the Bamboo API was happening synchronously — one by one :(

The Solution

So I set about refactoring the collector to asynchronously fetch batches (so as not to put too much strain on the server), and since it was written in Java using the Spring Framework, I had to get familiar with how it handles parallel processing — in this case within the context of batching HTTP requests.

You can view my Pull Request to the project here:

https://github.com/Hygieia/Hygieia/pull/2917

The Lessons

The code snippet below has a few noteworthy things highlighted:

@EnableAsync
@Component
class BambooAsyncFetch {
@Async
public CompletableFuture<JSONObject> fetchPlanAsync(...) {
...
}
}
  1. You must add the @EnableAsync attribute to the class which contains methods to be executed asynchronously (on another thread). Some devs will add this to Application.java (Spring entry class), but I think that’s a waste, as this attribute triggers a whole bunch of Spring specific overhead code not required in the rest of the app.
  2. Even if you use the @Async attribute and return some sort of Java future (in my case I returned aCompleteableFuture<JSONObject>), if you don’t do it right, everything will still execute synchronously even though the code compiles and runs fine.
  3. A function / method decorated with the @Async attribute HAS to be public
  4. The function HAS to have return type void or return a Java Future (or some implementation thereof)
  5. Async functions should be encapsulated in a separate “worker” Class. You cannot call an Async function from with its own class — as it will then still be executed synchronously.
  6. In order to implement the above “worker”, you cannot instantiate this class as usual — In other words, the following:
BambooAsyncFetch asyncFetch = new BambooAsyncFetch();
CompletableFuture<> t = asyncFetch.fetchPlanAsync(...);

Will not work! Your code will still execute synchronously!

To implement this Class correctly, you need to get a proxy to it — and the Spring framework has an attribute — @AutoWired — that makes it rather easy:

@Autowired
private BambooAsyncFetch bambooAsyncFetch;
public someFunction() { CompletableFuture<...> t = bambooAsyncFetch.fetchPlanAsync(...) }

Another way would be to get it’s Class context (depending on how your code is wired up):

ApplicationContext context = new AnnotationConfigApplicationContext(DefaultBambooClient.class);bambooAsyncFetch = context.getBean(BambooAsyncFetch.class);CompletableFuture<...> t = bambooAsyncFetch.fetchPlanAsync(...)

A bit extra

Once you can see your code executing correctly (i.e. asynchronously), you can tweak thread pools, by adding the following bean to your “worker” class

@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10);
executor.setMaxPoolSize(1000);
executor.setThreadNamePrefix("Async-");

return executor;
}

The Result

With this change I was able to pull the data on 5000+ build plans in about 20 minutes, instead of the almost 5 hours it took using the synchronous method.

Below is a comparison of pulling 10 plans with Async working / Not working.

Conclusion

I hope this post helps anyone who has to implement asynchronous code with the Spring framework for the first time. I must say that although I have always found Java VERY verbose, doing this refactor was refreshing.

Thanks for reading!

--

--