SpringBoot GettingStarted-创建异步方法

创建异步方法

本文描述了模拟访问GitHubHTTP接口,重点 是其中的异步化部分,这是一种用来Scaling services(提高服务吞吐)的技术

1、场景介绍(Why)

  • demo需求说明:
    • 业务需求:利用GitHub的API,获取(假想的)用户信息。要求做到高吞吐量。
    • 技术需求:基于SpringBoot,利用CompletableFuture + Async 注解做到异步化
  • 生产场景说明:
    • Service A中封装了ServiceB 、C,而且对B、C的运行顺序没有要求,B、C可以并行化,比如电商场景中聚合价格、库存等信息后,一起返回给调用方。
    • 另一种:异步化地将不需要返回结果任务放在background执行(甚至连是否执行结果都不需要关心)

2、核心类介绍

CompletableFuture: 可翻译成“可编排Future”。这个类提供了十分丰富的API,让使用者能够方便地“pipeline”多个异步操作,并把操作的结果merge (合并)成一个异步操作。
It makes it easy to pipeline multiple asynchronous operations merging them into a single asynchronous computation.

3、代码实现 (How) & 代码解释(What)

3.1 创建一个POJO

这是一个平淡无奇的POJO,不过,注意@JsonIgnoreProperties 这个注解,它表明在serialization | deserialization 的时候会将这些属性忽略掉。

**
 * ====Below are remarks from official documentation ,just in order
 * to remind u of the importance of official stuff.====
 *
 * Annotation that can be used to either suppress serialization
 * of properties (during serialization), or ignore processing of
 * JSON properties read (during deserialization).
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {

    private String name;
    private String blog;
	// getter setter toString ...ignored!    
}

3.2 创建一个Service

@Service
public class MyGithubLookUpService {
    private static final Logger logger = LoggerFactory.getLogger(MyGithubLookUpService.class);

    private final RestTemplate restTemplate;

    // 你可能惊讶这里的 RestTemplateBuilder 怎么就自动注入了?下面这个RestTemplateBuilder 的官方解释!
    // 原来这个Builder会自动在需要的地方自动注入。
    // RestTemplateBuilder :  In a typical auto-configured Spring Boot application
    // this builder is available as a bean and can be injected whenever a RestTemplate is needed.
    
    public MyGithubLookUpService(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder.build();
    }

    // 开启@Async 的方法的返回值应该是void 或者Future,为了能编排 Future,推荐返回值类型
    // 使用 CompletableFuture ,or ListenableFuture
    @Async
    public CompletableFuture<User> findUser(String user) throws InterruptedException {
        logger.info("Looking for " + user);
        String url = String.format("https://api.github.com/users/%s", user);
        User restResult = restTemplate.getForObject(url, User.class);
        // to fake artificial delay
        TimeUnit.SECONDS.sleep(1);
        return CompletableFuture.completedFuture(restResult);
    }
}

这个类使用RestTemplate请求远程REST风格的接口。SB会自动注入需要注入RestTemplateBuilder的地方,默认会使用MessageConverter的配置。

findUser上有注解@Async,说明该方法会在单独的线程上运行,返回值类型是CompletableFuture<User>,而不是普通的User这是异步化服务所必须的findUser返回的CompletableFuture 里封装了查询的结果。

尤其要注意的是:
假如直接new一个MyGithubLookUpService对象,然后调用findUser,那findUser是不会异步化的。MyGithubLookUpService的实例必须在@Configuration,或者 ComponentScan 的作用范围内。(换句话说:必须在Spring容器内!这是个坑!)
另附官文:

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.

现在知道厉害了吧?

3.3 启动SB应用

@SpringBootApplication
@EnableAsync
/**
 *@EnableAsync  这个注解实际是个开关,决定了整个SpringApplication 能不能开启异步化(@async 能不能奏效)
 */
public class MyApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(MyApplication.class, args);
        // Close this application context, releasing all resources and locks that the implementation might hold.
        // This includes destroying all cached singleton beans.
        context.close();
    }

    // 配置 @Async注解所注明的异步方法执行时所需的线程池,假如没有这个线程池,@Async不会发挥作用!
    // 方法上的 Bean 注解: 生成一个 名为 方法名(taskExecutor),类型为 Executor 的Bean。
    // @Async 注解的方法都会从这个 线程池 中获取线程来执行任务
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("MyThread-->");
        executor.initialize();
        return executor;
    }
}

下面来简单说明下上面的代码:

SpringBootApplication注解相当于是对多个注解的另外包装:

  • @Configuration
  • @EnableAutoConfiguration
  • @ComponentScan
    再来看看官网的解释:

Indicates a configuration class that declares one or more @Bean methods and also triggers auto-configuration and component scanning. This is a convenience annotation that is equivalent to declaring @Configuration, @EnableAutoConfiguration and @ComponentScan.

再来看@EnableAsync
这个注解相当于@Async 注解的开关。一定要小心的是,要实现异步化,不是说加上这两个注解就完事了。而是要指定线程池, 比如这里的taskExecutor()

这个CommandLineRunner 着实有趣!这种写法,就避免了我们写一个测试类了(注意上面说的:直接new 一个MyGithubLookUpService是不能开启 异步化的 findUser方法的! )。

/**
 public interface CommandLineRunner
 Interface used to indicate that a bean should run when it is contained within a SpringApplication.
 Multiple CommandLineRunner beans can be defined within the same application context and can be ordered using the
 Ordered interface or @Order annotation.
 If you need access to ApplicationArguments instead of the raw String array consider using ApplicationRunner.

 CommandLineRunner : 很有意思的一个接口,只要Spring容器里有实现这个接口Bean,就会运行该bean的 run()方法!
 一个容器中可以有多个这样的Bean,同时能用 ordered 接口或者 @Order 注解来指定顺序!
 */
@Component
@Order(1)
public class MyAppRunner implements CommandLineRunner {
    private static final Logger logger = LoggerFactory.getLogger(MyAppRunner.class);

    private final MyGithubLookUpService myGithubLookUpService;

    public MyAppRunner(MyGithubLookUpService myGithubLookUpService) {
        this.myGithubLookUpService = myGithubLookUpService;
    }

    @Override
    public void run(String... args) throws Exception {
        long startTime = System.currentTimeMillis();

        // 开始多个异步任务
        CompletableFuture<User> page1 = myGithubLookUpService.findUser("PivotalSoftware");
        CompletableFuture<User> page2 = myGithubLookUpService.findUser("CloudFoundry");
        CompletableFuture<User> page3 = myGithubLookUpService.findUser("Spring-Projects");

        // 等待所有任务均已完成
        CompletableFuture.allOf(page1, page2, page3).join();

        // 获取执行结果,打印 每个任务的耗时
        logger.info(" elapsed time " + (System.currentTimeMillis() - startTime));
        logger.info("-->" + page1.get());
        logger.info("-->" + page2.get());
        logger.info("-->" + page3.get());

    }
}

此外,我们可以改变taskExecutor()线程池的参数,来查看不同条件下的异步查询的结果。然后,把@EnableAsync注解注释掉,看看同步条件下 的执行的耗时是不是显著减少了。

一般来说呢,多个任务并行化的时候,并行化程度越高,你就越能看到其中的差异。当然,这也是有代价的。使用CompletableFuture,提高了调试和编码的难度。

4、总结

没啥好总结的。恭喜大家,get到了一个新的知识点。(也不算特别新吧!)

猜你喜欢

转载自blog.csdn.net/qq_30118563/article/details/83685264