0%

SpringBoot-WebClient

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

1
2
WebClient.create();
WebClient.builder();

请求方法

1
2
3
4
5
6
7
WebClient webClient =   WebClient.create();
webClient.get();
webClient.post();
webClient.delete();
webClient.put();
webClient.patch();
webClient.options();

获取结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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());
}
});

异常处理

1
2
.doOnError(t -> log.error("Error: ", t)) // 异常回调
.doFinally(s -> log.info("Finally ")) // Finally 回调

请求体

1
2
3
4
5
// 直接使用bodyValue方法
RequestHeadersSpec<?> headersSpec = bodySpec.bodyValue("data");
// 或者Publisher
RequestHeadersSpec<?> headersSpec = bodySpec.body(Mono.just(new Foo("name")), Foo.class);

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@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注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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 {
  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
[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":"王五"}]


END