Spring Boot - 使用@Async注解创建异步方法

x33g5p2x  于2022-10-07 转载在 Spring  
字(8.3k)|赞(0)|评价(0)|浏览(459)

在这篇文章中,我们将使用Spring的@Async注解探索Spring或Spring Boot中的异步执行支持。
我们将用@Async注解一个bean的方法,使其在一个单独的线程中执行,也就是说,调用者将不会等待被调用方法的完成。

如果你已经在Spring或Spring Boot应用程序上工作,并且需要使用异步机制,那么下面这三个快速步骤将有助于设置。

第1步:启用异步支持

让我们先用Java配置启用异步处理--只需在配置类中添加@EnableAsync。 

@EnableAsync注解开启了Spring在后台线程池中运行@Async方法的能力。

第2步:为方法添加@Async注解

确保我们用@Async注解的方法需要是公共的,这样它就可以被代理。而自我调用不起作用,因为它绕过了代理,直接调用底层方法。

第三步:执行器(自定义默认)

让我们来定制ThreadPoolTaskExecutor。在我们的例子中,我们想把并发线程的数量限制在2个,把队列的大小限制在500个。你可以调整的东西还有很多。默认情况下,使用的是SimpleAsyncTaskExecutor,

@Bean("threadPoolTaskExecutor")
    public TaskExecutor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(20);
        executor.setMaxPoolSize(1000);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setThreadNamePrefix("Async-");
        return executor;
    }

就这样,这三个快速步骤帮助你使用Spring或Spring Boot创建异步服务。
让我们开发一个完整的例子来演示如何使用Spring或Spring Boot创建异步服务。

我们将建立什么?

我们将构建一个查询服务,通过GitHub的API查询GitHub用户信息并检索数据。扩展服务的一种方法是在后台运行昂贵的作业,并使用Java的CompletableFuture接口等待结果。Java的CompletableFuture是普通Future的进化版。它使多个异步操作的管道化变得很容易,将它们合并成一个单一的异步计算。

使用的工具和技术

  • Spring Boot - 2.0.6.RELEASE
  • JDK - 1.8或更高版本
  • Spring Framework - 5.0.9 RELEASE
  • Maven - 3.2以上
  • IDE - Eclipse或Spring Tool Suite (STS)

创建和导入Spring Boot项目

有很多方法可以创建Spring Boot应用程序。最简单的方法是使用http://start.spring.io/的Spring Initializr,它是一个在线Spring Boot应用程序生成器。

看上面的图,我们指定了以下细节。

  • Generate:Maven项目
  • Java版本:1.8(默认)。
  • Spring Boot:2.0.4
  • Group:net.javaguides.springboot
  • Artifact: springboot-async-example
  • Name: springboot-async-example
  • Description: Spring Boot的演示项目
  • Package Name:net.guides.springboot.springbootasyncexample
  • Packaging: jar (这是默认值)
  • Dependencies: Web

一旦,所有的细节被输入,点击Generate Project按钮将生成一个spring boot项目并下载。接下来,解压下载的压缩文件并将其导入你最喜欢的IDE。

项目目录结构

下面,图中显示了一个项目结构,供参考。 

pom.xml文件

<?xmlversion="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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>net.guides.springboot</groupId>
    <artifactId>springboot-async-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>springboot-async-example</name>
    <description>Demo project for Spring Boot</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
        <relativePath/>
        <!-- lookup parent from repository -->
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <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>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

创建一个GitHub用户的代表

让我们创建带有姓名和博客字段的GitHub用户模型类。

package net.guides.springboot.springbootasyncexample.model;

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响应转换为用户对象。@JsonIgnoreProperties注解提示Spring忽略类中未列出的任何属性。这使得我们很容易进行REST调用并产生领域对象。

在这篇文章中,我们只抓取名字和博客的URL作为示范。

创建一个GitHub查询服务

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

package net.guides.springboot.springbootasyncexample.service;

import java.util.concurrent.CompletableFuture;

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 net.guides.springboot.springbootasyncexample.model.User;

@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("threadPoolTaskExecutor")
    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(1000 L);
        return CompletableFuture.completedFuture(results);
    }
}

GitHubLookupService类使用Spring的RestTemplate来调用一个远程REST点(api.github.com/users/),然后将答案转换成一个User对象。Spring Boot自动提供一个RestTemplateBuilder,用任何自动配置位(即MessageConverter)定制默认值。

findUser方法被标记为Spring的@Async注解,表明它将在一个单独的线程上运行。该方法的返回类型是CompletableFuture而不是User,这是任何异步服务的要求。这段代码使用completedFuture方法来返回一个CompletableFuture实例,该实例已经完成了GitHub查询的结果。

使应用程序可执行

为了运行一个样本,你可以创建一个可执行的jar。让我们使用CommandLineRunner,它注入了GitHubLookupService并调用该服务4次,以证明该方法是异步执行。

package net.guides.springboot.springbootasyncexample;

import java.util.concurrent.CompletableFuture;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import net.guides.springboot.springbootasyncexample.model.User;
import net.guides.springboot.springbootasyncexample.service.GitHubLookupService;

@SpringBootApplication
@EnableAsync
public class SpringbootAsyncApplication implements CommandLineRunner {

    private static final Logger logger = LoggerFactory.getLogger(SpringbootAsyncApplication.class);

    @Autowired
    private GitHubLookupService gitHubLookupService;

    @Bean("threadPoolTaskExecutor")
    public TaskExecutor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(20);
        executor.setMaxPoolSize(1000);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setThreadNamePrefix("Async-");
        return executor;
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringbootAsyncApplication.class, args);
    }

    @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");
        CompletableFuture < User > page4 = gitHubLookupService.findUser("RameshMF");
        // Wait until they are all done
        CompletableFuture.allOf(page1, page2, page3, page4).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());
        logger.info("--> " + page4.get());
    }
}

@EnableAsync注解开启了Spring在后台线程池中运行@Async方法的能力。这个类还可以自定义使用的执行器。在我们的例子中,我们想把并发线程的数量限制在2个,把队列的大小限制在500个。还有很多东西你可以调教。默认情况下,使用的是SimpleAsyncTaskExecutor

运行应用程序

我们可以通过两种方式启动独立的Spring boot应用程序。

*我们使用的是maven,所以只需用./mvnw spring-boot:run来运行该应用程序。或者你可以用./mvnw clean package构建JAR文件。然后你就可以运行JAR文件了。

java -jar target/springboot-async-example.jar
  • 下图显示了如何从IDE运行spring boot应用程序--右击--运行为--SpringbootAsyncApplication.main()方法作为一个独立的Java类。 

输出

当我们运行该应用程序时,我们将看到以下输出。

相关文章