基于HTTP的请求调用是一种常见的数据交互方式,Java也有很多类库可以实现HTTP请求调用,在大多数时候,它们是都同步调用。如果请求响应比较慢,甚至请求超时,程序就必须等到请求返回以后才能继续执行,在某些场合下,我并不需要等待请求的结果,或者我不关心请求是否执行成功,需要继续执行之后的逻辑,就需要通过异步处理。
在 Spring 3 时代,为了能更优雅地实现HTTP调用,引入了 RestTemplate
,其中提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率。
在 Spring 4 时代,为了能实现异步地HTTP调用,引入了AsyncRestTemplate
,使得编写异步代码和同步代码一样简单。
在认识 AsyncRestTemplate
之前,先回顾一下 RestTemplate
中的一个经典方法:
public
而在 AsyncRestTemplate
中,可以看到如下方法签名:
public
对比发现,只有返回值不同。这里我仅仅展示了其中的一个方法,其它的方法基本都遵循这样的规律,足以见得Spring在易用性上作出的努力。那为什么会出现这种似曾相识的感觉呢?是因为AsyncRestTemplate
底层是基于RestTemplate
+异步线程池 实现的。
ListenableFuture
是实现异步请求的关键,作为一个接口, 它继承自并发包下的另一个接口Future
,并且巧妙地利用回调来处理异步请求。
ListenableFuture
的两个方法签名如下,处理异步请求需要我们手动实现这两个接口(其中的一个)。
void addCallback(ListenableFutureCallback super T> callback);
void addCallback(SuccessCallback super T> successCallback, FailureCallback failureCallback);
与RestTemplate
相同,AsyncRestTemplate
中最重要的两个方法是exchange
和execute
。它们需要传入较多的参数,灵活度高,自由度大,自然掌握起来难度就大一些。大部分时候,调用getForEntity
、postForLocation
、headForHeaders
等方法足以应对。
Spring 提供了 AsyncRestTemplate
,但是没有提供默认的实现,不能直接注入,需要手动初始化:
@Bean
public AsyncRestTemplate asyncRestTemplate() {
return new AsyncRestTemplate();
}
在初始化逻辑中,可以指定线程池,指定错误处理器,设置超时时间等。
@Autowired
private AsyncRestTemplate asyncRestTemplate;
有很多种方式注入,最简单的就是使用
@Autowired
,当然这是不推荐的一种注入方式,在最新版的idea工具中,这样的代码会有警告提示,建议你使用构造器注入。为什么不建议@Autowired
,因为它使用的反射的机制,存在性能问题,当然还有其他因素考虑。
我使用其中的getForEntity
演示,这里和使用RestTemplate
一模一样。
asyncRestTemplate.getForEntity(url, String.class);
我模拟的url来自当前项目的另一个rest地址,
String.class
代表返回值类型,注意String.class
是一个泛指,因为任意一个HTTP请求的返回值都可以认为是字符串类型的。
在大多数情况下,你需要处理回调,查看请求是否成功(响应为200),对返回值进一步操作。但是这个过程不是必须的,比如我只想发个类似通知一样的信息到另一个系统,不在乎是否成功,不需要处理返回值。
public String demo1() {
String url = "http://localhost:8080/student/1";
log.info("Start");
ListenableFuture<ResponseEntity<String>> entity = asyncRestTemplate.getForEntity(url, String.class);
entity.addCallback(new SuccessCallback<ResponseEntity<String>>() {
@Override
public void onSuccess(ResponseEntity<String> result) {
log.info("A");
}
}, new FailureCallback() {
@Override
public void onFailure(Throwable ex) {
log.info("B");
}
});
log.info("C");
return "End";
}
这段代码记录了这个异步调用过程,我在其中关键节点打印了几个信息,在另个一方法中调用该方法。
public String call1() {
log.info(demo1());
return "success";
}
按照同步代码执行的规则,我们得到的结果应是这样的(假设请求成功):
Start
A
C
END
下面是我执行代码后,控制台信息片段:
23:14:53.751 INFO 7552 --- [nio-8080-exec-1] c.c.t.web.AsyncCallController : Start
23:14:53.761 INFO 7552 --- [nio-8080-exec-1] c.c.t.web.AsyncCallController : C
23:14:53.761 INFO 7552 --- [nio-8080-exec-1] c.c.t.web.AsyncCallController : End
23:14:53.815 INFO 7552 --- [cTaskExecutor-1] c.c.t.web.AsyncCallController : A
顺序当然和同步的代码不一致,还有一个关键的信息时间节点,从方法开始,到方法结束看起来“没有耗时”。而HTTP请求根本不可能这么快返回数据,最后一行的打印结果也证明了请求是通过另外一个线程执行的,并且耗时40+ms。
在业务代码中,http://localhost:8080/student/1
返回的是一个对象,而不是字符串类型,getForEntity
的第二个参数可以指定返回值类型,于是对代码改造一下。
public String demo2() {
String url = "http://localhost:8080/student/1";
log.info("Start");
ListenableFuture<ResponseEntity<Student>> entity = asyncRestTemplate.getForEntity(url, Student.class);
entity.addCallback(new SuccessCallback<ResponseEntity<Student>>() {
@Override
public void onSuccess(ResponseEntity<Student> result) {
log.info(result.getBody().getName());
log.info("A");
}
}, new FailureCallback() {
@Override
public void onFailure(Throwable ex) {
log.info("B");
}
});
log.info("C");
return "End";
}
这里直接使用定义好的领域模型来接收请求结果,在请求成功以后,输出对象的属性值,测试发现,控制台打印了正确结果:
23:31:47.389 INFO 7552 --- [nio-8080-exec-5] c.c.t.web.AsyncCallController : Start
23:31:47.389 INFO 7552 --- [nio-8080-exec-5] c.c.t.web.AsyncCallController : C
23:31:47.389 INFO 7552 --- [nio-8080-exec-5] c.c.t.web.AsyncCallController : End
23:31:47.400 INFO 7552 --- [cTaskExecutor-2] c.c.t.web.AsyncCallController : Tom
23:31:47.400 INFO 7552 --- [cTaskExecutor-2] c.c.t.web.AsyncCallController : A