Spring @Async 注解用于异步处理
Spring @Async注释允许我们在 Spring 中创建异步方法。让我们@Async
在本教程中探索 Spring 框架。简单来说,当我们注释 bean@Async
注释的方法时,Spring 将在单独的线程中执行该方法,并且该方法的调用者不会等到该方法执行完成。我们将在此示例中定义自己的服务并使用 Spring Boot 2。让我们开始吧!
Spring @Async示例
我们将使用 Maven 创建一个示例项目以供演示。要创建项目,请在将用作工作区的目录中执行以下命令:
mvn archetype:generate -DgroupId=com.journaldev.asynchmethods -DartifactId=JD-SpringBoot-AsyncMethods -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
如果您是第一次运行 maven,则需要几秒钟才能完成生成命令,因为 maven 必须下载所有必需的插件和工件才能完成生成任务。项目创建过程如下:创建项目后,请随意在您最喜欢的 IDE 中打开它。下一步是向项目添加适当的 Maven 依赖项。以下是pom.xml
具有适当依赖项的文件:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<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>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
最后,为了了解添加此依赖项时添加到项目中的所有 JAR,我们可以运行一个简单的 Maven 命令,该命令允许我们在向项目添加一些依赖项时查看项目的完整依赖树。这是我们可以使用的命令:
mvn dependency:tree
当我们运行此命令时,它将向我们显示以下依赖树:
启用异步支持
启用异步支持也一样,只需一个注释即可。除了启用异步执行之外,我们还将使用 Executor,它允许我们定义线程限制。编写代码后,我们将对此进行详细介绍:
package com.journaldev.asynchexample;
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 AsyncApp {
...
}
这里我们使用了@EnableAsync
注解,它使 Spring 能够在后台线程池中运行异步方法。接下来,我们还添加了上述的 Executor:
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("JDAsync-");
executor.initialize();
return executor;
}
这里我们设置最多有 2 个线程同时运行,队列大小设置为 500。下面是带有导入语句的类的完整代码:
package com.journaldev.asynchexample;
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 AsyncApp {
public static void main(String[] args) {
SpringApplication.run(AsyncApp.class, args).close();
}
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("JDAsync-");
executor.initialize();
return executor;
}
}
接下来我们将创建一个实际上由线程执行组成的服务。
制作模型
我们将使用一个公共电影 API,它只返回电影的数据。我们将为其定义模型:
package com.journaldev.asynchexample;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class MovieModel {
private String title;
private String producer;
// standard getters and setters
@Override
public String toString() {
return String.format("MovieModel{title='%s', producer='%s'}", title, producer);
}
}
我们已经使用过@JsonIgnoreProperties
,这样如果响应中有更多属性,Spring 就可以安全地忽略它们。
提供服务
现在是时候定义将调用上述 Movie API 的服务了。我们将使用一个简单的 RestTemplate 来调用 GET API 并异步获取结果。让我们看看我们使用的示例代码:
package com.journaldev.asynchexample;
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 MovieService {
private static final Logger LOG = LoggerFactory.getLogger(MovieService.class);
private final RestTemplate restTemplate;
public MovieService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
@Async
public CompletableFuture lookForMovie(String movieId) throws InterruptedException {
LOG.info("Looking up Movie ID: {}", movieId);
String url = String.format("https://ghibliapi.herokuapp.com/films/%s", movieId);
MovieModel results = restTemplate.getForObject(url, MovieModel.class);
// Artificial delay of 1s for demonstration purposes
Thread.sleep(1000L);
return CompletableFuture.completedFuture(results);
}
}
此类是 ,@Service
这使得它符合 Spring Component Scan 的要求。该lookForMovie
方法的返回类型是,CompletableFuture
这是任何异步服务的要求。由于 API 的时间可能会有所不同,我们添加了 2 秒的延迟以供演示。
制作命令行运行器
我们将使用 CommandLineRunner 运行我们的应用程序,这是测试应用程序的最简单方法。CommandLineRunner 在应用程序的所有 bean 初始化后立即运行。让我们看看 CommandLineRunner 的代码:
package com.journaldev.asynchexample;
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 ApplicationRunner implements CommandLineRunner {
private static final Logger LOG = LoggerFactory.getLogger(ApplicationRunner.class);
private final MovieService movieService;
public ApplicationRunner(MovieService movieService) {
this.movieService = movieService;
}
@Override
public void run(String... args) throws Exception {
// Start the clock
long start = System.currentTimeMillis();
// Kick of multiple, asynchronous lookups
CompletableFuture<MovieModel> page1 = movieService.lookForMovie("58611129-2dbc-4a81-a72f-77ddfc1b1b49");
CompletableFuture<MovieModel> page2 = movieService.lookForMovie("2baf70d1-42bb-4437-b551-e5fed5a87abe");
CompletableFuture<MovieModel> page3 = movieService.lookForMovie("4e236f34-b981-41c3-8c65-f8c9000b94e7");
// Join all threads so that we can wait until all are done
CompletableFuture.allOf(page1, page2, page3).join();
// Print results, including elapsed time
LOG.info("Elapsed time: " + (System.currentTimeMillis() - start));
LOG.info("--> " + page1.get());
LOG.info("--> " + page2.get());
LOG.info("--> " + page3.get());
}
}
我们刚刚使用 RestTemaplate 来访问我们使用的一些随机挑选的电影 ID 的示例 API。我们将运行我们的应用程序来查看它显示的输出。
运行应用程序
当我们运行该应用程序时,我们将看到以下输出:
2018-04-13 INFO 17868 --- [JDAsync-1] c.j.a.MovieService : Looking up Movie ID: 58611129-2dbc-4a81-a72f-77ddfc1b1b49
2018-04-13 08:00:09.518 INFO 17868 --- [JDAsync-2] c.j.a.MovieService : Looking up Movie ID: 2baf70d1-42bb-4437-b551-e5fed5a87abe
2018-04-13 08:00:12.254 INFO 17868 --- [JDAsync-1] c.j.a.MovieService : Looking up Movie ID: 4e236f34-b981-41c3-8c65-f8c9000b94e7
2018-04-13 08:00:13.565 INFO 17868 --- [main] c.j.a.ApplicationRunner : Elapsed time: 4056
2018-04-13 08:00:13.565 INFO 17868 --- [main] c.j.a.ApplicationRunner : --> MovieModel{title='My Neighbor Totoro', producer='Hayao Miyazaki'}
2018-04-13 08:00:13.565 INFO 17868 --- [main] c.j.a.ApplicationRunner : --> MovieModel{title='Castle in the Sky', producer='Isao Takahata'}
2018-04-13 08:00:13.566 INFO 17868 --- [main] c.j.a.ApplicationRunner : --> MovieModel{title='Only Yesterday', producer='Toshio Suzuki'}
如果仔细观察,会发现应用程序中只执行了两个线程,分别是JDAsync-1
和JDAsync-2
。
结论
在本课中,我们学习了如何在 Spring Boot 2 中使用 Spring 的异步功能。请在此处阅读更多与 Spring 相关的帖子。
下载源代码
下载 Spring Boot Async 示例项目