Anotação @Async do Spring para Processamento Assíncrono

Primavera A anotação @Async nos permite criar métodos assíncronos no Spring. Vamos explorar @Async neste tutorial sobre o framework Spring. Em resumo, quando anotamos um método de um bean com a anotação @Async, o Spring o executará em uma thread separada, e o chamador do método não precisará esperar até que o método seja concluído. Vamos definir nosso próprio serviço e usar o Spring Boot 2 neste exemplo. Vamos começar!

Primavera @Async Exemplo

Vamos usar o Maven para criar um projeto de exemplo para a demonstração. Para criar o projeto, execute o seguinte comando em um diretório que você usará como espaço de trabalho:

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

Se estiver a executar o Maven pela primeira vez, demorará alguns segundos para concluir o comando `generate`, pois o Maven terá que baixar todos os plugins e artefatos necessários para realizar a tarefa de geração. Aqui está como se parece a criação de um projeto:

Depois de ter criado o projeto, sinta-se à vontade para abri-lo no seu IDE favorito. O próximo passo é adicionar as dependências apropriadas do Maven ao projeto. Aqui está o arquivo pom.xml com as dependências apropriadas:

<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>

Por fim, para entender todos os JARs que são adicionados ao projeto quando adicionamos essa dependência, podemos executar um comando simples do Maven que nos permite ver uma Árvore de Dependências completa para um projeto quando adicionamos algumas dependências a ele. Aqui está um comando que podemos usar:

mvn dependency:tree

Quando executamos este comando, ele nos mostrará a seguinte Árvore de Dependências:

Habilitando o Suporte Assíncrono

Ativar o suporte Async também é apenas uma questão de uma única anotação. Além de habilitar a execução Async, também faremos uso do Executor, que nos permite definir limites de Threads também. Mais sobre isso uma vez que escrevermos o 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 {
    ...
}

Aqui nós fizemos uso da anotação @EnableAsync, que habilita a capacidade do Spring de executar métodos Assíncronos em um pool de threads em segundo plano. Em seguida, também adicionamos o 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;
}

Aqui, definimos que no máximo 2 threads devem ser executadas simultaneamente e o tamanho da fila é definido como 500. Aqui está o código completo da classe com as declarações de importação:

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;
    }
}

Em seguida, faremos um serviço que na verdade faz execuções de Threads.

Criando um Modelo

Vamos usar uma API de Filme pública que apenas retorna os dados de um Filme. Vamos definir nosso modelo para o mesmo:

package com.journaldev.asynchexample;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class MovieModel {

    private String title;
    private String producer;

    // getters e setters padrão

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

Usamos @JsonIgnoreProperties para que, se houver mais atributos na resposta, eles possam ser ignorados com segurança pelo Spring.

Criando o Serviço

É hora de definirmos nosso Serviço que estará chamando a API de Filmes mencionada. Vamos usar um simples RestTemplate para acessar uma API GET e obter resultados de forma assíncrona. Vamos dar uma olhada no código de exemplo 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);
        // Atraso artificial de 1s para fins de demonstração
        Thread.sleep(1000L);
        return CompletableFuture.completedFuture(results);
    }
}

Esta classe é um @Service, tornando-a elegível para a varredura de componentes do Spring. O tipo de retorno do método lookForMovie é CompletableFuture, o que é um requisito para qualquer serviço assíncrono. Como o tempo para a API pode variar, adicionamos um atraso de 2 segundos para a demonstração.

Criando um Command Line Runner

Vamos executar nosso aplicativo usando um CommandLineRunner, que é a maneira mais fácil de testar nossa aplicação. Um CommandLineRunner é executado logo após a inicialização de todos os beans da aplicação. Vamos ver o código para o 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 o relógio
        long start = System.currentTimeMillis();

        // Iniciar várias consultas assí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");

        // Aguardar até que todas as threads terminem
        CompletableFuture.allOf(page1, page2, page3).join();

        // Imprimir resultados, incluindo o tempo decorrido
        LOG.info("Elapsed time: " + (System.currentTimeMillis() - start));
        LOG.info("--> " + page1.get());
        LOG.info("--> " + page2.get());
        LOG.info("--> " + page3.get());
    }
}

Usamos o RestTemplate para acessar a amostra da API com alguns IDs de filmes escolhidos aleatoriamente. Vamos executar nossa aplicação para ver qual saída ela mostra.

Executando o aplicativo

Ao executarmos o aplicativo, veremos a seguinte saída:

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'}

Se observarmos de perto, apenas dois threads foram criados para serem executados no aplicativo, nomeadamente JDAsync-1 e JDAsync-2.

Conclusão

Nesta lição, estudamos como podemos utilizar as capacidades Assíncronas do Spring com o Spring Boot 2. Leia mais posts relacionados ao Spring aqui.

Baixe o Código Fonte

Baixe o Projeto de Exemplo de Assincronia do Spring Boot

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