创建异步方法
本文描述了模拟访问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到了一个新的知识点。(也不算特别新吧!)