在本文中,我们将讨论Spring Boot框架中的WebClient类。你可以在这里访问本文的源码。
这个类相当于RestTemplate类,但是它可以处理异步请求。
如果你想要使用这个类,你需要将这些依赖放到你的Maven文件中。
org.springframework.boot
spring-boot-starter-webflux
这也是为什么你需要使用WebFlux,它从Springframework 5.0开始可用。注意这个版本的Spring需要Java8或者更高的版本。
使用基于Project Reactor的WebFlux,你可以编写响应式应用。这种应用程序的特点是请求没有阻塞,函数式编程得到了广泛的应用。
如果你想要理解这篇文章,你需要对Reactor和Mono类有一定的认知。尽管如此,如果你使用过Java中的Streams,或者你可以认为一个Mono对象就像一个能够返回一个value或者error的Stream。
但是我并不打算深入研究这些问题,因为他们超出了本文的范畴。当使用WebClient类时,你可以并行的执行多个调用,因此如果每个请求在2秒内得到结果,而你执行了5个调用,那么你可以在2秒而不是10秒内获得所有的结果。
并发请求
在这个示例项目中,我编写了一个服务器和一个客户端,服务器运行在8081端口,客户端监听8080端口。
这个服务端执行的代码:
@SpringBootApplication
public class WebServerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(WebServerApplication.class).
properties(Collections.singletonMap("server.port", "8081")).run(args);
}
}
如果你向http://localhost:8080/client/XXX发起一个请求,方法testGet将会执行。
@RestController()
@RequestMapping("/client")
@Slf4j
public class ClientController {
final String urlServer="http://localhost:8081";
@GetMapping("/{param}")
public Mono>> testGet(@PathVariable String param) {
final long dateStarted = System.currentTimeMillis();
WebClient webClient = WebClient.create(urlServer+"/server/");
Mono respuesta = webClient.get().uri("?queryParam={name}", param).exchange();
Mono respuesta1 = webClient.get().uri("?queryParam={name}","SPEED".equals(param)?"SPEED":"STOP").exchange();
Mono>> f1 = Mono.zip(respuesta, respuesta1)
.map(t -> {
if (!t.getT1().statusCode().is2xxSuccessful()) {
return ResponseEntity.status(t.getT1().statusCode()).body(t.getT1().bodyToMono(String.class));
}
if (!t.getT2().statusCode().is2xxSuccessful()) {
return ResponseEntity.status(t.getT2().statusCode()).body(t.getT2().bodyToMono(String.class));
}
return ResponseEntity.ok().body(Mono.just(
"All OK. Seconds elapsed: " + (((double) (System.currentTimeMillis() - dateStarted) / 1000))));
});
return f1;
}
这是一个简单的controller,对URL http://localhost:8081执行两个请求。在第一个请求中,传递的参数是param变量中接收到的函数。在第二种情况下,如果param变量与不是SPEED,则发送STOP。
服务器在接收到参数STOP是将会等待5秒,然后再响应。
从我们创建WebClient类的实例起,我就指定了请求的URL。
WebClient webClient = WebClient.create(urlServer+"/server/");
然后,它执行GET类型的对传递参数QueryParam的服务器的调用。最后,当执行函数交换时,它会收到一个Mono对象,其中包含一个ClientResponse类,它相当于RestTemplate类的ResponseEntity对象。类ClientResponse将包含由服务器发送的http代码,主体和头部。
Mono respuesta = webClient.get().uri("?queryParam={name}", param).exchange();
等一下。。。
我是不是说了它会执行?emm,我撒谎了。事实上只是我想做的被声明了。在响应式编程中,直到有人不再订阅请求所有操作才会被执行,因此尚未向服务器发出请求。
在下一行中,定义了对服务器的第二个请求。
Mono respuesta1 = webClient.get()
.uri("?queryParam={name}","SPEED".equals(param)?"SPEED":"STOP").exchange();
最后,使用zip函数创建一个Mono对象,它将是前两个对象的结果。
使用map函数,如果两个请求已经响应了2XX代码,则返回一个HTTP代码等于OK的ResponseEntity对象,否则将返回服务器的代码和响应。
作为响应式WebClient,这两个请求是同时实现的,因此,如果你执行这段代码curl http://localhost:8080/client/STOP
,你将能够看到这一点,你只需要5s多一点就能拿得到响应,即使调用时间的总和大于10秒。
All OK. Seconds elapsed: 5.092
A POST request
在testURLs函数中,有一个使用POST的调用范例。
这这函数在消息体中接收一个map,然后将其放置在请求头中。此外,这个map会被放在对服务器的请求的主体中发送。
@PostMapping("")
public Mono testURLs(@RequestBody Map body,
@RequestParam(required = false) String url) {
log.debug("Client: in testURLs");
WebClient.Builder builder = WebClient.builder().baseUrl(urlServer).
defaultHeader(HttpHeaders.CONTENT_TYPE,MediaType.APPLICATION_JSON_VALUE);
if (body!=null && body.size()>0 )
{
for (Map.Entry map : body.entrySet() )
{
builder.defaultHeader(map.getKey(), map.getValue());
}
}
WebClient webClient = builder.build();
String urlFinal;
if (url==null)
urlFinal="/server/post";
else
urlFinal="/server/"+url;
Mono respuesta1 = webClient.post().uri(urlFinal).body(BodyInserters.fromObject(body)).exchange()
.flatMap( x ->
{
if ( ! x.statusCode().is2xxSuccessful())
return Mono.just("LLamada a "+urlServer+urlFinal+" Error 4xx: "+x.statusCode()+"\n");
return x.bodyToMono(String.class);
});
return respuesta1;
}
要插入消息主体,要用到辅助类BodyInserters。如果消息在Mono对对象中,此代码可以使用:
BodyInserters.fromPublisher(Mono.just(MONO_OBJECT),String.class);
在执行flatMap,ClientResponse对象的输出将被捕获并且一个带有将要返回的字符串的Moni对象会被返回。
发起下列调用:
curl -s -XPOST http://localhost:8080/client -H 'Content-Type: application/json' -d'{"aa": "bbd"}'
会获得下列输出:
the server said: {aa=bbd}
Headers: content-length:12
Headers: aa:bbd
Headers: accept-encoding:gzip
Headers: Content-Type:application/json
Headers: accept:*/*
Headers: user-agent:ReactorNetty/0.9.0.M3
Headers: host:localhost:8081
这个输出是由服务器的函数postExample产生的:
@PostMapping("post")
public ResponseEntity postExample(@RequestBody Map body,ServerHttpRequest request) {
String s="the server said: "+body+"\n";
for (Entry> map : request.getHeaders().entrySet())
{
s+="Headers: "+map.getKey()+ ":"+map.getValue().get(0)+"\n";
}
return ResponseEntity.ok().body(s);
}
注意当使用与javax.servlet非完全兼容的WebFlux类库时,我们必须接收一个ServerHttpRequest对象来收集所有的原始headers。在非响应式应用程序中等效的对象是HttpServletRequest对象。
如果你执行这个语句:
curl -s -XPOST http://localhost:8080/client?url=aa -H 'Content-Type: application/json' -d'{"aa": "bbd"}'
客户端会尝试调用http://localhost:8081/server/aa,这回导致一个error,并且会接收到下面的内容。
http://localhost:8081/server/aa Called. Error 4xx: 404 NOT_FOUND