本指南将引导您完成向GitHub创建异步查询的过程。重点是异步部分,这是在扩展服务时经常使用的功能。
你将构建
您将构建一个查询 GitHub 用户信息并通过 GitHub API 检索数据的查找服务。扩展服务的一种方法是运行耗时的后台作业,并使用 Java 的 CompletableFuture
接口等待结果。Java 的 CompletableFuture
是对常规 Future
的改进。它使得将多个异步操作串联并将它们合并为单个异步计算变得容易。
所需条件
-
大约15分钟
-
常用的文本编辑器或集成开发环境
-
Java 17 或更高版本
-
您也可以直接将代码导入到您的 IDE 中:
如何完成本指南 {#_how_to_complete_this_guide}
与大多数 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 开始
您可以使用这个预初始化项目并点击生成以下载一个ZIP文件。该项目已配置为适合本教程中的示例。
要手动初始化项目:
-
访问 https://start.spring.io。该服务会拉取应用程序所需的所有依赖项,并为您完成大部分设置工作。
-
选择 Gradle 或 Maven 以及您想要使用的语言。本指南假设您选择了 Java。
-
点击 Dependencies 并选择 Spring Web。
-
点击 Generate。
-
下载生成的 ZIP 文件,这是一个根据您的选择配置好的 Web 应用程序的存档。
如果您的 IDE 集成了 Spring Initializr,您可以直接在 IDE 中完成这一过程。
您也可以从 Github 上 fork 该项目,并在您的 IDE 或其他编辑器中打开它。
创建一个 GitHub 用户的表示
在创建 GitHub 查询服务之前,您需要定义一个用于表示通过 GitHub API 检索的数据的模型。
为了模拟用户表示,创建一个资源表示类。为此,提供一个包含字段、构造函数和访问器的普通 Java 对象,如下例(来自 src/main/java/com/example/asyncmethod/User.java
)所示:
package com.example.asyncmethod;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown=true)
public class User {
private String name;
private String blog;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBlog() {
return blog;
}
public void setBlog(String blog) {
this.blog = blog;
}
@Override
public String toString() {
return "User [name=" + name + ", blog=" + blog + "]";
}
}
Spring 使用 Jackson JSON 库将 GitHub 的 JSON 响应转换为 User
对象。@JsonIgnoreProperties
注解告诉 Spring 忽略类中未列出的任何属性。这使得进行 REST 调用并生成领域对象变得非常容易。
在本指南中,我们仅获取 name
和 blog
URL 用于演示目的。
创建一个 GitHub 查询服务
接下来,您需要创建一个服务来查询 GitHub 以获取用户信息。以下代码(来自 src/main/java/com/example/asyncmethod/GitHubLookupService.java
)展示了如何实现这一点:
package com.example.asyncmethod;
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 GitHubLookupService {
private static final Logger logger = LoggerFactory.getLogger(GitHubLookupService.class);
private final RestTemplate restTemplate;
public GitHubLookupService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
@Async
public CompletableFuture<User> findUser(String user) throws InterruptedException {
logger.info("Looking up " + user);
String url = String.format("https://api.github.com/users/%s", user);
User results = restTemplate.getForObject(url, User.class);
// Artificial delay of 1s for demonstration purposes
Thread.sleep(1000L);
return CompletableFuture.completedFuture(results);
}
}
GitHubLookupService
类使用 Spring 的 RestTemplate
调用远程 REST 端点(api.github.com/users/),然后将响应转换为 User
对象。Spring Boot 自动提供了一个 RestTemplateBuilder
,它会根据任何自动配置项(例如 MessageConverter
)自定义默认值。
该类被标记为 @Service
注解,使其成为 Spring 组件扫描的候选对象,以便检测并添加到应用程序上下文中。
findUser
方法被标记为 Spring 的 @Async
注解,表示它应该在单独的线程中运行。该方法的返回类型是 CompletableFuture<User>
而不是 User
,这是任何异步服务的要求。此代码使用 completedFuture
方法返回一个已经完成的 CompletableFuture
实例,该实例包含了 GitHub 查询的结果。
创建
GitHubLookupService
类的本地实例并不会让findUser
方法异步运行。它必须在@Configuration
类中创建,或者通过@ComponentScan
来获取。
GitHub API 的响应时间可能会有所不同。为了在本指南后面展示其优势,我们为该服务额外添加了一秒的延迟。
使应用程序可执行
要运行一个示例,您可以创建一个可执行的 jar 文件。Spring 的 @Async
注解适用于 Web 应用程序,但您无需设置 Web 容器即可看到其优势。以下代码清单(来自 src/main/java/com/example/asyncmethod/AsyncMethodApplication.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;
}
}
Spring Initializr 为您创建了一个
AsyncMethodApplication
类。您可以在从 Spring Initializr 下载的 zip 文件中找到它(位于src/main/java/com/example/asyncmethod/AsyncMethodApplication.java
)。您可以将该类复制到您的项目中然后进行修改,或者直接从前面的代码清单中复制该类。
@SpringBootApplication
是一个便捷的注解,它包含了以下所有内容:
-
@Configuration
: 将该类标记为应用程序上下文的 bean 定义源。 -
@EnableAutoConfiguration
: 告诉 Spring Boot 根据类路径设置、其他 bean 和各种属性设置开始添加 bean。例如,如果类路径上有spring-webmvc
,此注解会将应用程序标记为 Web 应用程序,并激活关键行为,例如设置DispatcherServlet
。 -
@ComponentScan
: 告诉 Spring 在com/example
包中查找其他组件、配置和服务,使其能够找到控制器。
main()
方法使用 Spring Boot 的 SpringApplication.run()
方法来启动应用程序。您是否注意到没有一行 XML 代码?也没有 web.xml
文件。这个 Web 应用程序是 100% 纯 Java 的,您无需处理任何管道或基础设施的配置。
@EnableAsync 注解开启了 Spring 在后台线程池中运行 @Async
方法的能力。这个类还通过定义一个新的 bean 来自定义 Executor
。在这里,该方法被命名为 taskExecutor
,因为这是 Spring 搜索的特定方法名称。在我们的例子中,我们希望将并发线程数限制为两个,并将队列大小限制为 500。您还可以 调整更多内容。如果没有定义 Executor
bean,Spring 将使用 ThreadPoolTaskExecutor
。
此外,还有一个 CommandLineRunner,它注入了 GitHubLookupService
并调用该服务三次,以演示该方法是异步执行的。
您还需要一个类来运行应用程序。您可以在 src/main/java/com/example/asyncmethod/AppRunner.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 of 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());
}
}
构建可执行的 JAR
您可以通过 Gradle 或 Maven 从命令行运行该应用程序。您还可以构建一个包含所有必要依赖项、类和资源的单一可执行 JAR 文件并运行它。构建可执行 JAR 文件可以方便地在整个开发生命周期中、跨不同环境等场景下进行服务的交付、版本控制和部署。
如果您使用 Gradle,可以通过 ./gradlew bootRun
运行应用程序。或者,您可以使用 ./gradlew build
构建 JAR 文件,然后按如下方式运行该 JAR 文件:
java -jar build/libs/gs-async-method-0.1.0.jar
如果使用 Maven,您可以通过 ./mvnw spring-boot:run
来运行应用程序。或者,您可以使用 ./mvnw clean package
构建 JAR 文件,然后按照以下方式运行 JAR 文件:
java -jar target/gs-async-method-0.1.0.jar
这里描述的步骤将创建一个可运行的JAR文件。您也可以构建一个经典的WAR文件。
应用程序显示了日志输出,展示了每次对 GitHub 的查询。借助 allOf
工厂方法,我们创建了一个 CompletableFuture
对象数组。通过调用 join
方法,可以等待所有 CompletableFuture
对象完成。
以下列表展示了此示例应用程序的典型输出:
2016-09-01 10:25:21.295 INFO 17893 --- [ GithubLookup-2] hello.GitHubLookupService : Looking up CloudFoundry
2016-09-01 10:25:21.295 INFO 17893 --- [ GithubLookup-1] hello.GitHubLookupService : Looking up PivotalSoftware
2016-09-01 10:25:23.142 INFO 17893 --- [ GithubLookup-1] hello.GitHubLookupService : Looking up Spring-Projects
2016-09-01 10:25:24.281 INFO 17893 --- [ main] hello.AppRunner : Elapsed time: 2994
2016-09-01 10:25:24.282 INFO 17893 --- [ main] hello.AppRunner : --> User [name=Pivotal Software, Inc., blog=https://pivotal.io]
2016-09-01 10:25:24.282 INFO 17893 --- [ main] hello.AppRunner : --> User [name=Cloud Foundry, blog=https://www.cloudfoundry.org/]
2016-09-01 10:25:24.282 INFO 17893 --- [ main] hello.AppRunner : --> User [name=Spring, blog=https://spring.io/projects]
请注意,前两次调用发生在不同的线程中(GithubLookup-2
、GithubLookup-1
),而第三次调用会被阻塞,直到其中一个线程可用。为了比较不使用异步功能时所需的时间,可以尝试注释掉 @Async
注解并再次运行服务。总耗时应该会显著增加,因为每个查询至少需要一秒钟。您还可以调整 Executor
,例如增加 corePoolSize
属性。
本质上,任务耗时越长且同时调用的任务越多,异步处理带来的好处就越明显。代价是需要处理 CompletableFuture
接口。这增加了一层间接性,因为您不再直接处理结果。
总结
恭喜!您刚刚开发了一个异步服务,该服务可以让您同时扩展多个调用。
另请参阅
以下指南可能也会有所帮助: