In questo articolo vogliamo descrivere una caratteristica molto interessante del framework Spring, ovvero la capacità di gestire compiti che richiedono una lunga elaborazione mediante l’utilizzo di thread asincroni. Questa caratteristica è molto utile quando si ha la necessità di realizzare servizi in grado di scalare in base al carico, e con Spring è possibile abilitarla semplicemente attraverso l’utilizzo di due semplici annotazioni: @Async
e @EnableAsync
.
Abilitazione del Supporto Asincrono
Procediamo innanzitutto creando un nuovo progetto Spring boot con Maven; il pom.xml
avrà il seguente aspetto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>it.javaboss</groupId> <artifactId>spring-async</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>1.8</java.version> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.4.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> |
Main
annonata con @SpringBootApplication
per abilitare tutte le caratteristiche di autoconfigurazione di Spring.
1 2 3 4 5 6 |
@SpringBootApplication public class MainApp { public static void main(String[] args) { SpringApplication.run(MainApp.class, args); } } |
Infine abilitiamo il supporto al processamento asincrono definendo una classe di configurazione ed annotandola con @EnableAsync
:
1 2 3 4 5 |
@Configuration @EnableAsync public class ApplicationConfiguration { } |
@Async
in thread separati.
Definizione del Metodo Asincrono
Come anticipato affinché l’elaborazione di un metodo avvenga in un thread separato è sufficiente annotare lo stesso con @Async
. Ci sono però alcune regole che devono essere tenute a mente quando si utilizza tale annotazione:
- Il metodo annotato deve essere pubblico, questo perché Spring utilizza una classe proxy per la sua gestione.
- Il punto di invocazione del metodo non deve essere all’interno della classe che lo definisce, altrimenti verrebbe bypassata la classe proxy che lo gestisce.
- Se il metodo deve restituire un valore questo dovrà essere di tipo
CompletableFuture
oFuture
.
Seguendo queste regole, introduciamo quindi una classe BatchService
in cui definiamo il metodo batchUpdate
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Service public class BatchService { private static final Logger log = LoggerFactory.getLogger(BatchService.class); @Async public void batchUpdate() { log.debug( "Begin Batch Update: " + Thread.currentThread().getName() ); try { Thread.sleep(2000); } catch (InterruptedException e) {} log.debug( "End Batch Update: " + Thread.currentThread().getName() ); } } |
Infine definiamo la classe BatchController
in cui implementiamo un semplice metodo REST start
per l’attivazione del batch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@RestController @RequestMapping("/batch") public class BatchController { private static final Logger log = LoggerFactory.getLogger(BatchController.class); @Autowired BatchService batchService; @GetMapping("/start") public void start() { log.debug( "Begin Batch Starter: " + Thread.currentThread().getName() ); batchService.batchUpdate(); log.debug( "End Batch Starter: " + Thread.currentThread().getName() ); } } |
Una volta avviato il progetto con la usuale istruzione maven mvn spring-boot:run
sarà possibili avviare il nostro batch asincrono aprendo il browser ed inserendo nella barra degli indirizzi la URL: http://localhost:8080/batch/start
. DI seguito è riportato il log generato, in cui si nota come le classi BatchController
e BatchService
sono eseguite su due thread differenti.
1 2 3 4 |
2020-10-01 11:46:43.614 DEBUG 14793 --- [nio-8080-exec-1] it.javaboss.BatchController : Begin Batch Starter: http-nio-8080-exec-1 2020-10-01 11:46:43.617 DEBUG 14793 --- [nio-8080-exec-1] it.javaboss.BatchController : End Batch Starter: http-nio-8080-exec-1 2020-10-01 11:46:43.620 DEBUG 14793 --- [ task-1] it.javaboss.BatchService : Begin Batch Update: task-1 2020-10-01 11:46:45.625 DEBUG 14793 --- [ task-1] it.javaboss.BatchService : End Batch Update: task-1 |
Gestione del Pool di Thread
Come è facile immaginare per la gestione dei processi in background Spring utilizza un pool di Thread. Per la sua configurazione ricercherà, tra i bean definiti nel contesto dell’applicazione, un bean di tipo TaskExecutor
o un chiamato taskExecutor
. Se un bean con queste caratteristiche non viene trovato, Spring instanzierà uno di quelli predefiniti (si veda qui) come ad esempio il ThreadPoolTaskExecutor
o SimpleAsyncTaskExecutor
.
Più interessante però è vedere come sia possibile personalizzare il comportamento del pool secondo le nostre esigenze. A tale scopo abbiamo due possibili opzioni:
- dichiarare l’executor da utilizzare al livello di metodo (quello annotato con
@Async
per intenderci); - definire l’executor di default a livello di applicazione.
Nel primo caso dobbiamo innanzitutto definire un beal che implementa Executor
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Configuration @EnableAsync public class ApplicationConfiguration { @Bean(name = "threadPoolTaskExecutor") public Executor asyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(4); executor.setQueueCapacity(50); executor.setThreadNamePrefix("Batch-Thread-"); executor.initialize(); return executor; } } |
Quindi dobbiamo citarlo nell’annotazione @Async
del metodo batchUpdate
:
1 2 3 4 |
@Async("threadPoolTaskExecutor") public void batchUpdate() { .... } |
Se eseguiamo l’applicazione come fatto sopra, vedremo nei log che il nome del thread è ora Method-Thread::1
, concordemente a quanto definito in configurazione impostando la proprietà setThreadNamePrefix
dell’Executor
.
Volendo invece optare per il secondo caso, in cui vogliamo sovrascrivere Executor
a livello dell’intera applicazione, è necessario implementare l’interfaccia AsyncConfigurer
, la quale definisce il metodo getAsyncExecutor()
che restituisce l’executor da utilizzare:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Configuration public class ServiceExecutorConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(4); taskExecutor.setMaxPoolSize(4); taskExecutor.setQueueCapacity(50); taskExecutor.initialize(); taskExecutor.setThreadNamePrefix("App-Thread-"); return taskExecutor; } } |
Questa volta eseguendo l’applicazione, dopo aver rimosso la dichiarazione dell’executor threadPoolTaskExecutor
dall’annotazione @Async
, noteremo nel log che il thread è ora chiamato App-Thread::1
.
Si noti, in conclusione, che la flessibilità di Spring consente non solo di definire un executor di default ma anche di avere più executor da utilizzare in modo differente su metodi asincroni diversi.
Il Codice Sorgente
Il codice sorgente del progetto è scaricabile qui spring-async.