![translation](https://cdn.durumis.com/common/trans.png)
Esta é uma postagem traduzida por IA.
Selecionar idioma
Texto resumido pela IA durumis
- Explicamos como implementar o processamento assíncrono Java usando o Spring @Async, seus benefícios e precauções.
- Usando o Spring @Async, você pode implementar o processamento assíncrono de forma simples adicionando a anotação @EnableAsync e anexando a anotação @Async ao método que você deseja processar de forma assíncrona.
- A configuração do pool de threads permite gerenciar threads de forma eficiente, e você pode retornar os resultados do processamento assíncrono usando Future, ListenableFuture e CompletableFuture, dependendo do tipo de retorno.
Processamento Assíncrono Java
Antes de olhar para o Spring @Async, os conceitos de sincronia, asincronia e multithreading são essenciais. Assumindo que você está familiarizado com esses conceitos, vamos dar uma olhada no processamento assíncrono Java puro em código. Se você estiver familiarizado com threads Java, pode pular este capítulo.
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 + "");
}
}
Se você criar uma função para receber uma mensagem e simplesmente imprimí-la de forma síncrona, você pode escrever o código como acima. Se você mudar isso para um método de multithreading assíncrono, você pode escrever o código-fonte como este.
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 + "");
}
}
No entanto, este método é muito perigoso porque você não pode gerenciar threads. Por exemplo, se 10.000 chamadas forem feitas simultaneamente, 10.000 threads precisarão ser criadas em um período muito curto. O custo de criar threads não é pequeno, então isso afetará negativamente o desempenho do programa e pode até causar um erro OOM. Portanto, para gerenciar threads, você precisa implementar um pool de threads, e o Java fornece a 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 + "");
}
}
O número total de threads é limitado a 10 e podemos fazer o processamento assíncrono de multithreading que queremos corretamente. No entanto, com este método, você precisa aplicar o método submit() do ExecutorService a cada método que deseja processar de forma assíncrona, portanto você precisa fazer alterações repetitivas. Ou seja, se você quiser mudar um método escrito originalmente em lógica síncrona para assíncrono, você precisará necessariamente mudar a lógica do próprio método.
Spring @Async
Método simples
@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 + "");
}
}
Basta adicionar a anotação @EnableAsync acima da classe Application e a anotação @Async acima do método de lógica síncrona que você deseja processar de forma assíncrona. No entanto, este método não gerencia threads. Isso ocorre porque a configuração padrão do @Async é usar o SimpleAsyncTaskExecutor, que não é um pool de threads, mas simplesmente cria threads.
Método usando um pool de threads
Primeiro, remova o @EnableAsync da classe Application. Se a classe Application tiver @EnableAutoConfiguration ou @SpringBootApplication configurado, ele lerá as informações do bean threadPoolTaskExecutor da classe SpringAsyncConfig (que será criada abaixo) no momento da execução, pois a classe SpringAsyncConfig tem @Configuration configurado.
@Configuration
@EnableAsync // Deve ser adicionado à classe de configuração assíncrona, não à Application.
public class SpringAsyncConfig {
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(3); // Número de threads padrão
taskExecutor.setMaxPoolSize(30); // Número máximo de threads
taskExecutor.setQueueCapacity(100); // Tamanho da fila
taskExecutor.setThreadNamePrefix("Executor-");
return taskExecutor;
}
Você pode definir o tamanho do core e do máximo. Nesse caso, você esperaria que ele funcionasse com o tamanho do core inicialmente e, se não conseguir processar mais trabalhos, as threads aumentariam para o tamanho máximo, mas não é esse o caso.
Internamente, um LinkedBlockingQueue do tamanho Integer.MAX_VALUE é criado e, se o trabalho não puder ser processado pelas threads do tamanho do core, ele ficará na fila. Se a fila ficar cheia, ele criará threads do tamanho máximo para processar o trabalho.
Nesse caso, se você não se sentir confortável em definir o tamanho da fila para Integer.MAX_VALUE, você pode configurar queueCapacity. Definindo isso como acima, ele será processado pelas 3 threads inicialmente, e se a velocidade de processamento diminuir, ele colocará as tarefas na fila de tamanho 100 e, se mais solicitações chegarem, ele criará até 30 threads para processar o trabalho.
Depois que a configuração do pool de threads estiver concluída, você poderá adicionar o nome do bean ao método ao qual a anotação @Async está anexada.
@Service
public class MessageService {
@Async("threadPoolTaskExecutor")
public void print(String message) {
System.out.println(message);
}
Se você quiser configurar vários tipos de pools de threads, crie vários métodos de criação de beans como threadPoolTaskExecutor() e insira o bean de pool de threads desejado ao configurar @Async.
Formato retornado por tipo de retorno
Sem valor de retorno
Este é o caso quando o método que precisa ser processado de forma assíncrona não precisa passar o resultado do processamento. Nesse caso, você pode definir o tipo de retorno da anotação @Async como void.
Com valor de retorno
Você pode usar os tipos Future, ListenableFuture e CompletableFuture como tipo de retorno. A forma de retorno do método assíncrono pode ser encapsulada em 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());
}
}
}
// Resultado da execução
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() age como bloqueio para aguardar até que o resultado da solicitação chegue. Portanto, ele se torna um método de bloqueio assíncrono e tem um desempenho ruim. Normalmente, Future não é usado.
[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()));
}
}
}
// Resultado da execução
Task Start - 1
Task Start - 3
Task Start - 2
jayon-1
jayon-2
Task Start - 5
jayon-3
Task Start - 4
jayon-4
ListenableFuture permite que você processe trabalhos de forma não bloqueante por meio de callbacks. O primeiro parâmetro do método addCallback() é o método de callback de conclusão da tarefa e o segundo parâmetro é o método de callback de falha da tarefa. Como referência, o core do pool de threads está definido como 3, então você pode ver que a mensagem "Task Start" é impressa 3 vezes no início.
[CompletableFuture]
Embora você possa implementar lógica não bloqueante com apenas o ListenableFuture, se você precisar de um callback dentro de um callback, isso levará a um código muito complicado, conhecido como inferno de callback.
Claro, não abordaremos o CompletableFuture em detalhes neste momento, então se você estiver interessado em lidar com callbacks complexos, consulte o link da fonte abaixo.
@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;
});
}
}
Ele é mais legível do que a definição de callback do ListenableFuture e executa completamente a função não bloqueante. Portanto, ao usar @Async, é recomendável usar o CompletableFuture se um valor de retorno for necessário.
Vantagens do @Async
Os desenvolvedores podem escrever métodos de forma síncrona e, se quiserem um método assíncrono, basta adicionar a anotação @Async acima do método. Portanto, você pode criar um código manutenível em termos de sincronia e asincronia.
Avisos sobre o @Async
Para usar a funcionalidade @Async, você precisa declarar a anotação @EnableAsync, mas se nenhuma configuração adicional for feita, ele funcionará no modo proxy. Ou seja, todos os métodos assíncronos que funcionam com a anotação @Async seguirão as restrições do Spring AOP. Para obter mais informações, consulteessa postagem.
- Se você anexar @Async a um método privado, o AOP não funcionará.
- Se você chamar métodos dentro do mesmo objeto, o AOP não funcionará.