创建异步方法

创建异步方法

创建异步方法

Creating Asynchronous Methods

通过完成此教程,你将创建到Github的异步请求。重点是在异步部分,这是编排服务的一个特性。

This guide walks you through the steps to create asynchronous queries to GitHub. The focus is on the asynchronous part, a feature often used when scaling services.

将要做什么

What you’ll build

你将建立一个查询服务,用于查询Github用户信息并通过Github的API获取数据。编排服务的一个方法是在后台运行一个任务,并使用Java的CompletableFuture接口来等待结果的返回。Java的CompletableFuture是普通Future的一个进化版本。新版本可以更容易的编排多个异步操作,合并为单个异步的计算。

You’ll build a lookup service that queries GitHub user information and retrieves data through GitHub’s API. One approach to scaling services is to run expensive jobs in the background and wait for the results using Java’s CompletableFuture interface. Java’s CompletableFuture is an evolution from the regular Future. It makes it easy to pipeline multiple asynchronous operations merging them into a single asynchronous computation.

创建一个Github用户的对象

Create a representation of a GitHub User

在创建查询服务之前,你需要为Github的API返回的数据定义一个对象。

Before you can create a GitHub lookup service, you need to define a representation for the data you’ll retrieve through GitHub’s API.

为了模型化用户对象,你需要创建一个资源类。提供一个普通的java类,其中包括属性,构造器,访问方法等:

To model the user representation, you create a resource representation class. Provide a plain old Java object with fields, constructors, and accessors:

src/main/java/hello/User.java

package hello;

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调用更简单并生成域对象。

Spring uses the Jackson JSON library to convert GitHub’s JSON response into a User object. The @JsonIgnoreProperties annotation signals Spring to ignore any attributes not listed in the class. This makes it easy to make REST calls and produce domain objects.

在此教程中,我们只获取nameblogURL为例。

In this guide, we are only grabbing the name and the blog URL for demonstration purposes.

创建一个Github查询服务

Create a GitHub lookup service

下一步你需要创建一个服务来请求Github获取用户信息。

Next you need to create a service that queries GitHub to find user information.

src/main/java/hello/GitHubLookupService.java

package hello;

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)的默认值。

The GitHubLookupService class uses Spring’s RestTemplate to invoke a remote REST point (api.github.com/users/), and then convert the answer into a User object. Spring Boot automatically provides a RestTemplateBuilder that customizes the defaults with any auto-configuration bits (i.e. MessageConverter).

此类使用了@Service注解,使其作为Spring的组件被扫描发现,并被添加进应用上下文

The class is marked with the @Service annotation, making it a candidate for Spring’s component scanning to detect it and add it to the application context.

findUser方法标记了Spring的@Async注解,表示其将运行在一个单独的线程上。方法的返回类型不是User而是CompletableFuture<User>,这是所有异步服务必需的。代码中使用了completedFuture方法来返回一个CompletableFuture实例,此实例为查询Github后已经完成的结果。

The findUser method is flagged with Spring’s @Async annotation, indicating it will run on a separate thread. The method’s return type is CompletableFuture<User> instead of User, a requirement for any asynchronous service. This code uses the completedFuture method to return a CompletableFuture instance which is already completed with result of the GitHub query.

创建一个本地的GitHubLookupService实例并不允许findUser方法异步运行。必须在一个@Configuration类中创建或者由@ComponentScan扫描。

Creating a local instance of the GitHubLookupService class does NOT allow the findUser method to run asynchronously. It must be created inside a @Configuration class or picked up by @ComponentScan.

请求Github的API的时间可能不同。为了在样例中展示,在服务中添加了一秒的延迟。

The timing for GitHub’s API can vary. To demonstrate the benefits later in this guide, an extra delay of one second has been added to this service.

使应用可运行

Make the application executable

你可以创建一个可执行的jar来运行此样例。Spring的@Async注解在web应用中可以起效,且不需要其他的web容器配置即可看见效果。

To run a sample, you can create an executable jar. Spring’s @Async annotation works with web apps, but you don’t need all the extra steps of setting up a web container to see its benefits.

src/main/java/hello/Application.java

package hello;

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 Application {

    public static void main(String[] args) {
        // close the application context to shut down the custom ExecutorService
        SpringApplication.run(Application.class, args).close();
    }

    @Bean
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("GithubLookup-");
        executor.initialize();
        return executor;
    }

}

@EnableAsync注解开启了Spring可以在后台线程池运行@Async方法的功能。此类也自定义了使用的Executor。在此例中,我们限制了并发线程为2以及队列的大小为500。还有其他的也可以自定义。默认使用的是SimpleAsyncTaskExecutor

The @EnableAsync annotation switches on Spring’s ability to run @Async methods in a background thread pool. This class also customizes the used Executor. In our case, we want to limit the number of concurrent threads to 2 and limit the size of the queue to 500. There are many more things you can tune. By default, a SimpleAsyncTaskExecutor is used.

GitHubLookupService中也注入了CommandLineRunner,并且调用3次来展示方法是异步执行的。

There is also a CommandLineRunner that injects the GitHubLookupService and calls that service 3 times to demonstrate the method is executed asynchronously.

src/main/java/hello/AppRunner.java

package hello;

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

Build an executable JAR

执行后可看见日志输出,显示了到Github的每个请求。使用allOf工厂方法,我们可以通过使用调用join方法来创建一个CompletableFutures数组,可以实现等待所有CompletableFutures的完成。

Logging output is displayed, showing each query to GitHub. With the help of allOf factory method we create an array of CompletableFutures on which by calling the join method makes it possible to wait for the completion of all of the CompletableFutures.

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=http://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=http://spring.io/projects]

注意前两次调用是在不同的线程上(GithubLookup-2, GithubLookup-1),第三个需要等待前两个线程直到可用。为了比较不用异步特性时的时间消耗,可以尝试去掉@Async注解并再次运行。可以发现总时间消耗增加了,因为每个请求至少消耗1秒。你也可以调整Executor来增加corePoolSize属性。

Note that the first two calls happen in separate threads (GithubLookup-2, GithubLookup-1) and the third one is parked until one of the two threads became available. To compare how long this takes without the asynchronous feature, try commenting out the @Async annotation and run the service again. The total elapsed time should increase noticeably because each query takes at least a second. You can also tune the Executor to increase the corePoolSize attribute for instance.

一般情况下,任务消耗的时间越长,越多的任务被同时调用,异步带来的优势就会越大。只需要操作CompletableFuture接口即可。此接口间接增加了一层因为你并不直接操作结果了。

Essentially, the longer the task takes and the more tasks are invoked simultaneously, the more benefit you will see with making things asynchronous. The trade off is handling the CompletableFuture interface. It adds a layer of indirection because you are no longer dealing directly with the results.

总结

Summary

恭喜!你已经完成了异步服务的开发,可以同时编排多个调用。

Congratulations! You’ve just developed an asynchronous service that lets you scale multiple calls at once.

翻译部分版权归YahaCode团队所有。仅供学习研究之用,任何组织或个人不得私自以此用于任何形式的商业目的

发表评论