Ez egy AI által fordított bejegyzés.
[Spring] @Async használatának módja
- Írás nyelve: Koreai
- •
- Referencia ország: Minden ország
- •
- Informatika
Válasszon nyelvet
A durumis AI által összefoglalt szöveg
- Bemutatja a Java aszinkron feldolgozás Spring @Async használatával történő implementálásának módját, előnyeit és figyelmeztetéseit.
- A Spring @Async használatával az @EnableAsync annotáció hozzáadásával és az aszinkron feldolgozásra szánt metódusokhoz az @Async annotáció hozzárendelésével egyszerűen implementálhatja az aszinkron feldolgozást.
- A szálkészlet beállításával hatékony szálakezelés érhető el, és a visszatérési típus függvényében használhatja a Future, ListenableFuture, CompletableFuture stb. a aszinkron feldolgozás eredményeinek visszaadására.
Java aszinkron feldolgozás
Mielőtt megvizsgálnánk a Spring @Async-et, elengedhetetlen az szinkron, aszinkron és a többszálas fogalmak ismerete. Feltételezve, hogy ismerjük ezeket a fogalmakat, nézzük meg a tiszta Java aszinkron feldolgozás módját kód formájában. Ha járatos vagy a Java szálakban, akkor ezt a fejezetet kihagyhatod.
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 + "");
}
}
Ha egy szinkron módon létrehozunk egy üzenet fogadására és egyszerűen kiírásra szolgáló funkciót, akkor a kódot a fent látható módon írhatjuk meg. Ha ezt többszálas aszinkron módon akarjuk megváltoztatni, akkor a következőképpen írhatjuk meg a forráskódot.
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 + "");
}
}
Ez a módszer azonban nagyon veszélyes, mert nem tudjuk kezelni a szálakat. Például, ha egyszerre 10 000 kérés érkezik, akkor nagyon rövid idő alatt 10 000 szálnak kellene létrejönnie. A szál létrehozásának költsége nem elhanyagolható, ezért befolyásolhatja a program teljesítményét, sőt OOM hiba is előfordulhat. Ezért szükséges a szálak kezelésére szálkészlet (Thread Pool) implementálása, amelyet Java az ExecutorService osztályban biztosít.
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 + "");
}
}
A szálak teljes számát 10-re korlátoztuk, és most már helyesen tudjuk végrehajtani a többszálas aszinkron feldolgozást. Ez a módszer azonban minden aszinkron módon feldolgozni kívánt metódusnál megköveteli az ExecutorService submit() metódusának alkalmazását, így ismétlődő módosításokat kell végeznünk. Más szavakkal, ha az elején szinkron logikával írtunk egy metódust, és azt aszinkronná akarjuk változtatni, akkor elkerülhetetlen a metódus maga logikájának módosítása.
Spring @Async
Egyszerű módszer
@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 + "");
}
}
Csupán az @EnableAsync annotációt kell hozzáadnunk az Application osztályhoz, és az @Async annotációt az aszinkron módon feldolgozni kívánt szinkron logikájú metódusokhoz. Ez a módszer azonban nem kezeli a szálakat. Miért? Mert az @Async alapértelmezett beállítása a SimpleAsyncTaskExecutor használatát határozza meg, amely nem szálkészlet, hanem egyszerűen szálakat hoz létre.
Szálkészlet használata
Először is távolítsuk el az @EnableAsync-et az Application osztályból. Ha az Application osztályban @EnableAutoConfiguration vagy @SpringBootApplication beállítás található, akkor futásidőben beolvassa a SpringAsyncConfig osztály (amelyet alább fogunk létrehozni) threadPoolTaskExecutor bean információit, amelyen @Configuration beállítás található.
@Configuration
@EnableAsync // Az Application helyett az aszinkron beállításos osztályhoz kell hozzáadni.
public class SpringAsyncConfig {
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(3); // Alapértelmezett szálak száma
taskExecutor.setMaxPoolSize(30); // Maximális szálak száma
taskExecutor.setQueueCapacity(100); // A sor mérete
taskExecutor.setThreadNamePrefix("Executor-");
return taskExecutor;
}
Beállíthatjuk a core és a max méretét. Elvárhatnánk, hogy a kezdeti core mérettel működjön, és ha nem tudja feldolgozni a további feladatokat, akkor a max méretig növekedjen a szálak száma. De ez nem így van.
Belsőleg egy Integer.MAX_VALUE méretű LinkedBlockingQueue jön létre, amelybe a core méretű szálak feldolgozzák a feladatokat. Ha a szálak nem tudják feldolgozni a feladatokat, akkor a sorban várnak. Ha a sor megtelt, akkor a maximális méretű szálak jönnek létre, amelyek feldolgozzák a feladatokat.
Ha a sor mérete túl nagy az Integer.MAX_VALUE-hoz képest, akkor beállíthatjuk a queueCapacity-t. A fenti beállítások alapján a kezdeti 3 szál feldolgozza a feladatokat. Ha a feldolgozási sebesség lelassul, akkor a feladatokat a 100 méretű sorban tárolják. Ha további kérelmek érkeznek, akkor legfeljebb 30 szál jön létre a feladatok feldolgozására.
Miután beállítottuk a szálkészletet, az @Async annotációval ellátott metódusokhoz csak a bean nevét kell hozzáadnunk.
@Service
public class MessageService {
@Async("threadPoolTaskExecutor")
public void print(String message) {
System.out.println(message);
}
Ha több szálkészlet típusát szeretnénk beállítani, akkor több bean létrehozó metódust kell létrehoznunk (például threadPoolTaskExecutor()), és az @Async beállításakor az kívánt szálkészlet bean-t kell megadnunk.
A visszatérési típus szerinti visszatérési forma
Visszatérési érték nélküli eset
Ez a helyzet akkor áll fenn, ha az aszinkron feldolgozásra szánt metódusnak nincs szüksége arra, hogy visszaküldjön egy eredményt. Ebben az esetben az @Async annotáció visszatérési típusát void-ra kell beállítani.
Visszatérési értékű eset
A Future, ListenableFuture és CompletableFuture típusokat használhatjuk visszatérési típusként. Az aszinkron metódus visszatérési formáját a new AsyncResult() segítségével kell becsomagolni.
[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());
}
}
}
// Végrehajtási eredmény
Task Start - 1
jayon-1
Task Start - 2
jayon-2
Task Start - 3
jayon-3
Task Start - 4
jayon-4
Task Start - 5
A future.get() blokkoló módon vár a kérés eredményére, amíg az meg nem érkezik. Ezért blokkoló aszinkron módszerré válik, ami nem túl hatékony. A Future-t általában nem használják.
[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()));
}
}
}
// Végrehajtási eredmény
Task Start - 1
Task Start - 3
Task Start - 2
jayon-1
jayon-2
Task Start - 5
jayon-3
Task Start - 4
jayon-4
A ListenableFuture segítségével hívásfüggvényekkel (callback) nem blokkoló módon végezhetünk feldolgozást. Az addCallback() metódus első paramétere a feladat befejeződésének hívásfüggvénye, a második paraméter pedig a feladat hibájának hívásfüggvénye. Megjegyzés: A szálkészlet core méretét 3-ra állítottuk be, ezért a "Task Start" üzenet kezdetben 3-szor jelenik meg.
[CompletableFuture]
A ListenableFuture segítségével is megvalósíthatunk nem blokkoló logikát, de ha a hívásfüggvényen belül egy újabb hívásfüggvényre van szükségünk, akkor a kód nagyon összetett lesz, és a "hívásfüggvény-pokol" nevű hibát okozhatja.
Természetesen ebben a tanfolyamban nem foglalkozunk részletesen a CompletableFuture-rel, így ha érdekelnek az összetett hívásfüggvények kezelésére szolgáló kódok, akkor kérjük, olvass el a következő linkeket.
@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;
});
}
}
A ListenableFuture hívásfüggvények definiálásához képest jobb az olvashatósága, és teljesen nem blokkoló módon hajtja végre a feladatokat. Ezért az @Async használatakor, ha visszatérési értékre van szükségünk, akkor a CompletableFuture használatát ajánljuk.
Az @Async előnyei
A fejlesztők szinkron módon írhatják a metódusokat, és ha aszinkronra van szükségük, akkor egyszerűen hozzáadhatják az @Async annotációt a metódushoz. Így a szinkron és aszinkron kódok könnyebben karbantarthatók.
Az @Async figyelmeztetései
Az @Async funkció használatához az @EnableAsync annotációt kell deklarálnunk. Ha nem adunk meg külön beállításokat, akkor az a proxy üzemmódban működik. Ez azt jelenti, hogy az @Async annotációval működő aszinkron metódusok mind betartják a Spring AOP korlátozásait. További információért lásd ezt a bejegyzést.
- Ha az @Async annotációt privát metódusokhoz adjuk hozzá, akkor az AOP nem fog működni.
- Ha ugyanazon az objektumon belül lévő metódusokat hívunk meg egymásból, akkor az AOP nem fog működni.