Anotación @Async de Spring para procesamiento asíncrono

La anotación @Async de Spring nos permite crear métodos asíncronos en Spring. Exploraremos @Async en este tutorial sobre el marco de trabajo de Spring. Para resumir, cuando anotamos un método de un bean con la anotación @Async, Spring lo ejecutará en un hilo separado y el llamador del método no esperará hasta que el método haya completado su ejecución. En este ejemplo, definiremos nuestro propio Servicio y usaremos Spring Boot 2. ¡Comencemos!

Ejemplo de Spring @Async

Utilizaremos Maven para crear un proyecto de ejemplo para la demostración. Para crear el proyecto, ejecuta el siguiente comando en un directorio que utilizarás como espacio de trabajo:

mvn archetype:generate -DgroupId=com.journaldev.asynchmethods -DartifactId=JD-SpringBoot-AsyncMethods -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

Si estás ejecutando Maven por primera vez, llevará unos segundos completar el comando generate porque Maven tiene que descargar todos los complementos y artefactos necesarios para realizar la tarea de generación. Así es como se ve la creación del proyecto:

\Creación de un proyecto con Maven\[/caption\]

Una vez que hayas creado el proyecto, siéntete libre de abrirlo en tu IDE favorito. El siguiente paso es agregar las dependencias adecuadas de Maven al proyecto. Aquí está el archivo pom.xml con las dependencias apropiadas:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

Finalmente, para entender todos los JAR que se agregan al proyecto cuando añadimos esta dependencia, podemos ejecutar un simple comando de Maven que nos permite ver un árbol de dependencias completo para un proyecto cuando añadimos algunas dependencias a él. Aquí tienes un comando que podemos usar:

mvn dependency:tree

Cuando ejecutamos este comando, nos mostrará el siguiente árbol de dependencias:

Activando el Soporte Asíncrono

Habilitar el soporte asíncrono es también solo cuestión de una sola anotación. Además de habilitar la ejecución asíncrona, también haremos uso de Executor, lo que nos permitirá definir límites de hilos también. Más sobre esto una vez que escribamos el código:

package com.journaldev.asynchexample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@SpringBootApplication
@EnableAsync
public class AsyncApp {
    ...
}

Aquí hicimos uso de la anotación @EnableAsync, que habilita la capacidad de Spring para ejecutar métodos asíncronos en un grupo de subprocesos en segundo plano. A continuación, también agregamos el Executor mencionado:

@Bean
public Executor asyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(2);
    executor.setMaxPoolSize(2);
    executor.setQueueCapacity(500);
    executor.setThreadNamePrefix("JDAsync-");
    executor.initialize();
    return executor;
}

Aquí, establecemos que un máximo de 2 hilos deben ejecutarse simultáneamente y el tamaño de la cola se establece en 500. Aquí está el código completo de la clase con las declaraciones de importación:

package com.journaldev.asynchexample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@SpringBootApplication
@EnableAsync
public class AsyncApp {

    public static void main(String[] args) {
        SpringApplication.run(AsyncApp.class, args).close();
    }

    @Bean
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("JDAsync-");
        executor.initialize();
        return executor;
    }
}

A continuación, haremos un servicio que realmente haga ejecuciones de hilos.

Creando un Modelo

Utilizaremos una API de Películas pública que simplemente devuelve los datos de una película. Definiremos nuestro modelo para lo mismo:

package com.journaldev.asynchexample;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class MovieModel {

    private String title;
    private String producer;

     // getters y setters estándar

    @Override
    public String toString() {
        return String.format("MovieModel{title='%s', producer='%s'}", title, producer);
    }
}

Hemos utilizado @JsonIgnoreProperties para que, si hay más atributos en la respuesta, puedan ser ignorados de manera segura por Spring.

Creando el Servicio

Es hora de definir nuestro Servicio que llamará a la API de Películas mencionada. Utilizaremos un simple RestTemplate para realizar una solicitud a la API GET y obtener resultados de forma asíncrona. Veamos el código de muestra que usamos:

package com.journaldev.asynchexample;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.concurrent.CompletableFuture;

@Service
public class MovieService {

    private static final Logger LOG = LoggerFactory.getLogger(MovieService.class);

    private final RestTemplate restTemplate;

    public MovieService(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder.build();
    }

    @Async
    public CompletableFuture lookForMovie(String movieId) throws InterruptedException {
        LOG.info("Looking up Movie ID: {}", movieId);
        String url = String.format("https://ghibliapi.herokuapp.com/films/%s", movieId);
        MovieModel results = restTemplate.getForObject(url, MovieModel.class);
         // Retardo artificial de 1 segundo con fines de demostración 
        Thread.sleep(1000L);
        return CompletableFuture.completedFuture(results);
    }
}

Esta clase es un @Service lo que la hace elegible para la Escaneo de Componentes de Spring. El tipo de retorno del método lookForMovie es CompletableFuture lo que es un requisito para cualquier servicio asíncrono. Dado que el tiempo para la API puede variar, hemos agregado un retardo de 2 segundos para la demostración.

Creando un CommandLineRunner

Ejecutaremos nuestra aplicación usando un CommandLineRunner que es la forma más fácil de probar nuestra aplicación. Un CommandLineRunner se ejecuta justo después de que se hayan inicializado todos los beans de la aplicación. Veamos el código para CommandLineRunner:

package com.journaldev.asynchexample;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.concurrent.CompletableFuture;

@Component
public class ApplicationRunner implements CommandLineRunner {

    private static final Logger LOG = LoggerFactory.getLogger(ApplicationRunner.class);

    private final MovieService movieService;

    public ApplicationRunner(MovieService movieService) {
        this.movieService = movieService;
    }


    @Override
    public void run(String... args) throws Exception {
         // Iniciar el reloj 
        long start = System.currentTimeMillis();

         // Iniciar múltiples búsquedas asíncronas 
        CompletableFuture page1 = movieService.lookForMovie("58611129-2dbc-4a81-a72f-77ddfc1b1b49");
        CompletableFuture page2 = movieService.lookForMovie("2baf70d1-42bb-4437-b551-e5fed5a87abe");
        CompletableFuture page3 = movieService.lookForMovie("4e236f34-b981-41c3-8c65-f8c9000b94e7");

         // Unir todos los hilos para que podamos esperar hasta que todos estén completos 
        CompletableFuture.allOf(page1, page2, page3).join();

         // Imprimir resultados, incluido el tiempo transcurrido 
        LOG.info("Elapsed time: " + (System.currentTimeMillis() - start));
        LOG.info("--> " + page1.get());
        LOG.info("--> " + page2.get());
        LOG.info("--> " + page3.get());
    }
}

Acabamos de usar RestTemplate para hacer una solicitud a la API de ejemplo que usamos con algunos IDs de Películas seleccionados al azar. Ejecutaremos nuestra aplicación para ver qué salida muestra.

Ejecutando la aplicación

Cuando ejecutamos la aplicación, veremos la siguiente salida:

2018-04-13  INFO 17868 --- [JDAsync-1] c.j.a.MovieService  : Looking up Movie ID: 58611129-2dbc-4a81-a72f-77ddfc1b1b49
2018-04-13 08:00:09.518  INFO 17868 --- [JDAsync-2] c.j.a.MovieService  : Looking up Movie ID: 2baf70d1-42bb-4437-b551-e5fed5a87abe
2018-04-13 08:00:12.254  INFO 17868 --- [JDAsync-1] c.j.a.MovieService  : Looking up Movie ID: 4e236f34-b981-41c3-8c65-f8c9000b94e7
2018-04-13 08:00:13.565  INFO 17868 --- [main] c.j.a.ApplicationRunner  : Elapsed time: 4056
2018-04-13 08:00:13.565  INFO 17868 --- [main] c.j.a.ApplicationRunner  : --> MovieModel{title='My Neighbor Totoro', producer='Hayao Miyazaki'}
2018-04-13 08:00:13.565  INFO 17868 --- [main] c.j.a.ApplicationRunner  : --> MovieModel{title='Castle in the Sky', producer='Isao Takahata'}
2018-04-13 08:00:13.566  INFO 17868 --- [main] c.j.a.ApplicationRunner  : --> MovieModel{title='Only Yesterday', producer='Toshio Suzuki'}

Si observas detenidamente, solo se hicieron dos hilos para ejecutarse en la aplicación, a saber, JDAsync-1 y JDAsync-2.

Conclusión

En esta lección, estudiamos cómo podemos utilizar las capacidades asíncronas de Spring con Spring Boot 2. Lee más publicaciones relacionadas con Spring aquí.

Descargar el código fuente

Descargar el proyecto de ejemplo de Spring Boot Async

Source:
https://www.digitalocean.com/community/tutorials/spring-async-annotation