Spring Async is so easy! (once you know the tricks…)

All source code under my github account.

Summary

Spring Async is designed to make threads easy to implement. It was introduced in Spring 3.0 intending to make Future easier to use as an annotation on Components (since then, CompletableFuture<T> has been introduced in Java8 and adds a ‘promise’-like framework, a topic for another blog). The key to using it correctly is realizing you MUST create your @Async annotated classes in another (Spring component) class so Spring can proxy it. More info below…

Background

A little background (with as much depth as I can provide without hopefully going beyond what I know as truth). At its core Spring is a Dependency Injection (DI) container. You may know this already, I did too and still fell into this @Async trap. In addition to a DI container, Spring also has lots of magic with instrumentation, AKA Aspects. When you annotate something in Spring (ex. @Async) what you are actually doing is telling Spring “when you load this class, you should apply AOP as methods are invoked” (i.e. points that Spring will invoke prior/after to invoking the function itself).

If you look at the spring example for @Async you’ll see that it requires you to put your @Async methods in a new spring bean, you CANNOT annotate local functions and invoke them and see any new threads created.

Creating a local instance of the FacebookLookupService class does NOT allow the findPage method to run asynchronously. It must be created inside a @Configuration class or picked up by @ComponentScan.

Back to the point

So given the above, it becomes more obvious (hopefully) why the @Async annotation needs to be present, because if it’s not Spring can’t wrap the calls prior/after to the function in order to create all the wrappers necessary to run your code through the boilerplate code in spring.

The hard way

(coming)

The easy way

First we need to turn on our @Async notations.

  1. @SpringBootApplication
  2. @EnableAsync
  3. public class SpringAsyncApplication {
  4.  
  5.     public static void main(String[] args) {
  6.         SpringApplication.run(SpringAsyncApplication.class, args);
  7.     }
  8.  
  9. }

Then we need to create a new spring component (in this case an @Service) so that spring can wrap things correctly:

  1. @Service
  2. public class AsyncService {
  3.  
  4.     @Async
  5.     public Future doFoo(String someArgument)
  6.             throws InterruptedException {
  7.         System.out.println("about to start Foo service (good async)");
  8.         Thread.sleep(3000);
  9.         System.out.println("finishing Foo service (good async)");
  10.  
  11.         return new AsyncResult(
  12.                 "Congrats. You finished a real thread from Foo Service (good async)");
  13.     }
  14.  
  15.     @Async
  16.     public Future doBar(String someArgument)
  17.             throws InterruptedException {
  18.         System.out.println("about to start Bar service (good async)");
  19.         Thread.sleep(3000);
  20.         System.out.println("finishing Bar service (good async)");
  21.  
  22.         return new AsyncResult(
  23.                 "Congrats. You finished a real thread from Bar Service (good async)");
  24.     }
  25. }

The output of our application shows things executing in parallel

[jim@galago~/projects/SpringAsync (master)]$ mvn clean package
...
[jim@galago~/projects/SpringAsync (master)]$ java -Dspring.profiles.active=goodasync -jar target/async-0.0.1-SNAPSHOT.jar
...
Starting up REAL async services.
Took 3 ms to start threads.
about to start Foo service (good async)
about to start Bar service (good async)
finishing Foo service (good async)
finishing Bar service (good async)
Foo done in 3018 ms.
Bar done in 3018 ms.
Total time ellapsed 3018 ms.

It took approximately 3s to execute both threads in parallel. If this were serially executed we would have seen things finish in over 6s (see ‘the wrong way’)

The wrong way

In this example we annotated local functions with @Async …. and things execute serially:

  1. @Component
  2. public class MyRunner implements CommandLineRunner {
  3.     @Autowired
  4.     private AsyncService realAsyncService;
  5.  
  6.     @Value("${app.goodasync:false}")
  7.     private boolean isGoodAsync;
  8.  
  9.     @Override
  10.     public void run(String... args) throws Exception {
  11.         Future fooService;
  12.         Future barService;
  13.  
  14.         StopWatch swOverall = new StopWatch();
  15.         swOverall.start("overall");
  16.  
  17.         StopWatch sw = new StopWatch();
  18.         sw.start();
  19.  
  20.         if (isGoodAsync) {
  21.             // call async methods through service call
  22.             System.out.println("Starting up REAL async services.");
  23.             fooService = this.realAsyncService.doFoo("an argument");
  24.             barService = this.realAsyncService.doBar("another argument");
  25.         } else {
  26.             System.out.println("Starting up BROKEN async services.");
  27.             fooService = this.doFoo("an argument");
  28.             barService = this.doBar("another argument");
  29.         }
  30.  
  31.         sw.stop();
  32.         System.out.println(
  33.                 "Took " + sw.getTotalTimeMillis() + " ms to start threads.");
  34.  
  35.         sw.start();
  36.         fooService.get();
  37.         sw.stop();
  38.         System.out.println("Foo done in " + sw.getTotalTimeMillis() + " ms.");
  39.  
  40.         sw.start();
  41.         barService.get();
  42.         sw.stop();
  43.         System.out.println("Bar done in " + sw.getTotalTimeMillis() + " ms.");
  44.  
  45.         swOverall.stop();
  46.         System.out.println("Total time ellapsed "
  47.                 + swOverall.getTotalTimeMillis() + " ms.");
  48.     }
  49.  
  50.     @Async
  51.     public Future doFoo(String someArgument)
  52.             throws InterruptedException {
  53.         System.out.println("about to start Foo service (broken async)");
  54.         Thread.sleep(3000);
  55.         System.out.println("finishing Foo service (broken async)");
  56.  
  57.         return new AsyncResult(
  58.                 "You finished a broken thread from Foo Service (broken async)");
  59.     }
  60.  
  61.     @Async
  62.     public Future doBar(String someArgument)
  63.             throws InterruptedException {
  64.         System.out.println("about to start Bar service (broken async)");
  65.         Thread.sleep(3000);
  66.         System.out.println("finishing Bar service (broken async)");
  67.  
  68.         return new AsyncResult(
  69.                 "You finished a broken thread from Bar Service (broken async)");
  70.     }
  71.  
  72. }

We expect that we will see execution time serially occurring, meaning in 6s

[jim@galago~/projects/SpringAsync (master)]$ mvn clean package
...
[jim@galago~/projects/SpringAsync (master)]$ java -Dspring.profiles.active=badasync -jar target/async-0.0.1-SNAPSHOT.jar 
...
Starting up BROKEN async services.
about to start Foo service (broken async)
finishing Foo service (broken async)
about to start Bar service (broken async)
finishing Bar service (broken async)
Took 6002 ms to start threads.
Foo done in 6002 ms.
Bar done in 6002 ms.
Total time ellapsed 6004 ms.

And this is exactly what we see…

Bottom Line

Spring is such a useful framework because it captures boilerplate code in a useful manner to save you time developing AND to save you bugs in what is normally foundational code. You shouldn’t feel you MUST use Spring for everything, but if it solves your problem it will likely save you bugs you may have written. Threaded code is ESPECIALLY nasty when you miss something.

For me, once I realized my @Async code needed to be in a separate @Bean/@Service/etc I was able to benefit from Spring’s logic.

Thanks for reading!