WebClient 和 RestClient 
简介 
1 Fourteen years ago, when RestTemplate was introduced in Spring Framework 3.0, we quickly discovered that exposing every capability of HTTP in a template-like class resulted in too many overloaded methods. 
十四年前,当在 Spring Framework 3.0 中引入时 RestTemplate,我们很快发现,在类似模板的类中公开HTTP的所有功能会导致太多的重载方法。
所以在 Spring Framework 中实现了如下两种客户端,用来执行 Http 请求:
WebClient:异步客户端 
RestClient:同步客户端 
 
WebClient 实现 
引入 WebFlux 包:
1 2 3 4 dependencies {'org.springframework.boot:spring-boot-starter-webflux' 'org.springframework.boot:spring-boot-starter-web' 
编写请求结果类:
1 2 public  record  JokeResponse (String id, String joke, Integer status)  {
编写请求类:
1 2 3 4 5 6 7 8 import  org.springframework.web.service.annotation.GetExchange;public  interface  JokeClient  {@GetExchange("/") random () ;
编写主类和 Bean:
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 import  org.springframework.boot.SpringApplication;import  org.springframework.boot.autoconfigure.SpringBootApplication;import  org.springframework.context.annotation.Bean;import  org.springframework.web.reactive.function.client.WebClient;import  org.springframework.web.reactive.function.client.support.WebClientAdapter;import  org.springframework.web.service.invoker.HttpServiceProxyFactory;@SpringBootApplication public  class  Application  {public  static  void  main (String[] args)  {@Bean jokeClient (WebClient.Builder builder)  {WebClient  client  =  builder"https://icanhazdadjoke.com/" )"Accept" , "application/json" )HttpServiceProxyFactory  factory  =  HttpServiceProxyFactory.builderFor(WebClientAdapter.create(client)).build();return  factory.createClient(JokeClient.class);
编写测试接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import  jakarta.annotation.Resource;import  org.springframework.web.bind.annotation.GetMapping;import  org.springframework.web.bind.annotation.RequestMapping;import  org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping public  class  TestController  {@Resource @GetMapping("/") public  String test ()  {JokeResponse  response  =  jokeClient.random();return  response.joke();
更换支持库(可选):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Bean public  JettyResourceFactory resourceFactory ()  {return  new  JettyResourceFactory ();@Bean public  WebClient webClient ()  {HttpClient  httpClient  =  new  HttpClient ();ClientHttpConnector  connector  = new  JettyClientHttpConnector (httpClient, resourceFactory()); return  WebClient.builder().clientConnector(connector).build(); 
注:每种库的日志需要单独调节,支持库的清单参阅官方文档。
 
配置代理(可选):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import  org.springframework.context.annotation.Bean;import  org.springframework.http.client.reactive.ReactorClientHttpConnector;import  org.springframework.web.reactive.function.client.WebClient;import  org.springframework.web.reactive.function.client.support.WebClientAdapter;import  org.springframework.web.service.invoker.HttpServiceProxyFactory;import  reactor.netty.http.client.HttpClient;import  reactor.netty.transport.ProxyProvider;@Bean jokeClient (WebClient.Builder builder)  {HttpClient  httpClient  =  HttpClient.create()"localhost" )7890 ));WebClient  client  =  buildernew  ReactorClientHttpConnector (httpClient))"https://icanhazdadjoke.com/" )"Accept" , "application/json" )HttpServiceProxyFactory  factory  =  HttpServiceProxyFactory.builderFor(WebClientAdapter.create(client)).build();return  factory.createClient(JokeClient.class);
RestClient 实现 
引入 Web 包:
1 2 3 dependencies {
编写请求结果类:
1 2 public  record  JokeResponse (String id, String joke, Integer status)  {
编写请求类:
1 2 3 4 5 6 7 8 import  org.springframework.web.service.annotation.GetExchange;public  interface  JokeClient  {@GetExchange("/") random () ;
编写主类和 Bean:
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 import  org.springframework.boot.SpringApplication;import  org.springframework.boot.autoconfigure.SpringBootApplication;import  org.springframework.context.annotation.Bean;import  org.springframework.web.client.RestClient;import  org.springframework.web.client.support.RestClientAdapter;import  org.springframework.web.service.invoker.HttpServiceProxyFactory;@SpringBootApplication public  class  Application  {public  static  void  main (String[] args)  {@Bean jokeClient (RestClient.Builder builder)  {RestClient  client  =  builder"https://icanhazdadjoke.com/" )"Accept" , "application/json" )HttpServiceProxyFactory  factory  =  HttpServiceProxyFactory.builderFor(RestClientAdapter.create(client)).build();return  factory.createClient(JokeClient.class);
编写测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import  jakarta.annotation.Resource;import  org.springframework.web.bind.annotation.GetMapping;import  org.springframework.web.bind.annotation.RequestMapping;import  org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping public  class  TestController  {@Resource @GetMapping("/") public  String test ()  {JokeResponse  response  =  jokeClient.random();return  response.joke();
更换支持库(可选):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import  org.springframework.boot.web.client.RestClientCustomizer;import  org.springframework.context.annotation.Bean;import  org.springframework.context.annotation.Configuration;import  org.springframework.http.client.JettyClientHttpRequestFactory;import  org.springframework.web.client.RestClient;@Configuration public  class  CustomRestClientConf  {@Bean public  RestClient restClient (RestClient.Builder builder)  {return  builder.build();@Bean public  RestClientCustomizer restClientCustomizer ()  {JettyClientHttpRequestFactory  requestFactory  =  new  JettyClientHttpRequestFactory ();return  (restClientBuilder) -> restClientBuilder"http://localhost:8080/" );
注:每种库的日志需要单独调节,支持库的清单参阅官方文档。
 
设置代理(可选):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import  org.springframework.context.annotation.Bean;import  org.springframework.http.client.SimpleClientHttpRequestFactory;import  org.springframework.web.client.RestClient;import  org.springframework.web.client.support.RestClientAdapter;import  org.springframework.web.service.invoker.HttpServiceProxyFactory;import  java.net.InetSocketAddress;import  java.net.Proxy;@Bean jokeClient (RestClient.Builder builder)  {Proxy  proxy  =  new  Proxy (Proxy.Type.HTTP, new  InetSocketAddress ("localhost" , 7890 ));SimpleClientHttpRequestFactory  simpleClientHttpRequestFactory  =  new  SimpleClientHttpRequestFactory ();RestClient  client  =  builder"https://icanhazdadjoke.com/" )"Accept" , "application/json" )HttpServiceProxyFactory  factory  =  HttpServiceProxyFactory.builderFor(RestClientAdapter.create(client)).build();return  factory.createClient(JokeClient.class);
常见问题 
获取请求状态码 
可以使用如下样例获取请求结果对象,然后读取状态码等信息
1 2 3 4 5 6 7 8 9 import  org.springframework.http.ResponseEntity;import  org.springframework.web.service.annotation.GetExchange;public  interface  JokeClient  {@GetExchange("/") random () ;
Mock 测试 
编写如下测试程序即可:
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 import  com.fasterxml.jackson.core.JsonProcessingException;import  com.fasterxml.jackson.databind.ObjectMapper;import  org.junit.jupiter.api.Test;import  org.springframework.beans.factory.annotation.Autowired;import  org.springframework.boot.test.autoconfigure.web.client.RestClientTest;import  org.springframework.http.MediaType;import  org.springframework.test.web.client.MockRestServiceServer;import  static  org.assertj.core.api.Assertions.assertThat;import  static  org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;import  static  org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;@RestClientTest(JokeClient.class) public  class  JokeTest  {@Autowired @Autowired @Autowired @Test public  void  shouldReturnAllPosts ()  throws  JsonProcessingException {JokeResponse  jokeResponse  =  new  JokeResponse ("1" , "demo" , 1 );this .server"https://icanhazdadjoke.com/" ))JokeResponse  result  =  jokeClient.random();"demo" );
拦截器 
可以使用拦截器的方式改变请求中的内容样例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import  org.slf4j.Logger;import  org.slf4j.LoggerFactory;import  org.springframework.http.HttpRequest;import  org.springframework.http.client.ClientHttpRequestExecution;import  org.springframework.http.client.ClientHttpRequestInterceptor;import  org.springframework.http.client.ClientHttpResponse;import  org.springframework.stereotype.Component;import  java.io.IOException;@Component public  class  TokenInterceptor  implements  ClientHttpRequestInterceptor  {private  static  final  Logger  log  =  LoggerFactory.getLogger(TokenInterceptor.class);@Override public  ClientHttpResponse intercept (HttpRequest request, byte [] body, ClientHttpRequestExecution execution)  throws  IOException {"Intercepting request: "  + request.getURI());"x-request-id" , "12345" );return  execution.execute(request, body);
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 import  org.springframework.http.client.ClientHttpRequestInterceptor;import  org.springframework.stereotype.Component;import  org.springframework.web.client.RestClient;@Component public  class  JokeClient  {private  final  RestClient restClient;public  JokeClient (RestClient.Builder builder, ClientHttpRequestInterceptor tokenInterceptor)  {this .restClient = builder"https://icanhazdadjoke.com" )"Accept" , "application/json" )public  JokeResponse random ()  {return  restClient.get()"/" )
处理不同的返回状态码和对象 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import  org.springframework.http.HttpStatus;import  org.springframework.web.reactive.function.client.ClientResponse;import  org.springframework.web.reactive.function.client.WebClient;import  reactor.core.publisher.Mono;public  class  AsyncRequestService  {private  final  WebClient webClient;public  AsyncRequestService (WebClient.Builder webClientBuilder)  {this .webClient = webClientBuilder.baseUrl("http://xxx.xxx.xxx" ).build();public  Mono<ClientResponse> asyncFetchDataWithStatus ()  {return  webClient.get()"/xxx" )if  (response.statusCode().equals(HttpStatus.OK) || response.statusCode().equals(HttpStatus.ACCEPTED)) {return  Mono.just(response);return  Mono.error(new  RuntimeException ("Failed to fetch data" ));
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 import  org.springframework.http.HttpStatus;import  org.springframework.http.ResponseEntity;import  org.springframework.web.bind.annotation.GetMapping;import  org.springframework.web.bind.annotation.RestController;import  reactor.core.publisher.Mono;@RestController public  class  AsyncRequestController  {private  final  AsyncRequestService asyncRequestService;public  AsyncRequestController (AsyncRequestService asyncRequestService)  {this .asyncRequestService = asyncRequestService;@GetMapping("/fetch-data") public  Mono<ResponseEntity<String>> fetchData ()  {return  asyncRequestService.asyncFetchDataWithStatus()if  (response.statusCode().equals(HttpStatus.OK)) {return  response.bodyToMono(String.class)else  if  (response.statusCode().equals(HttpStatus.ACCEPTED)) {return  Mono.just(ResponseEntity.status(HttpStatus.ACCEPTED).body("Processing..." ));else  {return  Mono.just(ResponseEntity.status(response.statusCode()).body("Unhandled status code" ));return  Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error occurred" ));
日志记录 
注:使用 SpringBoot Actuator 就可以拿到请求日志,但是问题在于不是很方便读取。之后可以测下 Micrometer 会不会集成。
 
参考代码 
之后在 RestClient 构建的时候配置拦截器就即可。
参考资料 
WebClient 文档 
WebClient 单元测试样例项目 
RestClient 文档 
Spring Security 6.4 中 OAuth2 的 RestClient 支持 
"Spring Boot REST Client Logging Made Easy