Spring Boot - 使用@Async注解创建异步方法

x33g5p2x  于2022-10-07 转载在 Spring  
字(8.3k)|赞(0)|评价(0)|浏览(1005)

在这篇文章中,我们将使用Spring的@Async注解探索Spring或Spring Boot中的异步执行支持。
我们将用@Async注解一个bean的方法,使其在一个单独的线程中执行,也就是说,调用者将不会等待被调用方法的完成。

如果你已经在Spring或Spring Boot应用程序上工作,并且需要使用异步机制,那么下面这三个快速步骤将有助于设置。

第1步:启用异步支持

让我们先用Java配置启用异步处理--只需在配置类中添加@EnableAsync。 

@EnableAsync注解开启了Spring在后台线程池中运行@Async方法的能力。

第2步:为方法添加@Async注解

确保我们用@Async注解的方法需要是公共的,这样它就可以被代理。而自我调用不起作用,因为它绕过了代理,直接调用底层方法。

第三步:执行器(自定义默认)

让我们来定制ThreadPoolTaskExecutor。在我们的例子中,我们想把并发线程的数量限制在2个,把队列的大小限制在500个。你可以调整的东西还有很多。默认情况下,使用的是SimpleAsyncTaskExecutor,

  1. @Bean("threadPoolTaskExecutor")
  2. public TaskExecutor getAsyncExecutor() {
  3. ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  4. executor.setCorePoolSize(20);
  5. executor.setMaxPoolSize(1000);
  6. executor.setWaitForTasksToCompleteOnShutdown(true);
  7. executor.setThreadNamePrefix("Async-");
  8. return executor;
  9. }

就这样,这三个快速步骤帮助你使用Spring或Spring Boot创建异步服务。
让我们开发一个完整的例子来演示如何使用Spring或Spring Boot创建异步服务。

我们将建立什么?

我们将构建一个查询服务,通过GitHub的API查询GitHub用户信息并检索数据。扩展服务的一种方法是在后台运行昂贵的作业,并使用Java的CompletableFuture接口等待结果。Java的CompletableFuture是普通Future的进化版。它使多个异步操作的管道化变得很容易,将它们合并成一个单一的异步计算。

使用的工具和技术

  • Spring Boot - 2.0.6.RELEASE
  • JDK - 1.8或更高版本
  • Spring Framework - 5.0.9 RELEASE
  • Maven - 3.2以上
  • IDE - Eclipse或Spring Tool Suite (STS)

创建和导入Spring Boot项目

有很多方法可以创建Spring Boot应用程序。最简单的方法是使用http://start.spring.io/的Spring Initializr,它是一个在线Spring Boot应用程序生成器。

看上面的图,我们指定了以下细节。

  • Generate:Maven项目
  • Java版本:1.8(默认)。
  • Spring Boot:2.0.4
  • Group:net.javaguides.springboot
  • Artifact: springboot-async-example
  • Name: springboot-async-example
  • Description: Spring Boot的演示项目
  • Package Name:net.guides.springboot.springbootasyncexample
  • Packaging: jar (这是默认值)
  • Dependencies: Web

一旦,所有的细节被输入,点击Generate Project按钮将生成一个spring boot项目并下载。接下来,解压下载的压缩文件并将其导入你最喜欢的IDE。

项目目录结构

下面,图中显示了一个项目结构,供参考。 

pom.xml文件

  1. <?xmlversion="1.0"encoding="UTF-8"?>
  2. <project
  3. xmlns="http://maven.apache.org/POM/4.0.0"
  4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  5. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  6. <modelVersion>4.0.0</modelVersion>
  7. <groupId>net.guides.springboot</groupId>
  8. <artifactId>springboot-async-example</artifactId>
  9. <version>0.0.1-SNAPSHOT</version>
  10. <packaging>jar</packaging>
  11. <name>springboot-async-example</name>
  12. <description>Demo project for Spring Boot</description>
  13. <parent>
  14. <groupId>org.springframework.boot</groupId>
  15. <artifactId>spring-boot-starter-parent</artifactId>
  16. <version>2.0.6.RELEASE</version>
  17. <relativePath/>
  18. <!-- lookup parent from repository -->
  19. </parent>
  20. <properties>
  21. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  22. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  23. <java.version>1.8</java.version>
  24. </properties>
  25. <dependencies>
  26. <dependency>
  27. <groupId>org.springframework.boot</groupId>
  28. <artifactId>spring-boot-starter-web</artifactId>
  29. </dependency>
  30. <dependency>
  31. <groupId>org.springframework.boot</groupId>
  32. <artifactId>spring-boot-starter-test</artifactId>
  33. <scope>test</scope>
  34. </dependency>
  35. </dependencies>
  36. <build>
  37. <plugins>
  38. <plugin>
  39. <groupId>org.springframework.boot</groupId>
  40. <artifactId>spring-boot-maven-plugin</artifactId>
  41. </plugin>
  42. </plugins>
  43. </build>
  44. </project>

创建一个GitHub用户的代表

让我们创建带有姓名和博客字段的GitHub用户模型类。

  1. package net.guides.springboot.springbootasyncexample.model;
  2. import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
  3. @JsonIgnoreProperties(ignoreUnknown = true)
  4. public class User {
  5. private String name;
  6. private String blog;
  7. public String getName() {
  8. return name;
  9. }
  10. public void setName(String name) {
  11. this.name = name;
  12. }
  13. public String getBlog() {
  14. return blog;
  15. }
  16. public void setBlog(String blog) {
  17. this.blog = blog;
  18. }
  19. @Override
  20. public String toString() {
  21. return "User [name=" + name + ", blog=" + blog + "]";
  22. }
  23. }

注意,Spring使用Jackson JSON库将GitHub的JSON响应转换为用户对象。@JsonIgnoreProperties注解提示Spring忽略类中未列出的任何属性。这使得我们很容易进行REST调用并产生领域对象。

在这篇文章中,我们只抓取名字和博客的URL作为示范。

创建一个GitHub查询服务

接下来,我们需要创建一个查询GitHub的服务,以查找用户信息。

  1. package net.guides.springboot.springbootasyncexample.service;
  2. import java.util.concurrent.CompletableFuture;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.boot.web.client.RestTemplateBuilder;
  6. import org.springframework.scheduling.annotation.Async;
  7. import org.springframework.stereotype.Service;
  8. import org.springframework.web.client.RestTemplate;
  9. import net.guides.springboot.springbootasyncexample.model.User;
  10. @Service
  11. public class GitHubLookupService {
  12. private static final Logger logger = LoggerFactory.getLogger(GitHubLookupService.class);
  13. private final RestTemplate restTemplate;
  14. public GitHubLookupService(RestTemplateBuilder restTemplateBuilder) {
  15. this.restTemplate = restTemplateBuilder.build();
  16. }
  17. @Async("threadPoolTaskExecutor")
  18. public CompletableFuture < User > findUser(String user) throws InterruptedException {
  19. logger.info("Looking up " + user);
  20. String url = String.format("https://api.github.com/users/%s", user);
  21. User results = restTemplate.getForObject(url, User.class);
  22. // Artificial delay of 1s for demonstration purposes
  23. Thread.sleep(1000 L);
  24. return CompletableFuture.completedFuture(results);
  25. }
  26. }

GitHubLookupService类使用Spring的RestTemplate来调用一个远程REST点(api.github.com/users/),然后将答案转换成一个User对象。Spring Boot自动提供一个RestTemplateBuilder,用任何自动配置位(即MessageConverter)定制默认值。

findUser方法被标记为Spring的@Async注解,表明它将在一个单独的线程上运行。该方法的返回类型是CompletableFuture而不是User,这是任何异步服务的要求。这段代码使用completedFuture方法来返回一个CompletableFuture实例,该实例已经完成了GitHub查询的结果。

使应用程序可执行

为了运行一个样本,你可以创建一个可执行的jar。让我们使用CommandLineRunner,它注入了GitHubLookupService并调用该服务4次,以证明该方法是异步执行。

  1. package net.guides.springboot.springbootasyncexample;
  2. import java.util.concurrent.CompletableFuture;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.boot.CommandLineRunner;
  7. import org.springframework.boot.SpringApplication;
  8. import org.springframework.boot.autoconfigure.SpringBootApplication;
  9. import org.springframework.context.annotation.Bean;
  10. import org.springframework.core.task.TaskExecutor;
  11. import org.springframework.scheduling.annotation.EnableAsync;
  12. import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
  13. import net.guides.springboot.springbootasyncexample.model.User;
  14. import net.guides.springboot.springbootasyncexample.service.GitHubLookupService;
  15. @SpringBootApplication
  16. @EnableAsync
  17. public class SpringbootAsyncApplication implements CommandLineRunner {
  18. private static final Logger logger = LoggerFactory.getLogger(SpringbootAsyncApplication.class);
  19. @Autowired
  20. private GitHubLookupService gitHubLookupService;
  21. @Bean("threadPoolTaskExecutor")
  22. public TaskExecutor getAsyncExecutor() {
  23. ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  24. executor.setCorePoolSize(20);
  25. executor.setMaxPoolSize(1000);
  26. executor.setWaitForTasksToCompleteOnShutdown(true);
  27. executor.setThreadNamePrefix("Async-");
  28. return executor;
  29. }
  30. public static void main(String[] args) {
  31. SpringApplication.run(SpringbootAsyncApplication.class, args);
  32. }
  33. @Override
  34. public void run(String...args) throws Exception {
  35. // Start the clock
  36. long start = System.currentTimeMillis();
  37. // Kick of multiple, asynchronous lookups
  38. CompletableFuture < User > page1 = gitHubLookupService.findUser("PivotalSoftware");
  39. CompletableFuture < User > page2 = gitHubLookupService.findUser("CloudFoundry");
  40. CompletableFuture < User > page3 = gitHubLookupService.findUser("Spring-Projects");
  41. CompletableFuture < User > page4 = gitHubLookupService.findUser("RameshMF");
  42. // Wait until they are all done
  43. CompletableFuture.allOf(page1, page2, page3, page4).join();
  44. // Print results, including elapsed time
  45. logger.info("Elapsed time: " + (System.currentTimeMillis() - start));
  46. logger.info("--> " + page1.get());
  47. logger.info("--> " + page2.get());
  48. logger.info("--> " + page3.get());
  49. logger.info("--> " + page4.get());
  50. }
  51. }

@EnableAsync注解开启了Spring在后台线程池中运行@Async方法的能力。这个类还可以自定义使用的执行器。在我们的例子中,我们想把并发线程的数量限制在2个,把队列的大小限制在500个。还有很多东西你可以调教。默认情况下,使用的是SimpleAsyncTaskExecutor

运行应用程序

我们可以通过两种方式启动独立的Spring boot应用程序。

*我们使用的是maven,所以只需用./mvnw spring-boot:run来运行该应用程序。或者你可以用./mvnw clean package构建JAR文件。然后你就可以运行JAR文件了。

  1. java -jar target/springboot-async-example.jar
  • 下图显示了如何从IDE运行spring boot应用程序--右击--运行为--SpringbootAsyncApplication.main()方法作为一个独立的Java类。 

输出

当我们运行该应用程序时,我们将看到以下输出。

相关文章