![translation](https://cdn.durumis.com/common/trans.png)
Ceci est un post traduit par IA.
[Spring] Utilisation de @Async
- Langue de rédaction : Coréen
- •
-
Pays de référence : Tous les pays
- •
- Technologies de l'information
Choisir la langue
Texte résumé par l'IA durumis
- Ce document explique comment implémenter le traitement asynchrone Java en utilisant Spring @Async, ainsi que ses avantages et ses précautions.
- En utilisant Spring @Async, vous pouvez implémenter facilement le traitement asynchrone en ajoutant l'annotation @EnableAsync et en plaçant l'annotation @Async sur les méthodes que vous souhaitez traiter de manière asynchrone.
- La configuration du pool de threads permet une gestion efficace des threads, et vous pouvez utiliser Future, ListenableFuture et CompletableFuture en fonction du type de retour pour renvoyer les résultats du traitement asynchrone.
Traitement asynchrone Java
Avant d'examiner Spring @Async, il est essentiel de comprendre les concepts de synchronie, d'asynchronicité et de multithreading. En supposant que vous connaissiez ces concepts, examinons la façon de procéder au traitement asynchrone pur Java à l'aide du code. Si vous êtes familier avec les threads Java, vous pouvez ignorer ce chapitre.
public class MessageService {
public void print(String message) {
System.out.println(message);
}
}
public class Main {
public static void main(String[] args) {
MessageService messageService = new MessageService();
for (int i = 1; i <= 100; i++) {
messageService.print(i + "");
}
}
Si vous créez une fonction qui reçoit un message et l'affiche simplement de manière synchrone, vous pouvez écrire le code comme ci-dessus. Si vous le modifiez en mode asynchrone multithread, vous pouvez écrire le code source comme suit.
public class MessageService {
public void print(String message) {
new Thread(() -> System.out.println(message))
.start();
}
}
public class Main {
public static void main(String[] args) {
MessageService messageService = new MessageService();
for (int i = 1; i <= 100; i++) {
messageService.print(i + "");
}
}
Cependant, cette méthode est très dangereuse car elle ne permet pas de gérer les threads. Par exemple, si 10 000 appels sont effectués simultanément, 10 000 threads doivent être créés en très peu de temps. La création de threads n'est pas bon marché et peut avoir un impact négatif sur les performances du programme, voire provoquer une erreur OOM. Par conséquent, vous devez implémenter un pool de threads pour gérer les threads, et Java fournit la classe ExecutorService.
public class MessageService {
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
public void print(String message) {
executorService.submit(() -> System.out.println(message));
}
}
public class Main {
public static void main(String[] args) {
MessageService messageService = new MessageService();
for (int i = 1; i <= 100; i++) {
messageService.print(i + "");
}
}
Le nombre total de threads est limité à 10, et nous pouvons maintenant effectuer le traitement asynchrone multithread que nous souhaitons correctement. Cependant, cette méthode nécessite l'application de la méthode submit() d'ExecutorService à chaque méthode que nous souhaitons traiter de manière asynchrone, ce qui nécessite des modifications répétitives. En d'autres termes, si vous souhaitez convertir une méthode écrite initialement de manière synchrone en mode asynchrone, vous devez nécessairement modifier la logique de la méthode elle-même.
Spring @Async
Méthode simple
@EnableAsync
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@Service
public class MessageService {
@Async
public void print(String message) {
System.out.println(message);
}
}
@RequiredArgsConstructor
@RestController
public class MessageController {
private final MessageService messageService;
@GetMapping("/messages")
@ResponseStatus(code = HttpStatus.OK)
public void printMessage() {
for (int i = 1; i <= 100; i++) {
messageService.print(i + "");
}
}
Il suffit d'ajouter l'annotation @EnableAsync au-dessus de la classe Application et l'annotation @Async au-dessus de la méthode de logique synchrone que vous souhaitez traiter de manière asynchrone. Cependant, cette méthode ne gère pas les threads. En effet, la configuration par défaut de @Async est d'utiliser SimpleAsyncTaskExecutor, qui n'est pas un pool de threads, mais simplement une fonction de création de threads.
Utilisation d'un pool de threads
Tout d'abord, supprimez @EnableAsync de la classe Application. Si la classe Application est configurée avec @EnableAutoConfiguration ou @SpringBootApplication, elle lira les informations sur le bean threadPoolTaskExecutor de la classe SpringAsyncConfig (à créer plus tard) configurée au moment de l'exécution.
@Configuration
@EnableAsync // Application이 아닌, Async 설정 클래스에 붙여야 함.
public class SpringAsyncConfig {
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(3); // 기본 스레드 수
taskExecutor.setMaxPoolSize(30); // 최대 스레드 수
taskExecutor.setQueueCapacity(100); // Queue 사이즈
taskExecutor.setThreadNamePrefix("Executor-");
return taskExecutor;
}
Vous pouvez définir les tailles de core et de max. Dans ce cas, vous vous attendez à ce que le pool de threads fonctionne avec la taille de core initiale, puis que le nombre de threads augmente jusqu'à la taille de max s'il n'est pas possible de traiter davantage de tâches. Cependant, ce n'est pas le cas.
En interne, une file d'attente LinkedBlockingQueue de taille Integer.MAX_VALUE est créée. Si les threads core ne peuvent pas traiter les tâches, ils attendent dans la file d'attente. Lorsque la file d'attente est pleine, des threads sont créés jusqu'à la taille de max pour traiter les tâches.
Si vous ne souhaitez pas définir la taille de la file d'attente sur Integer.MAX_VALUE, vous pouvez définir queueCapacity. Si vous définissez les paramètres comme ci-dessus, le pool de threads fonctionnera avec 3 threads initialement. Si la vitesse de traitement est lente, les tâches sont placées dans une file d'attente de 100 éléments. Si des demandes supplémentaires sont reçues, un maximum de 30 threads sont créés pour traiter les tâches.
Une fois la configuration du pool de threads terminée, il suffit d'ajouter le nom du bean à la méthode annotée @Async.
@Service
public class MessageService {
@Async("threadPoolTaskExecutor")
public void print(String message) {
System.out.println(message);
}
Si vous souhaitez définir plusieurs types de pools de threads, vous pouvez créer plusieurs méthodes de création de beans telles que threadPoolTaskExecutor(), et insérer le bean de pool de threads que vous souhaitez lors de la configuration de @Async.
Forme de retour en fonction du type de retour
Cas où il n'y a pas de valeur de retour
C'est le cas où la méthode à traiter de manière asynchrone n'a pas besoin de renvoyer de résultat de traitement. Dans ce cas, vous devez définir le type de retour de l'annotation @Async sur void.
Cas où il y a une valeur de retour
Vous pouvez utiliser les types Future, ListenableFuture et CompletableFuture comme type de retour. Le format de retour de la méthode asynchrone peut être enveloppé dans new AsyncResult().
[Future]
@Service
public class MessageService {
@Async
public Future print(String message) throws InterruptedException {
System.out.println("Task Start - " + message);
Thread.sleep(3000);
return new AsyncResult<>("jayon-" + message);
}
}
@RequiredArgsConstructor
@RestController
public class MessageController {
private final MessageService messageService;
@GetMapping("/messages")
@ResponseStatus(code = HttpStatus.OK)
public void printMessage() throws ExecutionException, InterruptedException {
for (int i = 1; i <= 5; i++) {
Future future = messageService.print(i + "");
System.out.println(future.get());
}
}
}
// 실행 결과
Task Start - 1
jayon-1
Task Start - 2
jayon-2
Task Start - 3
jayon-3
Task Start - 4
jayon-4
Task Start - 5
future.get() joue un rôle de blocage en attendant que le résultat de la requête arrive. Cela devient donc une méthode de blocage asynchrone, ce qui n'est pas performant. En général, Future n'est pas utilisé.
[ListenableFuture]
@Service
public class MessageService {
@Async
public ListenableFuture print(String message) throws InterruptedException {
System.out.println("Task Start - " + message);
Thread.sleep(3000);
return new AsyncResult<>("jayon-" + message);
}
}
@RequiredArgsConstructor
@RestController
public class MessageController {
private final MessageService messageService;
@GetMapping("/messages")
@ResponseStatus(code = HttpStatus.OK)
public void printMessage() throws InterruptedException {
for (int i = 1; i <= 5; i++) {
ListenableFuture listenableFuture = messageService.print(i + "");
listenableFuture.addCallback(System.out::println, error -> System.out.println(error.getMessage()));
}
}
}
// 실행 결과
Task Start - 1
Task Start - 3
Task Start - 2
jayon-1
jayon-2
Task Start - 5
jayon-3
Task Start - 4
jayon-4
ListenableFuture permet de traiter les tâches de manière non bloquante via des rappels. Le premier paramètre de la méthode addCallback() définit la méthode de rappel de fin de tâche, et le second paramètre définit la méthode de rappel d'échec de tâche. Notez que la taille du thread core du pool de threads est définie sur 3, vous pouvez donc voir que les messages « Task Start » sont affichés 3 fois au début.
[CompletableFuture]
ListenableFuture permet de mettre en œuvre une logique non bloquante, mais si un rappel dans un rappel est nécessaire, cela peut entraîner du code très complexe appelé « hell of callbacks ».
Bien sûr, étant donné que nous n'allons pas aborder CompletableFuture en détail dans ce tutoriel, si vous êtes curieux de savoir comment gérer les rappels complexes, veuillez consulter le lien source ci-dessous.
@Service
public class MessageService {
@Async
public CompletableFuture print(String message) throws InterruptedException {
System.out.println("Task Start - " + message);
Thread.sleep(3000);
return new AsyncResult<>("jayon-" + message).completable();
}
}
@RequiredArgsConstructor
@RestController
public class MessageController {
private final MessageService messageService;
@GetMapping("/messages")
@ResponseStatus(code = HttpStatus.OK)
public void printMessage() throws InterruptedException {
for (int i = 1; i <= 5; i++) {
CompletableFuture completableFuture = messageService.print(i + "");
completableFuture
.thenAccept(System.out::println)
.exceptionally(error -> {
System.out.println(error.getMessage());
return null;
});
}
}
La lisibilité est meilleure que celle des rappels de ListenableFuture, et elle exécute parfaitement les fonctionnalités non bloquantes. Par conséquent, il est recommandé d'utiliser CompletableFuture lorsque vous utilisez @Async et que vous avez besoin d'une valeur de retour.
Avantages de @Async
Les développeurs peuvent écrire des méthodes de manière synchrone et, s'ils souhaitent une méthode asynchrone, il suffit d'ajouter l'annotation @Async au-dessus de la méthode. Vous pouvez donc créer du code qui est bien entretenu en termes de synchronie et d'asynchronicité.
Points à noter concernant @Async
Pour utiliser la fonction @Async, vous devez déclarer l'annotation @EnableAsync. Si vous ne configurez pas d'autres paramètres, elle fonctionne en mode proxy. En d'autres termes, toutes les méthodes asynchrones qui fonctionnent avec l'annotation @Async suivent les contraintes de Spring AOP. Pour plus d'informations, consultezthis post.
- Si vous ajoutez @Async à une méthode privée, AOP ne fonctionne pas.
- Si des méthodes dans le même objet s'appellent mutuellement, AOP ne fonctionne pas.