目錄
常見的HttpClient
Spring 提供的HttpClient
RestTemplate
Spring 提供的模板類 XXXTemplate
RestTemplate的使用
RestTemplate的使用技巧
RestTemplate的問題
RestClient
?RestClinet的基本使用
RestClient的自動配置
RestClient 序列化對象
異常處理 onStatus
更精細的控制 exchange
HTTP消息轉換 - Jackson Json 視圖
RestClient真正使用的HTTP庫
非異步調用
WebClient
簡單使用
請求攔截器
深度配置WebClient
HTTP接口
配置和使用
常見的HttpClient
我們項目中通常要調用內部或者外部的HTTP接口,這時候就需要用到Http Client, 以前我們經常用第三方的組件比如:
- Apache HttpClient
- OkHttp
- Spring Cloud提供的Feign Http客戶端
但是對于一般的項目,我們直接使用Spring 提供的HttpClient客戶端就可以滿足大部分的場景了。
所以嚴格上說,本文介紹的不是Spring Boot的特性,而是Spring提供的特性。
Spring 提供的HttpClient
Spring框架提供了幾種調用REST API的方式:
- RestTemplate:調用API的同步客戶端
- RestClient:異步客戶端
- WebClient:異步并且是響應式的客戶端
- Http Interface:基于動態代理的聲明式接口
說明:
本文我們會用一個開源的HTTP REST API用來測試:
https://jsonplaceholder.typicode.com/users
為此,我們定義了Java類接收json響應
@Builder @Data public class TypiUser {private Integer id;private String name;private String username;private String email;private String phone;private String website;private TypiAddress address; }@Builder @Data public class TypiAddress {private String street;private String suite;private String city;private String zipcode; }
Tips:
使用builder構造對象時,報錯:
java: cannot find symbolsymbol: method builder()location: class lab.rest.TypiUser
后來打開target/classes 看編譯后的類才發現TypiUser沒有編譯正確,所以執行 `mvn clean build` 后可以解決報錯問題。
發現另一個地方設置也問題,應該不要勾選Processor Path
RestTemplate
Spring 提供的模板類 XXXTemplate
熟悉Spring框架的開發都知道,Spring提供了各種XXXTemplate,使用模板方法模式封裝了復雜的底層操作,簡化了我們對外部組件的操作和使用。
常見的有:
- JdbcTemplate:封裝了JDBC的底層操作,簡化了數據庫操作
- JmsTemplate:封裝了對JMS API的操作,簡化了收發消息的操作
- ElasticsearchRestTemplate:是Spring Data ES模塊的一部分,簡化了對ES的操作
- RedisTemplate:是Spring Data Redis模塊的一部分,簡化了對Redis的操作
- HibernateTemplate:簡化了對Hibernate的CRUD操作
- ……
擴展:模板方法模式
這些XXXTemplate用到的是模版方法模式
- 在在抽象父類中定義了算法的骨架
- 然后子類中實現某些步驟(抽象方法)
RestTemplate的使用
而本章節,我們要介紹的是RestTemplate,它是Spring用于進行HTTP 發送請求和接收響應的客戶端工具。使得我們調用REST API變得非常的簡單。
使用RestTemplate發送post請求
@Test
public void basicTest(){RestTemplate restTemplate = new RestTemplate();String url = "https://jsonplaceholder.typicode.com/users";TypiUser user = TypiUser.builder().name("Joe").username("joe").email("joe@gmail.com").phone("123456789").website("joe.com").address(TypiAddress.builder().street("street").suite("suite").city("city").zipcode("zipcode").build()).build();ResponseEntity<TypiUser> response = restTemplate.postForEntity(url, user, TypiUser.class);TypiUser body = response.getBody();System.out.printf("用戶創建成功,id:%s, name:%s%n", body.getId(), body.getName());
}
可以看到使用RestTemplate 發送RestTemplate非常簡單。
我們在Web開發中通常會通過配置類來配置RestTemplate實例
@Configuration
public class RestTemplateConfig {@Beanpublic RestTemplate restTemplate(RestTemplateBuilder builder) {return builder.setConnectTimeout(Duration.ofSeconds(10)) //設置連接超時時間.setReadTimeout(Duration.ofSeconds(10)) // 設置讀取超時時間.build();}
}
然后注入RestTemplate直接使用
@Service
public class TypiUserService {private final RestTemplate restTemplate;public TypiUserService(RestTemplate restTemplate) {this.restTemplate = restTemplate;}public TypiUser getUser(Integer id) {return restTemplate.getForObject("https://jsonplaceholder.typicode.com/users/" + id,TypiUser.class);}
}
RestTemplate的使用技巧
一般場景,我們可以直接用getXXX方法發起get請求,通過postForEntity發起post請求,通過put()方法發起put請求,通過delete發起delete請求。
但某些時候我們想更精準的控制client的時候,就需要用到更原始的方法exchange(),它允許我們指定HTTP方法,處理請求頭和請求體。
public TypiUser getUserByExchange(String id) {String url = "https://jsonplaceholder.typicode.com/users/" + id;ResponseEntity<TypiUser> response = restTemplate.exchange(url, HttpMethod.GET, null, TypiUser.class);TypiUser user = response.getBody();System.out.println(user);System.out.println(response.getStatusCode());System.out.println(response.getHeaders());return user;
}
異常處理
接口如果處理有問題,RestTemplate 也會拋出一些異常,尤其是在遇到錯誤響應時。常見的異常包括:
- HttpClientErrorException:用于 4xx 錯誤。
- HttpServerErrorException:用于 5xx 錯誤。
- ResourceAccessException:網絡或連接問題時拋出。
我們需要捕獲并處理這些異常。
一種方式是我們直接在service中捕獲
public TypiUser getUser(Integer id) {try{return restTemplate.getForObject("https://jsonplaceholder.typicode.com/users/" + id,TypiUser.class);}catch (HttpClientErrorException e){throw new RuntimeException("客戶端異常", e);}catch (HttpServerErrorException e){throw new RuntimeException("服務器端異常", e);}catch (RestClientException e) {throw new RuntimeException("Rest Client異常", e);}
}
但是一般沒人這么干,因為如果我們有100個方法調用rest client,那不是得寫100遍。所以我們一般會統一的處理異常,使用Spring提供的@ControllerAdvice 和 @ExceptionHandler全局捕獲并處理異常。
@ControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(HttpClientErrorException.class)public ResponseEntity<ApiErrorResponse> handleHttpClientError(HttpClientErrorException e) {ApiErrorResponse errorResponse = new ApiErrorResponse(e.getStatusCode().value(),"Client Error",e.getResponseBodyAsString());return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(e.getStatusCode().value()));}// 略
}
不僅僅是RestTemplate,其它的Client也需要這種全局的異常處理。
RestTemplate的問題
RestTemplate簡單,但是也存在一些問題。
最重要的一點是它是同步的,它的每個請求都會阻塞直到收到響應。因為每個請求都占用一個線程,所以當大量請求同時發起時,導致系統的線程數會很快被耗盡,所以他的并發性能比較低。
RestClient
Spring引入了更現代化的HTTP客戶端 RestClient.
它本身也是同步的,不支持異步。但是它支持流式的調用方式,相比RestTemplate提供一個方法發起請求,流式調用更容易使用和控制。
?RestClinet的基本使用
@Service
public class TypiRestClientService {private final RestClient.Builder builder;private RestClient restClient;public TypiRestClientService(RestClient.Builder builder) {this.builder = builder;}// 使用 @PostConstruct 注解在 Spring 完成構造器注入后再進行初始化@PostConstructpublic void init() {// 使用 builder 創建 RestClient 實例,進行初始化this.restClient = this.builder.baseUrl("https://jsonplaceholder.typicode.com").build();}public TypiUser getUser(Integer id) {return restClient.get().uri("/users/" + id).retrieve().body(TypiUser.class);}
}
RestClient的自動配置
我們都沒有顯示的配置RestClient.Builder,它是怎么自動注入的呢?
還是那一套,SpringBoot的自動配置。
在autoconfiguration包下面
可以看到,自動配置了RestClient和RestTemplate
這里自動配置了Builder Bean
@ConditionalOnClass(RestClient.class) 這個注解表示,RestClientAutoConfiguration 只會在類路徑中存在 RestClient 類時才會被加載
@Conditional(NotReactiveWebApplicationCondition.class)如果是反應式應用,RestClientAutoConfiguration 就不會被加載。
@AutoConfiguration(after = { HttpClientAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,SslAutoConfiguration.class })
@ConditionalOnClass(RestClient.class)
@Conditional(NotReactiveWebApplicationCondition.class)
public class RestClientAutoConfiguration {@Bean@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)@ConditionalOnMissingBeanRestClient.Builder restClientBuilder(RestClientBuilderConfigurer restClientBuilderConfigurer) {return restClientBuilderConfigurer.configure(RestClient.builder());}
}
直接注入RestClient行不行呢?
@Service
public class TypiRestClientService {private final RestClient restClient;public TypiRestClientService(RestClient restClient) {this.restClient = restClient;}
當然不行,因為自動配置類中并沒有配置RestClient的Bean.
啟動報錯如下:
Action:
Consider defining a bean of type 'org.springframework.web.client.RestClient' in your configuration.
RestClient 序列化對象
requestbody和responseBody會自動轉對象
public TypiUser saveUser() {TypiUser user = TypiUser.builder().name("Joe").username("joe").email("joe@gmail.com").address(TypiAddress.builder().city("Beijing").build()).build();TypiUser result = restClient.post().uri("/users").body(user).retrieve().body(TypiUser.class);return result;}
retrieve方法之前的body()是request body, 后面的body()是 response body.
異常處理 onStatus
它提供了很優雅的異常處理
TypiUser result = restClient.post().uri("/users").body(user).retrieve().onStatus(HttpStatusCode::is4xxClientError, ((request, response) -> {throw new RuntimeException("status code: " + response.getStatusCode());})).body(TypiUser.class);
也是拋出異常,在全局類里面捕獲并處理異常。
更精細的控制 exchange
如果想要更精準的控制請求和響應,我們可以利用更底層的方法來發起請求。
TypiUser result = restClient.post().uri("/users").body(user).exchange((request, response) -> {if(response.getStatusCode().is2xxSuccessful()){return response.bodyTo(TypiUser.class);} else if(response.getStatusCode().is4xxClientError()){throw new RuntimeException("客戶端錯誤,status code: " + response.getStatusCode());} else {throw new RuntimeException("其它錯誤,status code: " + response.getStatusCode());}});
雖然可以更精細操作,但是操作得都是底層的request和response對象,也變得更麻煩了。世界上沒有絕對完美的事情。
HTTP消息轉換 - Jackson Json 視圖
發送請求時,我們不想把整個對象的所有字段都序列化成json后發送,而是序列化部分字段,可以做到的嗎?
當然可以,最笨的方法是直接新建一個新的對象,只包含部分屬性,然后用BeanUtils.copy復制屬性。
因為我們用的是Jackson,所以可以更優雅
使用Jackson的json視圖
MappingJacksonValue value = new MappingJacksonValue(user);
value.setSerializationView(TypiUser.TypiUserView.class);
TypiUser result = restClient.post().uri("/users").body(value).retrieve().onStatus(HttpStatusCode::is4xxClientError, ((request, response) -> {throw new RuntimeException("status code: " + response.getStatusCode());})).body(TypiUser.class);return result;
第一次聽到這個技術,挺有意思。
RestClient真正使用的HTTP庫
RestClient是對外提供的客戶端,要執行HTTP請求,它底層還是得依賴其它HTTP庫。
很像Slf4j, 它是個門面,底層是需要用log4j等日志框架的。
在 Spring 中,RestClient 通過適配不同的 HTTP 庫來執行 HTTP 請求。這些庫的適配是通過實現 ClientRequestFactory 接口來實現的。
具體實現有:
- JdkClientHttpRequestFactory:使用的是?Java的?HttpClient
- HttpComponentsClientHttpRequestFactory 使用的是Apache HTTP Components HttpClient
- JettyClientHttpRequestFactory:使用Jetty的?HttpClient
- ReactorNettyClientRequestFactory:使用Reactor Netty的?HttpClient
- SimpleClientHttpRequestFactory:簡單的實現
我們什么也沒有配置,使用默認配置,看看是什么工廠類:
可以看到,使用的是JDK的HTTP Client。
非異步調用
RestClient也是同步HTTP客戶端。但是對比RestTemplate,API是流式的更容易使用。如果沒有異步的場景需求,我們可以使用RestClient。
WebClient
RestTemplate和RestClient都是同步的,WebClient是非阻塞 響應式的HTTP 客戶端。
它是Spring5引入的,用來替代RestTemplate。
簡單使用
WebClient是Spring WebFlux模塊的功能,所以我們需要先引入依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency>
WebClient也是SpringBoot自動配置的,我們直接注入使用即可
@Service
public class TypiWebClientService {private final WebClient webClient;public TypiWebClientService(WebClient.Builder builder) {this.webClient = builder.baseUrl("https://jsonplaceholder.typicode.com").build();}
}
自動配置類
配置條件是classpath下找到了WebClient就自動配置WebClient.Builder這個Bean。所以我們注入的是WebClient.Builder這個Bean 而不是WebClient。
這塊配置配置和RestClient一樣!
@AutoConfiguration(after = { CodecsAutoConfiguration.class, ClientHttpConnectorAutoConfiguration.class })
@ConditionalOnClass(WebClient.class)
public class WebClientAutoConfiguration {@Bean@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)@ConditionalOnMissingBeanpublic WebClient.Builder webClientBuilder(ObjectProvider<WebClientCustomizer> customizerProvider) {WebClient.Builder builder = WebClient.builder();customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder));return builder;}
發起異步調用
public TypiUser getUser(Integer id) {Mono<TypiUser> mono = webClient.get().uri("/users/{id}", id).retrieve().bodyToMono(TypiUser.class).doOnTerminate(() -> System.out.println("調用結束"));System.out.println("繼續調用");TypiUser user = mono.block(); // 阻塞等待System.out.println("獲取到用戶:" + user);return user;}
繼續調用
調用結束
獲取到用戶:TypiUser(id=1, name=Leanne Graham, username=Bret, email=Sincere@april.biz, phone=1-770-736-8031 x56442, website=hildegard.org, address=TypiAddress(street=Kulas Light, suite=Apt. 556, city=Gwenborough, zipcode=92998-3874))
可以看到,發起請求后,線程并沒有blocked住,而是繼續往下走,直到執行到block()方法時,線程才會阻塞。
以前看了這么多八股文,很難理解異步WebClient。其實只要寫個最簡單的例子,就非常容易理解了。Java八股害死人。
請求攔截器
我們可以攔截請求,做一些特殊的處理。
public TypiWebClientService(WebClient.Builder builder) {this.webClient = builder.baseUrl("https://jsonplaceholder.typicode.com").filter((request, next) -> {System.out.println("開始調用,路徑 " + request.url().toString());return next.exchange(request);}).build();}
這樣所有的請求,都會執行這個操作
繼續調用
開始調用,路徑 https://jsonplaceholder.typicode.com/users/1
調用結束
深度配置WebClient
在配置類中配置WebClient Bean
配置連接池和選擇底層使用的HTTP客戶端。
@Configuration
public class WebClientConfig {@Beanpublic WebClient webClient() {ConnectionProvider provider = ConnectionProvider.builder("custom").maxConnections(50) // 設置最大連接數.build();HttpClient httpClient = HttpClient.create(provider).responseTimeout(Duration.ofSeconds(5)) // 設置響應超時.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).baseUrl("https://jsonplaceholder.typicode.com").build();}
}
這里我們使用的HTTP Client是Netty的客戶端。
并設置了連接池的大小還有HTTP的響應時間。
這樣我們可以直接注入使用WebClient了
@Service
public class TypiWebClientService {private final WebClient webClient;public TypiWebClientService(WebClient webClient) {this.webClient = webClient;}
HTTP接口
Spring允許我們通過Java接口的方式調用HTTP服務。我們不需要寫代碼顯示調用HTTP,而是通過注解聲明即可。
底層是利用動態代理技術,簡化了遠程 HTTP 調用。
配置和使用
首先,我們定義一個接口,并用注解聲明
public interface TypiUserRestService {@GetExchange("/users/{id}")TypiUser getUser(@PathVariable Integer id);
}
然后,我們需要創建一個代理,底層還是需要其它HTTP庫的
當然我們也可以選用其它的庫比如WebClient等
@Configuration
public class TypiUserRestServiceConfig {@Beanpublic TypiUserRestService config(){RestClient restClient = RestClient.builder().baseUrl("https://jsonplaceholder.typicode.com").build();RestClientAdapter adapter = RestClientAdapter.create(restClient);HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();return factory.createClient(TypiUserRestService.class);}
}
接下來,我們就可以在我們的Controller中注入直接使用了。
@RestController
@RequestMapping("/typi")
public class TypiUserController {private final TypiUserRestService typiUserRestService;@GetMapping("/v4/user/{id}")public TypiUser getUser4(@PathVariable Integer id){return typiUserRestService.getUser(id);}
這種聲明式的調用,讓我們的代碼顯得非常簡潔!
很類似Spring Cloud Feign的調用方式。