L'exécution asynchrone dans une application Spring Boot repose sur deux annotations complémentaires : @EnableAsync pour activer le support au niveau de l'applicaiton, et @Async pour marquer les méthodes devant s'exécuter dans un thread séparé.
Activation de l'asynchronisme
L'annotation @EnableAsync doit être placée sur la classe principale de l'application. Sans elle, les annotations @Async seront tout simplement ignorées par le framework.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class ApplicationBootstrap {
public static void main(String[] args) {
SpringApplication.run(ApplicationBootstrap.class, args);
}
}
Service avec méthodes asynchrones
Chaque méthode annotée avec @Async s'exécutera dans un thread distinct. Il est possible de spécifier un pool de threads personnalisé en passant son nom en paramètre, sinon Spring Boot utilisera son exécuteur par défaut.
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.LinkedList;
import java.util.List;
@Service
public class NotificationService {
@Async("customExecutor")
public void dispatchEmail() {
logStep("Envoi email - début");
pause(5000);
logStep("Envoi email - fin");
}
@Async("customExecutor")
public void dispatchPushNotification() {
logStep("Envoi push - début");
pause(5000);
logStep("Envoi push - fin");
}
@Async
public void performBulkImport() {
logStep("Import massif - début");
generateLargeDataset();
logStep("Import massif - fin");
}
private int generateLargeDataset() {
List<Integer> dataset = new LinkedList<>();
for (int idx = 0; idx < 100000; idx++) {
dataset.add(idx);
}
return dataset.size();
}
private void logStep(String message) {
System.out.println(Thread.currentThread().getName() + " | " + message);
}
private void pause(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
Contrôleur exposant les endpoints
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NotificationController {
private final NotificationService notificationService;
@Autowired
public NotificationController(NotificationService notificationService) {
this.notificationService = notificationService;
}
@GetMapping("/api/notify")
public void triggerAllTasks() {
notificationService.performBulkImport();
notificationService.dispatchEmail();
notificationService.dispatchPushNotification();
}
}
Configuration d'un pool de threads dédié
Définir un pool personnalisé permet de contrôler le nombre de threads concurrents, la taille de la file d'attente et la politique de rejet. Le nom du bean correspondant est celui référencé dans @Async("customExecutor").
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
public class AsyncExecutorConfig {
private static final int CORE_THREADS = 20;
private static final int MAX_THREADS = 100;
private static final int IDLE_TIMEOUT = 10;
private static final int QUEUE_SIZE = 200;
private static final String NAME_PREFIX = "Async-Pool-";
@Bean("customExecutor")
public ThreadPoolTaskExecutor customExecutor() {
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setCorePoolSize(CORE_THREADS);
pool.setMaxPoolSize(MAX_THREADS);
pool.setQueueCapacity(QUEUE_SIZE);
pool.setKeepAliveSeconds(IDLE_TIMEOUT);
pool.setThreadNamePrefix(NAME_PREFIX);
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
pool.initialize();
return pool;
}
}
La stratégie CallerRunsPolicy fait exécuter la tâche rejetée par le thread émetteur, ce qui ralentit naturellement le producteur et évite une perte de données.
Causes courantes d'inefficacité de @Async
- La méthode annotée est déclarée
static— Spring ne peut pas créer de proxy autour d'une méthode statique. - La classe conteneur n'est pas détectée par le composant scan (absence de
@Service,@Component, etc.). - La méthode appelante et la méthode
@Asyncrésident dans la même classe — l'appel interne contourne le proxy Spring. - L'objet contenant la méthode asynchrone est instancié manuellement via
newau lieu d'être injecté par Spring. - L'annotation
@EnableAsyncest absente de la classe de configuration ou de démarrage.