webclient 和webflux(参考(webflux)) 这一系列,我们可以开好多节课程来讲解
什么是webclient,在spring5中,出现了reactive 响应式编程思想(参考响应式编程文章),并且为网络编程提供相关响应式编程的支持,如提供webflux(参考另外一个教程),他是spring 提供的异步非阻塞的响应式的网络框架,可以充分利用多cpu并行处理一些功能,虽然不能提高单个请求的响应能力,但是总体可以提高多核的服务器性能,提高系统吞吐量和伸缩性,特别适合于i/o密集型服务,webflux对应的是老式的sevlet的阻塞式服务容器。
而webclient提供的基于响应式的非阻塞的web请求客户端,对应的是老式的restTemplate这类,相对于老式的restTemplate,他不阻塞代码、异步执行。
他的执行流程是先创建个webclient.create()实例,之后调用get(),post()等调用方式,uri()指定路径,retrieve()用来发起请求并获得响应,bodyToMono(String.class)
用来指定请求结果需要处理为String,并包装为Reactor的Mono对象,
简单实例如下:
WebClient webClient = WebClient.create();
Mono mono = webClient.get().uri("https://www.baidu.com").retrieve().bodyToMono(String.class);
mono.subscribe(System.out::println);
以下我们讲解具体的实例
以下加webclient的pom
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-webflux
org.projectreactor
reactor-spring
1.0.1.RELEASE
org.springframework.boot
spring-boot-starter-test
test
WebClient webClient = WebClient.create();
// 如果是调用特定服务的API,可以在初始化webclient 时使用,baseUrl
WebClient webClient = WebClient.create("https://api.github.com");
或者用构造者模式构造
WebClient webClient1 = WebClient.builder()
.baseUrl("https://api.github.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
.defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
.build();
Mono
.method(HttpMethod.GET)
.uri("http://www.baidu.com")
.cookie("token","xxxx")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.retrieve().bodyToMono(String.class);
@Test
public void testFormParam(){
MultiValueMap formData = new LinkedMultiValueMap();
formData.add("name1","value1");
formData.add("name2","value2");
Mono resp = WebClient.create().post()
.uri("http://www.w3school.com.cn/test/demo_form.asp")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(formData))
.retrieve().bodyToMono(String.class);
System.out.print("result:" + resp.block());
}
可以使用BodyInserters类提供的各种工厂方法来构造BodyInserter对象并将其传递给body方法。BodyInserters类包含从Object,Publisher,Resource,FormData,MultipartData等创建BodyInserter的方法。
static class Book {
String name;
String title;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
@Test
public void testPostJson(){
Book book = new Book();
book.setName("name");
book.setTitle("this is title");
Mono resp = WebClient.create().post()
.uri("http://localhost:8080/demo/json")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(Mono.just(book),Book.class)
.retrieve().bodyToMono(String.class);
LOGGER.info("result:{}",resp.block());
}
@Test
public void testUploadFile(){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
HttpEntity entity = new HttpEntity<>(new ClassPathResource("parallel.png"), headers);
MultiValueMap parts = new LinkedMultiValueMap<>();
parts.add("file", entity);
Mono resp = WebClient.create().post()
.uri("http://localhost:8080/upload")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(parts))
.retrieve().bodyToMono(String.class);
LOGGER.info("result:{}",resp.block());
@Test
public void testFormParam4xx(){
WebClient webClient = WebClient.builder()
.baseUrl("https://api.github.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
.defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
.build();
WebClient.ResponseSpec responseSpec = webClient.method(HttpMethod.GET)
.uri("/user/repos?sort={sortField}&direction={sortDirection}",
"updated", "desc")
.retrieve();
Mono mono = responseSpec
.onStatus(e -> e.is4xxClientError(),resp -> {
log.error("error:{},msg:{}",resp.statusCode().value(),resp.statusCode().getReasonPhrase());
return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp.statusCode().getReasonPhrase()));
})
.bodyToMono(String.class)
.doOnError(WebClientResponseException.class, err -> {
log.info("ERROR status:{},msg:{}",err.getRawStatusCode(),err.getResponseBodyAsString());
throw new RuntimeException(err.getMessage());
})
.onErrorReturn("fallback");
String result = mono.block();
System.out.print(result);
}
可以使用onStatus根据status code进行异常适配
可以使用doOnError异常适配
可以使用onErrorReturn返回默认值
在应用中使用WebClient时也许你要访问的URL都来自同一个应用,只是对应不同的URL地址,这个时候可以把公用的部分抽出来定义为baseUrl,然后在进行WebClient请求的时候只指定相对于baseUrl的URL部分即可。这样的好处是你的baseUrl需要变更的时候可以只要修改一处即可。下面的代码在创建WebClient时定义了baseUrl为http://localhost:8081
,在发起Get请求时指定了URL为/user/1
,而实际上访问的URL是http://localhost:8081/user/1
。
String baseUrl = "http://localhost:8081";
WebClient webClient = WebClient.create(baseUrl);
Mono mono = webClient.get().uri("user/{id}", 1).retrieve().bodyToMono(User.class);
retrieve方法是直接获取响应body,但是,如果需要响应的头信息、Cookie等,可以使用exchange方法,该方法可以访问整个ClientResponse。由于响应的得到是异步的,所以都可以调用 block 方法来阻塞当前程序,等待获得响应的结果。
以下代码是把获取写入的cookie
String baseUrl = "http://localhost:8081";
WebClient webClient = WebClient.create(baseUrl);
MultiValueMap map = new LinkedMultiValueMap<>();
map.add("username", "u123");
map.add("password", "p123");
Mono mono = webClient.post().uri("login").syncBody(map).exchange();
ClientResponse response = mono.block();
if (response.statusCode() == HttpStatus.OK) {
Mono resultMono = response.bodyToMono(Result.class);
resultMono.subscribe(result -> {
if (result.isSuccess()) {
ResponseCookie sidCookie = response.cookies().getFirst("sid");
Flux userFlux = webClient.get().uri("users").cookie(sidCookie.getName(), sidCookie.getValue()).retrieve().bodyToFlux(User.class);
userFlux.subscribe(System.out::println);
}
});
}
WebClient也提供了Filter,对应于org.springframework.web.reactive.function.client.ExchangeFilterFunction
接口,其接口方法定义如下。Mono
增加基本身份验证:
WebClient webClient = WebClient.builder()
.baseUrl(GITHUB_API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE)
.filter(ExchangeFilterFunctions
.basicAuthentication(username, token))
.build();
过滤response:
@Test
void filter() {
Map uriVariables = new HashMap<>();
uriVariables.put("p1", "var1");
uriVariables.put("p2", 1);
WebClient webClient = WebClient.builder().baseUrl("http://www.ifeng.com")
.filter(logResposneStatus())
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
.build();
Mono resp1 = webClient
.method(HttpMethod.GET)
.uri("/")
.cookie("token","xxxx")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.retrieve().bodyToMono(String.class);
String re= resp1.block();
System.out.print("result:" +re);
}
private ExchangeFilterFunction logResposneStatus() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
log.info("Response Status {}", clientResponse.statusCode());
return Mono.just(clientResponse);
});
}
使用过滤器记录日志:
WebClient webClient = WebClient.builder()
.baseUrl(GITHUB_API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE)
.filter(ExchangeFilterFunctions
.basicAuthentication(username, token))
.filter(logRequest())
.build();
private ExchangeFilterFunction logRequest() {
return (clientRequest, next) -> {
logger.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers()
.forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value)));
return next.exchange(clientRequest);
};
}
注意:
将response body 转换为对象/集合
bodyToMono
如果返回结果是一个Object,WebClient将接收到响应后把JSON字符串转换为对应的对象。
bodyToFlux
如果响应的结果是一个集合,则不能继续使用bodyToMono(),应该改用bodyToFlux(),然后依次处理每一个元素。
二、webclient原理
webclient的整个实现技术基本采用reactor-netty实现,下章我讲述相关细节
https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-client