10 minutes read

Async methods can be used to execute different methods at the same time. This type of method execution is beneficial for many reasons. For example, async can be used to run background tasks, such as checking for new messages, at the same time as other methods which display data or handle user interaction. Async methods are also useful in situations where potentially long network requests might occur. For example, if your application needs to query a database, the query can be made asynchronously to avoid stalling other requests. In addition, async can be used to run multiple tasks at the same time when they are not reliant on each other. This significantly improves the average application performance. In this topic, we will learn how asynchronous tasks work and how to implement them.

Creating async methods

To demonstrate what async methods are, we are going to use a simple Spring application that generates random passwords. This application has four components. The first component sets up configuration options for the password.

Java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PasswordConfig {
    private static final String ALPHA = "abcdefghijklmnopqrstuvwxyz";
    private static final String NUMERIC = "0123456789";
    private static final String SPECIAL_CHARS = "!@#$%^&*_=+-/";

    @Bean
    public PasswordAlphabet allCharacters() {
        return new PasswordAlphabet(ALPHA + NUMERIC + SPECIAL_CHARS);
    }

    static class PasswordAlphabet {
        private final String characters;

        public PasswordAlphabet(String characters) {
            this.characters = characters;
        }

        public String getCharacters() {
            return characters;
        }
    }
}
Kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class PasswordConfig {

    @Bean
    fun allCharacters(): PasswordAlphabet {
        return PasswordAlphabet(ALPHA + NUMERIC + SPECIAL_CHARS)
    }

    class PasswordAlphabet(val characters: String)

    companion object {
        private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
        private const val NUMERIC = "0123456789"
        private const val SPECIAL_CHARS = "!@#$%^&*_=+-/"
    }
}

The second component is the generator logic which is used to build the generated passwords.

Java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Random;

@Component
public class PasswordGenerator {
    private static final Random random = new Random();
    private final PasswordAlphabet alphabet;

    public PasswordGenerator(@Autowired PasswordAlphabet alphabet) {
        this.alphabet = alphabet;
    }

    public String generate(int length) {
        String allCharacters = alphabet.getCharacters(); // take the characters from the bean
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < length; i++) {
            int index = random.nextInt(allCharacters.length());
            result.append(allCharacters.charAt(index));
        }
        return result.toString();
    }
}
Kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component

import java.util.Random

@Component
class PasswordGenerator(@Autowired private val alphabet: PasswordAlphabet) {
    fun generate(length: Int): String {
        val allCharacters = alphabet.characters // take the characters from the bean
        val result = StringBuilder()
        for (i in 0 until length) {
            val index = random.nextInt(allCharacters.length)
            result.append(allCharacters[index])
        }
        return result.toString()
    }

    companion object {
        private val random = Random()
    }
}

The third component is the class that runs our password generator. However, instead of using the PasswordGenerator presented above, it will run another version named AsyncPasswordGenerator.

Java
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class Runner implements CommandLineRunner {
    private final AsyncPasswordGenerator passwordGenerator;

    public Runner(AsyncPasswordGenerator passwordGenerator) {
        this.passwordGenerator = passwordGenerator;
    }

    @Override
    public void run(String... args) throws InterruptedException {
        this.passwordGenerator.generateLong();
        this.passwordGenerator.generateShort();
    }
}
Kotlin
import org.springframework.boot.CommandLineRunner
import org.springframework.stereotype.Component

@Component
class Runner(private val passwordGenerator: AsyncPasswordGenerator) : CommandLineRunner {
    override fun run(vararg args: String) {
        passwordGenerator.generateLong()
        passwordGenerator.generateShort()
    }
}

The final component is our AsyncPasswordGenerator, which implements asynchronous methods used to generate passwords.

Java
import org.springframework.stereotype.Component;

@Component
public class AsyncPasswordGenerator {
    private PasswordGenerator passwordGenerator;

    public AsyncPasswordGenerator(PasswordGenerator passwordGenerator) {
        this.passwordGenerator = passwordGenerator;
    }
}
Kotlin
import org.springframework.stereotype.Component

@Component
class AsyncPasswordGenerator(private val passwordGenerator: PasswordGenerator)

Suppose that we wanted to modify this application to generate the passwords asynchronously so that it can handle multiple requests at once. To do this, we will need to start by enabling the asynchronous functionality in our program. For that, we need to add the @EnableAsync annotation to the class containing the async methods. We will add it to the AsyncPasswordGenerator class as shown below.

Java
import org.springframework.scheduling.annotation.EnableAsync;

@Component
@EnableAsync
public class AsyncPasswordGenerator {

}
Kotlin
import org.springframework.scheduling.annotation.EnableAsync

@Component
@EnableAsync
class AsyncPasswordGenerator {

}

Now that async is enabled, we can create some methods that run asynchronously. To demonstrate this, let's create a method that generates a long password and another one that generates a short password. To make a method asynchronous, we must add the @Async annotation before it.

Java
import org.springframework.scheduling.annotation.Async;

@Component
@EnableAsync
public class AsyncPasswordGenerator {
    private PasswordGenerator passwordGenerator;

    public AsyncPasswordGenerator(PasswordGenerator passwordGenerator) {
        this.passwordGenerator = passwordGenerator;
    }

    @Async
    public void generateLong() throws InterruptedException {
        System.out.println("A long password: " + passwordGenerator.generate(10));
    }

    @Async
    public void generateShort() throws InterruptedException {
        System.out.println("A short password: " + passwordGenerator.generate(5));
    }
}
Kotlin
@Component
@EnableAsync
class AsyncPasswordGenerator(private val passwordGenerator: PasswordGenerator) {
    @Async
    fun generateLong() {
        println("A long password: " + passwordGenerator.generate(10))
    }

    @Async
    fun generateShort() {
        println("A short password: " + passwordGenerator.generate(5))
    }
}

When creating async methods, it is best to make them public, protected, or package-default. We advise against making async methods private.

Once we've done it, we can call these asynchronous methods in the same way we would call a usual method.

Java
@Override
public void run(String... args) throws InterruptedException {
    this.passwordGenerator.generateLong();
    this.passwordGenerator.generateShort();
}
Kotlin
override fun run(vararg args: String) {
    this.passwordGenerator.generateLong()
    this.passwordGenerator.generateShort()
}

When these methods are called, they run asynchronously in two different threads. Since these two methods don't return values, we don't need to worry about anything after executing them. We can simply try printing the thread names using Thread.currentThread() method to see the threads running our method calls and check if everything works correctly. The adjusted methods look like this:

Java
@Async
public void generateLong() throws InterruptedException {
    System.out.println("A long password: " + passwordGenerator.generate(10));
    System.out.println(Thread.currentThread());
}

@Async
public void generateShort() throws InterruptedException {
    System.out.println("A short password: " + passwordGenerator.generate(5));
    System.out.println(Thread.currentThread());
}
Kotlin
@Async
fun generateLong() {
    println("A long password: " + passwordGenerator.generate(10))
    println(Thread.currentThread())
}

@Async
fun generateShort() {
    println("A short password: " + passwordGenerator.generate(5))
    println(Thread.currentThread())
}

When we run our program, you will get an output similar to the one shown below.

A long password: 4te_ivxdbr
A short password: =ruzz
Thread[task-1,5,main]
Thread[task-2,5,main]

This result means that we have two threads named task-1 and task-2, and each method is executed in its own thread.

Async returns

Asynchronous methods can also return values, using what is known as CompletableFuture. Since asynchronous methods execute separately from an application's main thread, the return value from the method may come at any time. CompletableFuture allows us to resolve the return when it is needed, so that we don't have to wait when the method is called. The CompletableFuture class is a generic class, which means it can take any type, depending on the value we want to return.

Let's look at an example. Suppose we now wanted to return the generated password from both our short and long methods. To do this, we can modify the return types to make them a CompletableFuture. Specifically, since the values we return are text, we can use CompletableFuture<String>.

Java
@Async
public CompletableFuture<String> generateLong() throws InterruptedException {
    return CompletableFuture.completedFuture("A long password: " + generator.generate(10));
}

@Async
public CompletableFuture<String> generateShort() throws InterruptedException {
    return CompletableFuture.completedFuture("A short password: " + generator.generate(5));
}
Kotlin
@Async
fun generateLong(): CompletableFuture<String> {
    return CompletableFuture.completedFuture("A long password: " + generator.generate(10))
}

@Async
fun generateShort(): CompletableFuture<String> {
    return CompletableFuture.completedFuture("A short password: " + generator.generate(5))
}

Now, when we call these methods, we get back a CompletableFuturethat we can resolve to the value it contains at the time when we need it. To resolve the value, we are going to use the get() method of the CompletableFuture class. It is important to note that this method throws exceptions of InterruptedException and ExecutionException type, so we should catch these exceptions when we attempt to resolve the futures.

Java
@Override
public void run(String... args) throws InterruptedException {
    CompletableFuture<String> longPassFuture = this.passwordGenerator.generateLong();
    CompletableFuture<String> shortPassFuture = this.passwordGenerator.generateShort();

    try {
        String longPass = longPassFuture.get();
        String shortPass = shortPassFuture.get();

        System.out.println(longPass);
        System.out.println(shortPass);

    } catch (InterruptedException | ExecutionException ex) {
        System.out.println(ex);
    }
}
Kotlin
override fun run(vararg args: String) {
    val longPassFuture: CompletableFuture<String> = this.passwordGenerator.generateLong()
    val shortPassFuture: CompletableFuture<String> = this.passwordGenerator.generateShort()

    try {
        val longPass = longPassFuture.get()
        val shortPass = shortPassFuture.get()

        println(longPass)
        println(shortPass)

    } catch (ex: InterruptedException) {
        println(ex)
    } catch (ex: ExecutionException) {
        println(ex)
    }
}

With all this, we now have asynchronous methods that can return values.

Asynchronous executors

By default, Spring will place all threads inside a simple thread pool known as SimpleAsyncTaskExecutor. In some cases, you may want to have more control over the thread pool and specify details such as the number of threads allowed, or the prefix of the threads to make them easily identifiable. This customization can be done at either the method level or at the application level. To override the executor at the method level, you can start by creating a new @Bean that sets the desired properties.

Java
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

    executor.setCorePoolSize(4);
    executor.setMaxPoolSize(4);
    executor.setQueueCapacity(15);
    executor.setThreadNamePrefix("RunnerThread::");
    executor.initialize();

    return executor;
}
Kotlin
@Bean(name = ["threadPoolTaskExecutor"])
fun threadPoolTaskExecutor(): Executor {
    val executor = ThreadPoolTaskExecutor()
    
    executor.apply {
        corePoolSize = 4
        maxPoolSize = 4
        queueCapacity = 15
        setThreadNamePrefix("RunnerThread::")
        initialize()
    }
    
    return executor
}

This sets up an executor with a custom pool size of 4, a queue of 15, and a special name prefix to help identify the thread. To add an async method to this thread pool now, you can specify it in the @Async annotation as shown below.

Java
@Async("threadPoolTaskExecutor")
private CompletableFuture<String> generateLong() throws InterruptedException {
    return CompletableFuture.completedFuture("A long password: " + generator.generate(10));
}
Kotlin
@Async("threadPoolTaskExecutor")
fun generateLong(): CompletableFuture<String> {
    return CompletableFuture.completedFuture("A long password: " + generator.generate(10))
}

To override at the application level, you can extend the AsyncConfigurer interface and implement the getAsyncExecutor() method with the required customizations. Here is how this can be done:

Java
public class PasswordConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();

        taskExecutor.setCorePoolSize(4);
        taskExecutor.setMaxPoolSize(4);
        taskExecutor.setQueueCapacity(50);
        taskExecutor.setThreadNamePrefix("RunnerThread::");
        taskExecutor.initialize();
    
        return taskExecutor;
    }
}
Kotlin
class PasswordConfig : AsyncConfigurer {
    override fun getAsyncExecutor(): Executor {
        val taskExecutor = ThreadPoolTaskExecutor()
        
        taskExecutor.apply {
            corePoolSize = 4
            maxPoolSize = 4
            queueCapacity = 50
            setThreadNamePrefix("RunnerThread::")
            initialize()    
        }
        
        return taskExecutor
    }
}

This way all methods in the application will follow the same Executor standards. As for our generators of long and short passwords, we get an output similar to this:

A long password: s7ts34pd8a
A short password: s$@ds
Thread[RunnerThread::2,5,main]
Thread[RunnerThread::1,5,main]

Conclusion

Now you've learned how to implement async methods in Spring. Using asynchronous methods is ideal for situations where multiple tasks may need to run at the same time. When returning data from async methods, the value might not always be available immediately, which is whyCompletableFuture types should be used to get the value when we need it. If you want further customization of your async threads, you can customize executors at a method or application level according to your needs.

30 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo