WebClient
Spring 有两个web客户端的实现,一个是RestTemplate另一个是spring5的响应代替WebClient。
WebClient是一个以Reactive方式处理HTTP请求的非阻塞客户端。
RestTemplate是阻塞客户端
- 它基于thread-pre-requset模型。
- 这意味着线程将阻塞,直到 Web 客户端收到响应。阻塞代码的问题是由于每个线程消耗了一些内存和 CPU 周期。当出现慢速请求的时候,等待结果的线程会堆积起来,将导致创建更多的线程、消耗更多的资源。频繁切换CPU资源也会降低性能。
WebClient是异步、非阻塞的方案。
WebClient将为每个事件创建类似于“任务”的东西。在幕后,Reactive 框架会将这些“任务”排队并仅在适当的响应可用时执行它们。
WebClient是Spring WebFlux库的一部分。因此,我们还可以使用具有反应类型(Mono和Flux的功能性、流畅的 API 作为声明性组合来编写客户端代码。
底层支持的库
- Reactor Netty - ReactorClientHttpConnector
- Jetty ReactiveStream HttpClient - JettyHttpConnector
关于IDEA开启 Reactive Streams DEBUG
演示代码
基础用法
创建WebClient
WebClient.create();
WebClient.builder();
请求方法
WebClient webClient = WebClient.create();
webClient.get();
webClient.post();
webClient.delete();
webClient.put();
webClient.patch();
webClient.options();
获取结果
// exchangeToMono方法
// exchangeToFlux方法
Mono<Object> entityMono = webClient.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Object.class);
} else if (response.statusCode().is4xxClientError()) {
return response.bodyToMono(Object.class);
} else {
return Mono.error((Supplier<? extends Throwable>) response.createException());
}
});
Flux<Object> entityFlux = webClient.get()
.uri("/persons")
.accept(MediaType.APPLICATION_JSON)
.exchangeToFlux(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToFlux(Object.class);
} else if (response.statusCode().is4xxClientError()) {
return response.bodyToMono(Object.class).flux();
} else {
return Flux.error((Supplier<? extends Throwable>) response.createException());
}
});
异常处理
.doOnError(t -> log.error("Error: ", t)) // 异常回调
.doFinally(s -> log.info("Finally ")) // Finally 回调
请求体
// 直接使用bodyValue方法
RequestHeadersSpec<?> headersSpec = bodySpec.bodyValue("data");
// 或者Publisher
RequestHeadersSpec<?> headersSpec = bodySpec.body(Mono.just(new Foo("name")), Foo.class);
代码演示
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class AppApplication {
public static void main(String[] args) {
ApplicationContext applicationContext = SpringApplication.run(AppApplication.class, args);
}
// 声明默认的WebClient
@Bean
public WebClient register() {
return WebClient.create();
}
}
@RestController
@RequestMapping("/")
@Slf4j
public class AppController {
@Autowired
private WebClient webClient;
@PostMapping("/list")
public Flux<UserVo> list() {
List<UserVo> userList = new ArrayList<>();
userList.add(new UserVo("1", "张三"));
userList.add(new UserVo("2", "王五"));
return Flux.fromIterable(userList);
}
@GetMapping("/{id}")
public Mono<UserVo> info(@PathVariable(value = "id") String id) {
return Mono.just(new UserVo(id, "某某"));
}
@GetMapping("/ip/info")
public Mono<String> ip() {
String url = "https://myip.ipip.net/";
Mono<String> body = webClient.get()
.uri(url)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) // 响应的格式json
.acceptCharset(StandardCharsets.UTF_8) // 编码集 utf-8
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(String.class);
} else {
return response.createException().flatMap(Mono::error);
}
})
.doOnError(t -> log.error("Error: ", t)) // 异常回调
.doFinally(s -> log.error("Finally ")) // Finally 回调
.subscribeOn(Schedulers.single());
return body;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserVo {
private String uid;
private String name;
}
接口Mock测试
- 添加@WebFluxTest注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(WebFluxTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(WebFluxTypeExcludeFilter.class)
@AutoConfigureCache
@AutoConfigureJson
@AutoConfigureWebFlux // 自动装配WebFlux
@AutoConfigureWebTestClient // 自动装配WebTestClient
@ImportAutoConfiguration
public @interface WebFluxTest {
- 代码
@Slf4j
@ExtendWith(SpringExtension.class)
@ActiveProfiles("dev")
@WebFluxTest
public class AppApplicationMockTest {
@Autowired
private WebTestClient webTestClient;
@MockBean
private AppController appController;
@Test
@Disabled
public void get() {
UserVo user = new UserVo("1", "张三");
Mockito.when(appController.info("1")).thenReturn(Mono.just(user));
EntityExchangeResult<UserVo> result = webTestClient.get()
.uri("/1")
.exchange()
.expectStatus().isOk()
.expectBody(UserVo.class)
.returnResult();
log.info("{}", result);
}
@Test
@Disabled
public void list() {
List<UserVo> userList = new ArrayList<>();
userList.add(new UserVo("1", "张三"));
userList.add(new UserVo("2", "王武"));
Mockito.when(appController.list()).thenReturn(Flux.fromIterable(userList));
EntityExchangeResult<List<UserVo>> result = webTestClient.post()
.uri("/list")
.exchange()
.expectStatus().isOk()
.expectBodyList(UserVo.class)
.returnResult();
log.info("{}", result);
}
}
- Console
[main]
> GET /1
> WebTestClient-Request-Id: [1]
No content
< 200 OK OK
< Content-Type: [application/json]
< Content-Length: [27]
{"uid":"1","name":"张三"}
[main]
> POST /list
> WebTestClient-Request-Id: [1]
No content
< 200 OK OK
< Content-Type: [application/json]
[{"uid":"1","name":"张三"},{"uid":"2","name":"王武"}]
接口测试
- 添加@AutoConfigureWebTestClient 启用WebTestClient。
@Slf4j
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = AppApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("dev")
@AutoConfigureWebTestClient
public class AppApplicationTest {
@Autowired
private WebTestClient webTestClient;
@Test
@Disabled
public void get() {
EntityExchangeResult<UserVo> result = webTestClient.get()
.uri("/1")
.exchange()
.expectStatus().isOk()
.expectBody(UserVo.class)
.returnResult();
log.info("{}", result);
}
@Test
@Disabled
public void ip() {
EntityExchangeResult<String> result = webTestClient.get()
.uri("/ip/info")
.exchange()
.expectStatus().isOk()
.expectBody(String.class)
.returnResult();
log.info("{}", result);
}
@Test
@Disabled
public void list() {
EntityExchangeResult<List<UserVo>> result = webTestClient.post()
.uri("/list")
.exchange()
.expectStatus().isOk()
.expectBodyList(UserVo.class)
.returnResult();
log.info("{}", result);
}
}
- Console
[main]
> GET /1
> WebTestClient-Request-Id: [1]
No content
< 200 OK OK
< Content-Type: [application/json]
< Content-Length: [27]
{"uid":"1","name":"某某"}
[reactor-http-nio-3] Finally
[main]
> GET /ip/info
> WebTestClient-Request-Id: [1]
No content
< 200 OK OK
< Content-Type: [text/plain;charset=UTF-8]
< Content-Length: [67]
当前 IP:000.000.00.0 来自于:中国 浙江 杭州 电信
[main]
> POST /list
> WebTestClient-Request-Id: [1]
No content
< 200 OK OK
< Content-Type: [application/json]
[{"uid":"1","name":"张三"},{"uid":"2","name":"王五"}]