Spring Boot 与 Docker
观察 GraphQL 的实际运行

本指南将引导您完成向GitHub创建异步查询的过程。重点是异步部分,这是在扩展服务时经常使用的功能。

你将构建

您将构建一个查询 GitHub 用户信息并通过 GitHub API 检索数据的查找服务。扩展服务的一种方法是运行耗时的后台作业,并使用 Java 的 CompletableFuture 接口等待结果。Java 的 CompletableFuture 是对常规 Future 的改进。它使得将多个异步操作串联并将它们合并为单个异步计算变得容易。

所需条件

如何完成本指南 {#_how_to_complete_this_guide}

与大多数 Spring 入门指南一样,您可以从头开始并完成每个步骤,也可以跳过您已经熟悉的基本设置步骤。无论哪种方式,您最终都会得到可运行的代码。

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

跳过基础步骤,请执行以下操作:

完成后,您可以将您的结果与 gs-async-method/complete 中的代码进行对比。

从 Spring Initializr 开始

您可以使用这个预初始化项目并点击生成以下载一个ZIP文件。该项目已配置为适合本教程中的示例。

要手动初始化项目:

  1. 访问 https://start.spring.io。该服务会拉取应用程序所需的所有依赖项,并为您完成大部分设置工作。

  2. 选择 Gradle 或 Maven 以及您想要使用的语言。本指南假设您选择了 Java。

  3. 点击 Dependencies 并选择 Spring Web

  4. 点击 Generate

  5. 下载生成的 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 调用并生成领域对象变得非常容易。

在本指南中,我们仅获取 nameblog 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-2GithubLookup-1),而第三次调用会被阻塞,直到其中一个线程可用。为了比较不使用异步功能时所需的时间,可以尝试注释掉 @Async 注解并再次运行服务。总耗时应该会显著增加,因为每个查询至少需要一秒钟。您还可以调整 Executor,例如增加 corePoolSize 属性。

本质上,任务耗时越长且同时调用的任务越多,异步处理带来的好处就越明显。代价是需要处理 CompletableFuture 接口。这增加了一层间接性,因为您不再直接处理结果。

总结

恭喜!您刚刚开发了一个异步服务,该服务可以让您同时扩展多个调用。

另请参阅

以下指南可能也会有所帮助:

本页目录