学习如何在 Spring Boot 中创建异步方法

异步方法将会在独立的线程中被执行,调用者无需等待它的完成,即可继续其他的操作。


0. 前言

你将创建什么?

你将创建一个使用 GitHub API 查询用户信息的方法,并且异步调用它。

调用 https://api.github.com/users/{username} 将会返回对应的 GitHub 用户信息:

{
"login": "lxiaocode",
"id": 48038525,
"node_id": "MDQ6VXNlcjQ4MDM4NTI1",
"avatar_url": "https://avatars2.githubusercontent.com/u/48038525?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/lxiaocode",
"html_url": "https://github.com/lxiaocode",
"followers_url": "https://api.github.com/users/lxiaocode/followers",
"following_url": "https://api.github.com/users/lxiaocode/following{/other_user}",
"gists_url": "https://api.github.com/users/lxiaocode/gists{/gist_id}",
"starred_url": "https://api.github.com/users/lxiaocode/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/lxiaocode/subscriptions",
"organizations_url": "https://api.github.com/users/lxiaocode/orgs",
"repos_url": "https://api.github.com/users/lxiaocode/repos",
"events_url": "https://api.github.com/users/lxiaocode/events{/privacy}",
"received_events_url": "https://api.github.com/users/lxiaocode/received_events",
"type": "User",
"site_admin": false,
"name": "lxiaocode",
"company": null,
"blog": "http://www.lxiaocode.com",
"location": null,
"email": null,
"hireable": null,
"bio": null,
"public_repos": 4,
"public_gists": 0,
"followers": 0,
"following": 0,
"created_at": "2019-02-27T01:43:37Z",
"updated_at": "2020-04-29T08:25:35Z"
}

你需要什么知识?


1. 创建初始项目

1.1 创建 Spring Initializr 项目

你可以使用 IntelliJ IDEA 进行创建项目,也可以在 Spring Initializr 进行创建。

1.2 导入 maven 依赖

pom.xml 文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lxiaocode</groupId>
<artifactId>async-method</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>async-method</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

2. 创建 Github User 资源对象

你现在已经完成对项目的初始设置,GitHub API 会返回大量的相关数据,但是我们只获取 name 还有 blog 相关信息:

@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 + '\'' +
'}';
}
}

User 用于获取 GitHub API 所返回的信息,@JsonIgnoreProperties 注解会忽略不存在的字段。

3. 创建 GitHub 查询服务

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

@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 username) throws InterruptedException {
logger.info("looking up " + username);
String url = String.format("https://api.github.com/users/%s", username);

User result = restTemplate.getForObject(url, User.class);
// 模拟延迟
Thread.sleep(2000);
return CompletableFuture.completedFuture(result);
}
}

使用 RestTemplate 调用远程 RESTful API 服务,User 用于接收返回的数据。@Service 注解会将该类标记为 Spring 组件,并创建为 Spring Bean 保存在应用上下文中。findUser() 方法使用了 @Async 注解,表示该方法为异步方法,会在单独的线程中执行。该方法的返回类型为 CompletableFuture<T>,这是异步方法的所需的返回类型。

使用 Thread.sleep() 方法,睡眠线程 2 秒,模拟方法的延迟。

4. 开启异步功能

到此为止,你已经完成异步方法的创建,但是离使用异步方法还需要一步。我们需要到 Spring Boot 启动类中使用 @EnableAsync 注解开启异步功能:

@SpringBootApplication
@EnableAsync
public class AsyncMethodApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncMethodApplication.class, args);
}
}

5. 测试异步方法

5.1 调用异步方法

你已经成功创建了异步方法,现在只要调用异步方法以及接收它的返回。我们使用 CommandLineRunner 接口在程序初始化时调用异步方法:

@Component
public class AppRunner implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(AppRunner.class);

@Autowired
private GitHubLookupService gitHubLookupService;

@Override
public void run(String... args) throws Exception {
// 计时开始
long start = System.currentTimeMillis();

CompletableFuture<User> user1 = gitHubLookupService.findUser("lxiaocode");
CompletableFuture<User> user2 = gitHubLookupService.findUser("PivotalSoftware");
CompletableFuture<User> user3 = gitHubLookupService.findUser("CloudFoundry");
CompletableFuture<User> user4 = gitHubLookupService.findUser("Spring-Projects");

// 等待所有任务执行完毕
CompletableFuture.allOf(user1, user2, user3, user4).join();

logger.info("用时:" + (System.currentTimeMillis() - start));
// 获取结果
logger.info("-> " + user1.get());
logger.info("-> " + user2.get());
logger.info("-> " + user3.get());
logger.info("-> " + user3.get());

}
}

CommandLineRunner 接口有一个 run() 方法,当它被创建后就会调用该方法,常用于对应用程序的初始化。CompletableFuture.allOf() 方法用于等待参数中的 CompletableFuture<T> 接收数据,保证异步方法执行完毕,这就是异步方法的返回值为什么是 CompletableFuture<T>

5.2 不使用异步测试

looking up lxiaocode
looking up PivotalSoftware
looking up CloudFoundry
looking up Spring-Projects
用时:13144
-> User{name='lxiaocode', blog='http://www.lxiaocode.com'}
-> User{name='Pivotal Software, Inc.', blog='http://pivotal.io'}
-> User{name='Cloud Foundry', blog='https://www.cloudfoundry.org/'}
-> User{name='Cloud Foundry', blog='https://www.cloudfoundry.org/'}

不使用异步,调用四次方法总共用时:13144 ms。

由于不是异步方法,所有的方法都在一个线程中执行,导致后面的方法需要等待前面的方法执行完毕。

5.3 使用异步测试

looking up lxiaocode
looking up PivotalSoftware
looking up CloudFoundry
looking up Spring-Projects
用时:3585
-> User{name='lxiaocode', blog='http://www.lxiaocode.com'}
-> User{name='Pivotal Software, Inc.', blog='http://pivotal.io'}
-> User{name='Cloud Foundry', blog='https://www.cloudfoundry.org/'}
-> User{name='Cloud Foundry', blog='https://www.cloudfoundry.org/'}

使用异步,调用四次方法总共用时:3585 ms。

异步方法将会在独立的线程中被执行,也就说四次调用是在同一时间执行的。

GitHub 项目源码


参考文献:Spring | Guides

评论