Spring 异步方法入门:Java 与 Kotlin 完整示例

创建异步方法

本指南介绍如何创建对 GitHub 的异步查询,重点是异步部分。这是服务扩展时经常用到的功能。

你将构建什么

你将构建一个查询服务,通过 GitHub API 获取用户信息。扩展服务的一种方法,是把耗时任务放到后台运行,并使用 CompletableFuture 类等待结果。CompletableFuture 是普通 Future 的演进版本,它使多个异步操作的串联,以及合并为单个异步计算变得更容易。

所需条件

如何完成本指南

和多数 Spring 入门指南 一样,你可以从头完成每一步,也可以跳过熟悉的基础设置。两种方式最终都会得到可工作的代码。

从头开始,请继续阅读 从 Spring Initializr 开始。

要跳过基础设置,按以下步骤操作:

完成后,可以与 gs-async-method/complete 中的代码对照。

从 Spring Initializr 开始

可以使用这个 预先初始化的项目,点击 Generate 下载 ZIP 文件。项目已按本教程示例配置。

手动初始化项目的方法:

  1. 访问 https://start.spring.io。这个服务会引入应用所需依赖,并完成大部分初始设置。

  2. 选择 Gradle 或 Maven,以及要使用的编程语言。

  3. 点击 Dependencies,选择 Spring Web 和 HTTP Client。

  4. 点击 Generate。

  5. 下载生成的 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:它增加了一层间接访问,因为你不再直接处理结果。

小结

恭喜!你已经开发出一个支持多个调用并发执行的异步服务。

另请参阅

以下指南也可能有帮助:

想撰写新指南或参与现有指南?请阅读 贡献指南。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容