在 Spring 5 之前,如果我们想要调用其他系统提供的 HTTP 服务,通常可以使用 Spring 提供的 RestTemplate 来访问,不过由于 RestTemplate 是 Spring 3 中引入的同步阻塞式 HTTP 客户端,因此存在一定性能瓶颈。根据 Spring 官方文档介绍,在将来的版本中它可能会被弃用。
作为替代,Spring 官方已在 Spring 5 中引入了 WebClient 作为非阻塞式 Reactive HTTP 客户端。下面通过样例演示如何使用 WebClient。
(1)与 RestTemplate 相比,WebClient 有如下优势:
(2)RestTemplate 不适合在非阻塞应用程序中使用,因此 Spring WebFlux 应用程序应始终使用 WebClient。在大多数高并发场景中,WebClient 也应该是 Spring MVC 中的首选,并且用于编写一系列远程,相互依赖的调用。
编辑 pom.xml 文件,添加 Spring WebFlux 依赖,从而可以使用 WebClient。
org.springframework.boot
spring-boot-starter-webflux
从 WebClient 的源码中可以看出,WebClient 接口提供了三个不同的静态方法来创建 WebClient 实例:
(1)下面利用 create() 方法创建一个 WebClient 对象,并利用该对象请求一个网络接口,最后将结果以字符串的形式打印出来。
注意:由于利用 create() 创建的 WebClient 对象没有设定 baseURL,所以这里的 uri() 方法相当于重写 baseURL。
WebClient webClient = WebClient.create();
Mono mono = webClient
.get() // GET 请求
.uri("http://jsonplaceholder.typicode.com/posts/1") // 请求路径
.retrieve() // 获取响应体
.bodyToMono(String.class); //响应数据类型转换
System.out.println(mono.block());
2,利用 create(String baseUrl) 创建
(1)下面利用 create(String baseUrl) 方法创建一个 WebClient 对象,并利用该对象请求一个网络接口,最后将结果以字符串的形式打印出来。
注意:由于利用 create(String baseUrl) 创建的 WebClient 对象时已经设定了 baseURL,所以 uri() 方法会将返回的结果和 baseUrl 进行拼接组成最终需要远程请求的资源 URL。
WebClient webClient = WebClient.create("http://jsonplaceholder.typicode.com");
Mono mono = webClient
.get() // GET 请求
.uri("/posts/1") // 请求路径
.retrieve() // 获取响应体
.bodyToMono(String.class); //响应数据类型转换
System.out.println(mono.block());
(1)下面使用 builder() 返回一个 WebClient.Builder,然后再调用 build 就可以返回 WebClient 对象。并利用该对象请求一个网络接口,最后将结果以字符串的形式打印出来。
注意:由于返回的不是 WebClient 类型而是 WebClient.Builder,我们可以通过返回的 WebClient.Builder 设置一些配置参数(例如:baseUrl、header、cookie 等),然后再调用 build 就可以返回 WebClient 对象了
WebClient webClient = WebClient.builder()
.baseUrl("http://jsonplaceholder.typicode.com")
.defaultHeader(HttpHeaders.USER_AGENT,"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)")
.defaultCookie("ACCESS_TOKEN", "test_token")
.build();
Mono mono = webClient
.get() // GET 请求
.uri("/posts/1") // 请求路径
.retrieve() // 获取响应体
.bodyToMono(String.class); //响应数据类型转换
System.out.println(mono.block());
下面代码将响应结果映射为一个 String 字符串,并打印出来。
@RestController
public class HelloController {
// 创建 WebClient 对象
private WebClient webClient = WebClient.builder()
.baseUrl("http://jsonplaceholder.typicode.com")
.build();
@GetMapping("/test")
public void test() {
Mono mono = webClient
.get() // GET 请求
.uri("/posts/1") // 请求路径
.retrieve() // 获取响应体
.bodyToMono(String.class); //响应数据类型转换
System.out.println(mono.block());
return;
}
}
(1)当响应的结果是 JSON 时,也可以直接指定为一个 Object,WebClient 将接收到响应后把 JSON 字符串转换为对应的对象。
@RestController
public class HelloController {
// 创建 WebClient 对象
private WebClient webClient = WebClient.builder()
.baseUrl("http://jsonplaceholder.typicode.com")
.build();
@GetMapping("/test")
public void test() {
Mono mono = webClient
.get() // GET 请求
.uri("/posts/1") // 请求路径
.retrieve() // 获取响应体
.bodyToMono(PostBean.class); //响应数据类型转换
System.out.println(mono.block());
return;
}
}
(2)其中定义的实体 Bean 代码如下:
@Getter
@Setter
@ToString
public class PostBean {
private int userId;
private int id;
private String title;
private String body;
}
(1)假设接口返回的是一个 json 数组,内容如下:
(2)我们也可以将其转成对应的 Bean 集合:
@RestController
public class HelloController {
// 创建 WebClient 对象
private WebClient webClient = WebClient.builder()
.baseUrl("http://jsonplaceholder.typicode.com")
.build();
@GetMapping("/test")
public void test() {
Flux flux = webClient
.get() // GET 请求
.uri("/posts") // 请求路径
.retrieve() // 获取响应体
.bodyToFlux(PostBean.class); //响应数据类型转换
List posts = flux.collectList().block();
System.out.println("结果数:" + posts.size());
return;
}
}
下面 3 种方式的结果都是一样的。
(1)使用占位符的形式传递参数:
Mono mono = webClient
.get() // GET 请求
.uri("/{1}/{2}", "posts", "1") // 请求路径
.retrieve() // 获取响应体
.bodyToMono(String.class); //响应数据类型转换
(2)另一种使用占位符的形式:
String type = "posts";
int id = 1;
Mono mono = webClient
.get() // GET 请求
.uri("/{type}/{id}", type, id) // 请求路径
.retrieve() // 获取响应体
.bodyToMono(String.class); //响应数据类型转换
System.out.println(mono.block());
(3)我们也可以使用 map 装载参数:
Map map = new HashMap<>();
map.put("type", "posts");
map.put("id", 1);
Mono mono = webClient
.get() // GET 请求
.uri("/{type}/{id}", map) // 请求路径
.retrieve() // 获取响应体
.bodyToMono(String.class); //响应数据类型转换
(1)前面的样例我们都是人为地使用 block 方法来阻塞当前程序。其实 WebClient 是异步的,也就是说等待响应的同时不会阻塞正在执行的线程。只有在响应结果准备就绪时,才会发起通知。
@RestController
public class HelloController {
// 创建 WebClient 对象
private WebClient webClient = WebClient.builder()
.baseUrl("http://jsonplaceholder.typicode.com")
.build();
@GetMapping("/test")
public void test() {
System.out.println("--- begin ---");
Mono mono = webClient
.get() // GET 请求
.uri("/posts/1") // 请求路径
.retrieve() // 获取响应体
.bodyToMono(String.class); //响应数据类型转换
// 订阅(异步处理结果)
mono.subscribe(result -> {
System.out.println(result);
});
System.out.println("--- end ---");
return;
}
}
1,方法介绍
(1)前面我们都是使用 retrieve() 方法直接获取到了响应的内容,如果我们想获取到响应的头信息、Cookie 等,可以在通过 WebClient 请求时把调用 retrieve() 改为调用 exchange()。
(2)通过 exchange() 方法可以访问到代表响应结果的对象,通过该对象我们可以获取响应码、contentType、contentLength、响应消息体等。
下面代码请求一个网络接口,并将响应体、响应头、响应码打印出来。其中响应体的类型设置为 String。
@RestController
public class HelloController {
// 创建 WebClient 对象
private WebClient webClient = WebClient.builder()
.baseUrl("http://jsonplaceholder.typicode.com")
.build();
@GetMapping("/test")
public void test() {
Mono mono = webClient
.get() // GET 请求
.uri("/posts/1") // 请求路径
.exchange();
// 获取完整的响应对象
ClientResponse response = mono.block();
HttpStatus statusCode = response.statusCode(); // 获取响应码
int statusCodeValue = response.rawStatusCode(); // 获取响应码值
Headers headers = response.headers(); // 获取响应头
// 获取响应体
Mono resultMono = response.bodyToMono(String.class);
String body = resultMono.block();
// 输出结果
System.out.println("statusCode:" + statusCode);
System.out.println("statusCodeValue:" + statusCodeValue);
System.out.println("headers:" + headers.asHttpHeaders());
System.out.println("body:" + body);
return;
}
}
(1)下面代码使用 post 方式发送一个 json 格式的字符串,并将结果打印出来(以字符串的形式)。
@RestController
public class HelloController {
// 创建 WebClient 对象
private WebClient webClient = WebClient.builder()
.baseUrl("http://jsonplaceholder.typicode.com")
.build();
@GetMapping("/test")
public void test() {
// 需要提交的 json 字符串
String jsonStr = "{\"userId\": 222,\"title\": \"abc\",\"body\": \"航歌\"}";
// 发送请求
Mono mono = webClient
.post() // POST 请求
.uri("/posts") // 请求路径
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(jsonStr))
.retrieve() // 获取响应体
.bodyToMono(String.class); //响应数据类型转换
// 输出结果
System.out.println(mono.block());
return;
}
}
2,发送一个 JSON 格式数据(使用 Java Bean)
(1)下面代码使用 post 方式发送一个 Bean 对象,并将结果打印出来(以字符串的形式)。结果同上面是一样的:
@RestController
public class HelloController {
// 创建 WebClient 对象
private WebClient webClient = WebClient.builder()
.baseUrl("http://jsonplaceholder.typicode.com")
.build();
@GetMapping("/test")
public void test() {
// 要发送的数据对象
PostBean postBean = new PostBean();
postBean.setUserId(222);
postBean.setTitle("abc");
postBean.setBody("航歌");
// 发送请求
Mono mono = webClient
.post() // POST 请求
.uri("/posts") // 请求路径
.contentType(MediaType.APPLICATION_JSON_UTF8)
.syncBody(postBean)
.retrieve() // 获取响应体
.bodyToMono(String.class); //响应数据类型转换
// 输出结果
System.out.println(mono.block());
return;
}
}
(2)上面发送的 Bean 对象实际上会转成如下格式的 JSON 数据提交:
(1)下面样例使用 POST 方式发送 multipart/form-data 格式的数据:
@RestController
public class HelloController {
// 创建 WebClient 对象
private WebClient webClient = WebClient.builder()
.baseUrl("http://jsonplaceholder.typicode.com")
.build();
@GetMapping("/test")
public void test() {
//提交参数设置
MultiValueMap map = new LinkedMultiValueMap<>();
map.add("title", "abc");
map.add("body", "航歌");
// 发送请求
Mono mono = webClient
.post() // POST 请求
.uri("/posts") // 请求路径
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(map))
.retrieve() // 获取响应体
.bodyToMono(String.class); //响应数据类型转换
// 输出结果
System.out.println(mono.block());
return;
}
}
(2)上面代码最终会通过如下这种 form 表单方式提交数据:
4,将结果转成自定义对象
上面样例我们都是将响应结果以 String 形式接收,其实 WebClient 还可以自动将响应结果转成自定的对象或则数组。具体可以参考前面写的文章:
(1)如果 url 地址上面需要传递一些参数,可以使用占位符的方式:
String url = "http://jsonplaceholder.typicode.com/{1}/{2}";
String url = "http://jsonplaceholder.typicode.com/{type}/{id}";
(2)具体的用法可以参考前面写的文章:
(1)前面的样例我们都是人为地使用 block 方法来阻塞当前程序。其实 WebClient 是异步的,也就是说等待响应的同时不会阻塞正在执行的线程。只有在响应结果准备就绪时,才会发起通知。
@RestController
public class HelloController {
// 创建 WebClient 对象
private WebClient webClient = WebClient.builder()
.baseUrl("http://jsonplaceholder.typicode.com")
.build();
@GetMapping("/test")
public void test() {
System.out.println("--- begin ---");
// 需要提交的 json 字符串
String jsonStr = "{\"userId\": 222,\"title\": \"abc\",\"body\": \"航歌\"}";
Mono mono = webClient
.post() // POST 请求
.uri("/posts") // 请求路径
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(jsonStr))
.retrieve() // 获取响应体
.bodyToMono(String.class); //响应数据类型转换
// 订阅(异步处理结果)
mono.subscribe(result -> {
System.out.println(result);
});
System.out.println("--- end ---");
return;
}
}
附:使用 exchange() 方法获取完整的响应内容
1,方法介绍
(1)前面我们都是使用 retrieve() 方法直接获取到了响应的内容,如果我们想获取到响应的头信息、Cookie 等,可以在通过 WebClient 请求时把调用 retrieve() 改为调用 exchange()。
(2)通过 exchange() 方法可以访问到代表响应结果的对象,通过该对象我们可以获取响应码、contentType、contentLength、响应消息体等。
2,使用样例
下面代码请求一个网络接口,并将响应体、响应头、响应码打印出来。其中响应体的类型设置为 String。
@RestController
public class HelloController {
// 创建 WebClient 对象
private WebClient webClient = WebClient.builder()
.baseUrl("http://jsonplaceholder.typicode.com")
.build();
@GetMapping("/test")
public void test() {
// 需要提交的 json 字符串
String jsonStr = "{\"userId\": 222,\"title\": \"abc\",\"body\": \"航歌\"}";
// 发送请求
Mono mono = webClient
.post() // POST 请求
.uri("/posts") // 请求路径
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(jsonStr))
.exchange();
// 获取完整的响应对象
ClientResponse response = mono.block();
HttpStatus statusCode = response.statusCode(); // 获取响应码
int statusCodeValue = response.rawStatusCode(); // 获取响应码值
Headers headers = response.headers(); // 获取响应头
// 获取响应体
Mono resultMono = response.bodyToMono(String.class);
String body = resultMono.block();
// 输出结果
System.out.println("statusCode:" + statusCode);
System.out.println("statusCodeValue:" + statusCodeValue);
System.out.println("headers:" + headers.asHttpHeaders());
System.out.println("body:" + body);
return;
}
}