创建异步方法
本指南介绍如何创建对 GitHub 的异步查询,重点是异步部分。这是服务扩展时经常用到的功能。
你将构建什么
你将构建一个查询服务,通过 GitHub API 获取用户信息。扩展服务的一种方法,是把耗时任务放到后台运行,并使用 CompletableFuture 类等待结果。CompletableFuture 是普通 Future 的演进版本,它使多个异步操作的串联,以及合并为单个异步计算变得更容易。
所需条件
-
约15分钟
-
常用文本编辑器或 IDE
-
Java 17 或更高版本
-
也可以直接将代码导入 IDE:
如何完成本指南
和多数 Spring 入门指南 一样,你可以从头完成每一步,也可以跳过熟悉的基础设置。两种方式最终都会得到可工作的代码。
从头开始,请继续阅读 从 Spring Initializr 开始。
要跳过基础设置,按以下步骤操作:
-
下载 并解压本指南的源码仓库,或者用 Git 克隆:
git clone https://github.com/spring-guides/gs-async-method.git。 -
进入
gs-async-method/initial目录。 -
直接跳到 创建 GitHub 用户的数据表示。
完成后,可以与 gs-async-method/complete 中的代码对照。
从 Spring Initializr 开始
可以使用这个 预先初始化的项目,点击 Generate 下载 ZIP 文件。项目已按本教程示例配置。
手动初始化项目的方法:
-
访问 https://start.spring.io。这个服务会引入应用所需依赖,并完成大部分初始设置。
-
选择 Gradle 或 Maven,以及要使用的编程语言。
-
点击 Dependencies,选择 Spring Web 和 HTTP Client。
-
点击 Generate。
-
下载生成的 ZIP 文件,其中包含按你所选选项配置的 Web 应用。
| 如果 IDE 集成了 Spring Initializr,可直接在 IDE 内完成。 |
| 也可以在 GitHub 上 fork 项目,然后用 IDE 或其他编辑器打开。 |
创建 GitHub 用户的数据表示
创建 GitHub 查询服务前,需要定义从 GitHub API 获取的数据的表示方式。
为用户建模,创建不可变的资源表示类:Java record 或 Kotlin data class。示例如下:
Java
package com.example.asyncmethod;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public record User(String name, String blog) {
}
Kotlin
package com.example.asyncmethod
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class User(val name: String?, val blog: String?)
Spring 使用 Jackson JSON 库,将 GitHub 返回的 JSON 转成 User 对象。@JsonIgnoreProperties 注解告诉 Jackson 忽略类中未列出的属性,从而便于调用 REST 接口并构建领域对象。
本指南仅获取 name 和 blog URL,以便演示。
创建 GitHub 查询服务
接下来创建调用 GitHub 并查询用户信息的服务。代码如下:
Java
package com.example.asyncmethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import java.util.concurrent.CompletableFuture;
@Service
public class GitHubLookupService {
private static final Logger logger = LoggerFactory.getLogger(GitHubLookupService.class);
private final RestClient restClient;
public GitHubLookupService(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.build();
}
@Async
public CompletableFuture<User> findUser(String user) throws InterruptedException {
logger.info("Looking up " + user);
User results = restClient.get()
.uri("https://api.github.com/users/{user}", user)
.retrieve()
.body(User.class);
// Artificial delay of 1s for demonstration purposes
Thread.sleep(1000L);
return CompletableFuture.completedFuture(results);
}
}
Kotlin
package com.example.asyncmethod
import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Service
import org.springframework.web.client.RestClient
import org.springframework.web.client.requiredBody
import java.util.concurrent.CompletableFuture
@Service
class GitHubLookupService(restClientBuilder: RestClient.Builder) {
private val logger = LoggerFactory.getLogger(javaClass)
private val restClient = restClientBuilder.build()
@Async
fun findUser(user: String): CompletableFuture<User> {
logger.info("Looking up $user")
val results = restClient.get()
.uri("https://api.github.com/users/{user}", user)
.retrieve()
.requiredBody<User>()
// Artificial delay of 1s for demonstration purposes
Thread.sleep(1000L)
return CompletableFuture.completedFuture(results)
}
}
GitHubLookupService 类使用 Spring 的 RestClient 调用远程 REST 端点 api.github.com/users/,再将响应转换为 User 对象。Spring Boot 会自动提供 RestClient.Builder,把自动配置内容——例如 HttpMessageConverter——应用到默认设置,前提是 classpath 中存在 spring-boot-starter-restclient 依赖。
这个类标有 @Service 注解,因而可被 Spring 组件扫描发现,并加入应用上下文。
findUser 方法标有 Spring 的 @Async 注解,表示应在独立线程运行。返回类型是 CompletableFuture<User> 而不是 User,这是该异步服务的要求。代码通过 completedFuture 方法返回一个已经携带 GitHub 查询结果、处于已完成状态的 CompletableFuture 实例。
直接创建 GitHubLookupService 的本地实例,不会让 findUser 异步执行。它必须在 @Configuration 类中创建,或由 @ComponentScan 扫描发现。 |
GitHub API 响应时间会变化。为了展示后文的异步优势,示例服务额外加入了一秒延迟。
让应用可执行
为了运行示例,可以创建可执行 JAR。Spring 的 @Async 注解适用于 Web 应用,但观察其效果不必搭建 Web 容器。示例如下:
Java
package com.example.asyncmethod;
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 AsyncMethodApplication {
public static void main(String[] args) {
// close the application context to shut down the custom ExecutorService
SpringApplication.run(AsyncMethodApplication.class, args).close();
}
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("GithubLookup-");
executor.initialize();
return executor;
}
}
Kotlin
package com.example.asyncmethod
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
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
class AsyncMethodApplication {
@Bean
fun taskExecutor(): Executor = ThreadPoolTaskExecutor().apply {
corePoolSize = 2
maxPoolSize = 2
queueCapacity = 500
setThreadNamePrefix("GithubLookup-")
}
}
fun main(args: Array<String>) {
// close the application context to shut down the custom ExecutorService
runApplication<AsyncMethodApplication>(*args).close()
}
Spring Initializr 已为你创建 AsyncMethodApplication 类,可在下载的 ZIP 文件中找到。可以把它复制到项目后修改,也可以直接复制前面的示例类。 |
@SpringBootApplication 是一个便捷注解,包含以下各项:
-
@Configuration:将该类标记为应用上下文中 bean 定义的来源。 -
@EnableAutoConfiguration:根据 classpath、其他 bean 和各种属性配置,让 Spring Boot 自动添加 bean。例如,当 classpath 中有spring-webmvc时,它会将应用标记为 Web 应用,并启用设置DispatcherServlet等关键行为。 -
@ComponentScan:让 Spring 在com/example包中寻找其他组件、配置和服务,因而能找到控制器。
main() 方法通过 Spring Boot 的 SpringApplication.run() 启动应用。注意,这里没有一行 XML,也没有 web.xml 文件。这个 Web 应用完全使用 Java,不必处理底层基础设施配置。
@EnableAsync 启用 Spring 在后台线程池执行 @Async 方法的能力。这个类还通过定义新 bean 自定义 Executor。方法命名为 taskExecutor,因为这是 Spring 查找时使用的特定方法名称。本例把并发线程限制为2,把队列大小限制为500;此外还有 更多可以调整的参数。如果不定义 Executor bean,Spring 会使用 ThreadPoolTaskExecutor。
示例还提供一个 CommandLineRunner,注入 GitHubLookupService 并调用三次,以演示异步方法的执行。
还需要一个运行应用的类,代码如下:
Java
package com.example.asyncmethod;
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 AppRunner implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(AppRunner.class);
private final GitHubLookupService gitHubLookupService;
public AppRunner(GitHubLookupService gitHubLookupService) {
this.gitHubLookupService = gitHubLookupService;
}
@Override
public void run(String... args) throws Exception {
// Start the clock
long start = System.currentTimeMillis();
// Kick off multiple, asynchronous lookups
CompletableFuture<User> page1 = gitHubLookupService.findUser("PivotalSoftware");
CompletableFuture<User> page2 = gitHubLookupService.findUser("CloudFoundry");
CompletableFuture<User> page3 = gitHubLookupService.findUser("Spring-Projects");
// Wait until they are all done
CompletableFuture.allOf(page1, page2, page3).join();
// Print results, including elapsed time
logger.info("Elapsed time: " + (System.currentTimeMillis() - start));
logger.info("--> " + page1.get());
logger.info("--> " + page2.get());
logger.info("--> " + page3.get());
}
}
Kotlin
package com.example.asyncmethod
import org.slf4j.LoggerFactory
import org.springframework.boot.CommandLineRunner
import org.springframework.stereotype.Component
import java.util.concurrent.CompletableFuture
@Component
class AppRunner(private val gitHubLookupService: GitHubLookupService) : CommandLineRunner {
private val logger = LoggerFactory.getLogger(javaClass)
override fun run(vararg args: String) {
// Start the clock
val start = System.currentTimeMillis()
// Kick off multiple, asynchronous lookups
val page1 = gitHubLookupService.findUser("PivotalSoftware")
val page2 = gitHubLookupService.findUser("CloudFoundry")
val page3 = gitHubLookupService.findUser("Spring-Projects")
// Wait until they are all done
CompletableFuture.allOf(page1, page2, page3).join()
// Print results, including elapsed time
logger.info("Elapsed time: ${System.currentTimeMillis() - start}")
logger.info("--> ${page1.get()}")
logger.info("--> ${page2.get()}")
logger.info("--> ${page3.get()}")
}
}
构建可执行 JAR
可以从命令行通过 Gradle 或 Maven 运行应用,也可以构建包含所有依赖、类和资源的单个可执行 JAR 再运行。可执行 JAR 使开发生命周期中跨环境的分发、版本管理和部署更方便。
使用 Gradle 时,可以通过 ./gradlew bootRun 运行应用。也可以先用 ./gradlew build 构建 JAR,再按以下方式运行:
java -jar build/libs/gs-async-method-0.0.1-SNAPSHOT.jar
使用 Maven 时,可以通过 ./mvnw spring-boot:run 运行应用。也可以先用 ./mvnw clean package 构建 JAR,再按以下方式运行:
java -jar target/gs-async-method-0.0.1-SNAPSHOT.jar
应用日志会显示每次 GitHub 查询。通过 allOf 工厂方法创建一组 CompletableFuture 对象,再调用 join 方法,等待所有 CompletableFuture 完成。
下面是示例应用的典型输出,使用 Java 的 User record:
2026-04-09T14:50:49.565+02:00 INFO 94338 --- [ GithubLookup-2] c.e.asyncmethod.GitHubLookupService : Looking up CloudFoundry
2026-04-09T14:50:49.565+02:00 INFO 94338 --- [ GithubLookup-1] c.e.asyncmethod.GitHubLookupService : Looking up PivotalSoftware
2026-04-09T14:50:50.876+02:00 INFO 94338 --- [ GithubLookup-2] c.e.asyncmethod.GitHubLookupService : Looking up Spring-Projects
2026-04-09T14:50:52.067+02:00 INFO 94338 --- [ main] com.example.asyncmethod.AppRunner : Elapsed time: 2504
2026-04-09T14:50:52.068+02:00 INFO 94338 --- [ main] com.example.asyncmethod.AppRunner : --> User[name=Pivotal Software, Inc., blog=http://pivotal.io]
2026-04-09T14:50:52.068+02:00 INFO 94338 --- [ main] com.example.asyncmethod.AppRunner : --> User[name=Cloud Foundry, blog=https://www.cloudfoundry.org/]
2026-04-09T14:50:52.069+02:00 INFO 94338 --- [ main] com.example.asyncmethod.AppRunner : --> User[name=Spring, blog=https://spring.io/projects]
前两次调用在独立线程 GithubLookup-2 和 GithubLookup-1 中执行,第三次等待其中一个线程空闲。若想比较不启用异步时的耗时,可注释掉 @Async 再运行服务。由于每次查询至少花一秒,总耗时应明显增加。还可以调整 Executor,例如增加 corePoolSize。
原则上,任务耗时越长、同时调用的任务越多,异步带来的收益越明显。代价是必须处理 CompletableFuture:它增加了一层间接访问,因为你不再直接处理结果。
小结
恭喜!你已经开发出一个支持多个调用并发执行的异步服务。












暂无评论内容