Table of contents
1 Java Spring 提供既 HTTP client APIs
微服務(又或者 monolith applications)可以透過 HTTP clients 以 HTTP REST calls 既形式連接其他服務,包括獲取、上載、更新、刪除資料。
以下係 Java Spring Boot 或者 Spring Cloud 項目可以用到既 HTTP client APIs:
- Java
HttpClient
- Spring
RestTemplate
- Spring Webflux
WebClient
- Spring 6.1
RestClient
(新)
- Spring Cloud OpenFeign
2 動手寫
2.1 Java HttpClient
以下係 Java 本身提供既 HttpClient
API。
構建 client object:
final HttpClient client = HttpClient.newBuilder()
.version(Version.HTTP_1_1)
.connectTimeout(Duration.ofSeconds(10))
.build();
發送 request:
1final String requestBody = "{ \"value\": \"123\" }";
2
3final HttpRequest request = HttpRequest.newBuilder()
4 .version(Version.HTTP_1_1)
5 .uri(new URI("http://localhost:8081/api/order/123?param1=value¶m2=value"))
6 .POST(BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
7 .header("Authorization", "Basic " + Base64.getEncoder().encodeToString(("username" + ":" + "password").getBytes()))
8 .header("Content-Type", "application/json")
9 .header("header1", "header1-value")
10 .header("header2", "header2-value")
11 .build();
12
13final HttpResponse<String> response = client.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8));
2xx
、3xx
、4xx
或 5xx
responses:
1final String responseBody = response.body();
2final int responseStatusCode = response.statusCode();
3final boolean isResponse2xx = responseStatusCode >= 200 && responseStatusCode < 300;
4final boolean isResponse3xx = responseStatusCode >= 300 && responseStatusCode < 400;
5final boolean isResponse4xx = responseStatusCode >= 400 && responseStatusCode < 500;
6final boolean isResponse5xx = responseStatusCode >= 500 && responseStatusCode < 600;
7final Map<String, List<String>> responseHeaders = response.headers().map();
2.2 Spring RestTemplate
以下係 Spring 一直以黎都有既 RestTemplate
API。自從 Spring 5,呢個 class 已經係喺 maintenance mode,Spring 建議我地轉用 WebClient
。
構建 client object:
1final RestTemplate client = new RestTemplateBuilder()
2 .setConnectTimeout(Duration.ofSeconds(10))
3 .basicAuthentication("username", "password")
4 .defaultHeader("header1", "header1-value")
5 .defaultHeader("header2", "header2-value")
6 .build();
7
8// 如果加入左 jackson-dataformat-xml
9// 令 RestTemplate 唔好喺 Accept request header 度加入 application/xml 或相關既 values
10client.getMessageConverters().removeIf(e -> e.getClass()==MappingJackson2XmlHttpMessageConverter.class);
發送 request:
1final String requestBody = "{ \"value\": \"123\" }";
2final HttpHeaders requestHeaders = new HttpHeaders();
3requestHeaders.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
4requestHeaders.set("header1", "header1-value");
5requestHeaders.set("header2", "header2-value");
6
7try {
8 final ResponseEntity<String> response = client.exchange(
9 "http://localhost:8081/api/order/{orderId}?param1={param1}¶m2={param2}",
10 HttpMethod.POST,
11 new HttpEntity<>(requestBody, requestHeaders),
12 String.class,
13 "123",
14 "param1-value",
15 "param2-value");
16} catch (RestClientResponseException e) {
17 // 4xx 或 5xx responses
18}
2xx
或 3xx
responses:
final String responseBody = response.getBody();
final int responseStatusCode = response.getStatusCode().value();
final boolean isResponse2xx = response.getStatusCode().is2xxSuccessful();
final boolean isResponse3xx = response.getStatusCode().is3xxRedirection();
final Map<String, List<String>> responseHeaders = response.getHeaders();
4xx
或 5xx
responses:
1} catch (RestClientResponseException e) {
2 final String responseBody = e.getResponseBodyAsString();
3 final int responseStatusCode = e.getStatusCode().value();
4 final boolean isResponse4xx = e.getStatusCode().is4xxClientError();
5 final boolean isResponse5xx = e.getStatusCode().is5xxServerError();
6 final Map<String, List<String>> responseHeaders = e.getResponseHeaders();
7}
注意:
- 某啲 classpath libraries(Maven dependencies)會影響到 Spring
RestTemplate
幫我地 send HTTP calls 既時候加入既 Accept
request header。
- 創建 Spring
RestTemplate
既時候,佢會因為喺 classpath 存在既 classes 而去推斷 Accept
request header。
- 例如 classpath 度有
jackson-dataformat-xml
既話,Spring RestTemplate
就會用左 Accept: text/plain, application/xml, text/xml, application/json, application/*+xml, application/*+json, */*
既 request header 去 send HTTP calls。
- 如果呢啲 libraries 係必需,咁我地可以透過刪除 Spring
RestTemplate
object 裡面既 message converters 去改變佢既 behavior。
2.3 Spring Webflux WebClient
以下係黎自 Spring Webflux 既 WebClient
API。佢支援 synchronous(blocking)以及 asynchronous(non-blocking)既 HTTP calls。
Spring Boot servlet WebMVC 既項目只要加入 Spring Webflux 既 library 就用得到呢個 client。
構建 client object:
1final HttpClient httpClient = HttpClient.newBuilder()
2 .version(Version.HTTP_1_1)
3 .connectTimeout(Duration.ofSeconds(10))
4 .build();
5
6final WebClient client = WebClient.builder()
7 .clientConnector(new JdkClientHttpConnector(httpClient))
8 .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
9 .defaultHeader("header1", "header1-value")
10 .defaultHeader("header2", "header2-value")
11 .build();
發送 request:
1final String requestBody = "{ \"value\": \"123\" }";
2
3try {
4 final ResponseEntity<String> response = client.post()
5 .uri("http://localhost:8081/api/order/{orderId}?param1={param1}¶m2={param2}", "123", "param1-value", "param2-value")
6 .headers(headers -> headers.setBasicAuth("username", "password"))
7 .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
8 .header("header1", "header1-value")
9 .header("header2", "header2-value")
10 .bodyValue(requestBody)
11 .retrieve()
12 .toEntity(String.class)
13 .block();
14} catch (WebClientResponseException e) {
15 // 4xx 或 5xx responses
16}
2xx
或 3xx
responses:
final String responseBody = response.getBody();
final int responseStatusCode = response.getStatusCode().value();
final boolean isResponse2xx = response.getStatusCode().is2xxSuccessful();
final boolean isResponse3xx = response.getStatusCode().is3xxRedirection();
final Map<String, List<String>> responseHeaders = response.getHeaders();
4xx
或 5xx
responses:
1} catch (WebClientResponseException e) {
2 final String responseBody = e.getResponseBodyAsString();
3 final int responseStatusCode = e.getStatusCode().value();
4 final boolean isResponse4xx = e.getStatusCode().is4xxClientError();
5 final boolean isResponse5xx = e.getStatusCode().is5xxServerError();
6 final Map<String, List<String>> responseHeaders = e.getHeaders();
7}
2.4 Spring 6.1 RestClient
以下係 Spring 6.1 新加入既 RestClient
API。佢支援 synchronous(blocking)既 HTTP calls。
用呢個 client 既話,就唔需要再喺 Spring Boot servlet WebMVC 既項目加入 Spring Webflux 既 library。
構建 client object:
final RestClient client = RestClient.builder()
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader("header1", "header1-value")
.defaultHeader("header2", "header2-value")
.build();
發送 request:
1final String requestBody = "{ \"value\": \"123\" }";
2
3try {
4 final ResponseEntity<String> response = client.post()
5 .uri("http://localhost:8081/api/order/{orderId}?param1={param1}¶m2={param2}", "123", "param1-value", "param2-value")
6 .headers(headers -> headers.setBasicAuth("username", "password"))
7 .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
8 .header("header1", "header1-value")
9 .header("header2", "header2-value")
10 .body(requestBody)
11 .retrieve()
12 .toEntity(String.class);
13} catch (RestClientResponseException e) {
14 // 4xx 或 5xx responses
15}
2xx
或 3xx
responses:
final String responseBody = response.getBody();
final int responseStatusCode = response.getStatusCode().value();
final boolean isResponse2xx = response.getStatusCode().is2xxSuccessful();
final boolean isResponse3xx = response.getStatusCode().is3xxRedirection();
final Map<String, List<String>> responseHeaders = response.getHeaders();
4xx
或 5xx
responses:
1} catch (RestClientResponseException e) {
2 final String responseBody = e.getResponseBodyAsString();
3 final int responseStatusCode = e.getStatusCode().value();
4 final boolean isResponse4xx = e.getStatusCode().is4xxClientError();
5 final boolean isResponse5xx = e.getStatusCode().is5xxServerError();
6 final Map<String, List<String>> responseHeaders = e.getResponseHeaders();
7}
2.5 Spring Cloud OpenFeign
Spring Cloud OpenFeign 將開源既 Feign library(本來係 Netflix,但之後開源成為 OpenFeign)整合到 Spring Cloud framework 裡面。
用 Spring Cloud OpenFeign 既好處係佢係 declarative,基本上將需要用到 upstream apps 既 declarative request mappings 搬入黎我地既 downstream app project 就得。
構建 client object:
1@FeignClient(
2 name = "fooClient",
3 configuration = FooFeignConfig.class
4)
5public interface FooFeignClient {
6
7 // 注意:
8 // consumes ➜ Content-Type request header
9 // produces ➜ Accept request header
10
11 @PostMapping(path = "/api/order/{orderId}", consumes = MediaType.APPLICATION_JSON_VALUE)
12 ResponseEntity<String> api(@PathVariable("orderId") String orderId,
13 @RequestParam(name = "param1", required = false) String param1,
14 @RequestParam(name = "param2", required = false) String param2,
15 @RequestBody String body,
16 @RequestHeader("header1") String header1,
17 @RequestHeader("header2") String header2);
18}
Basic auth Spring configuration:
1// 如果加 @Configuration,呢個 config 就會係所有 Feign clients 既默認 config
2public class FooFeignConfig {
3
4 @Bean
5 public BasicAuthRequestInterceptor fooBasicAuthInterceptor() {
6 return new BasicAuthRequestInterceptor("username", "password");
7 }
8}
Application 配置:
1spring.cloud.openfeign.client.config:
2 default:
3 connectTimeout: 10000
4 loggerLevel: FULL
5 fooClient:
6 connectTimeout: 10000
7 loggerLevel: FULL
8 url: http://localhost:8081
9 defaultRequestHeaders:
10 header1: header1-value
11 header2: header2-value
12
13logging:
14 level:
15 my.package.FooFeignClient: DEBUG
發送 request:
1@Autowired
2FooFeignClient client;
3
4public void sampleCall() {
5 final String requestBody = "{ \"value\": \"123\" }";
6
7 try {
8 final ResponseEntity<String> response =
9 client.api("123", "param1-value", "param2-value", requestBody, "header1-value", "header2-value");
10 } catch (FeignException e) {
11 // 3xx、4xx 或 5xx responses
12 }
13}
2xx
responses:
final String responseBody = response.getBody();
final int responseStatusCode = response.getStatusCode().value();
final boolean isResponse2xx = response.getStatusCode().is2xxSuccessful();
final Map<String, List<String>> responseHeaders = response.getHeaders();
3xx
、4xx
或 5xx
responses:
1} catch (FeignException e) {
2 final String responseBody = e.contentUTF8();
3 final int responseStatusCode = e.status();
4 final boolean isResponse3xx = e.status() >= 300 && e.status() < 400;
5 final boolean isResponse4xx = e.status() >= 400 && e.status() < 500;
6 final boolean isResponse5xx = e.status() >= 500 && e.status() < 600;
7 final Map<String, Collection<String>> responseHeaders = e.responseHeaders();
8}
注意:
- 需要喺某個 Spring config class 度加上
@EnableFeignClients
,否則唔會生成 Feign client objects。
- Feign clients 裡面既 request mapping methods 既 API contract 應該係要 match 返 target upstream app 既 request mapping methods。
consumes
➜ Content-Type
produces
➜ Accept
- ❌ 唔好以為
produces
指既係呢個 app 所產生既 request body 既 Content-Type
,咁樣係錯既。
- 如果唔畀
consumes
attribute,佢會根據 method signature 自動推斷 Content-Type
request header 既 value。
- 就算 Feign client method 既 return type 係
ResponseEntity
,當 response status code 唔係 2xx
既時候,佢會照 throw exception。
- ❌ 唔好諗住 return
ResponseEntity
就可以拎到 response status code 然後 apply 自己既 logic,呢個係做唔到既。
- ⚠️ 某啲 Spring Cloud OpenFeign
4.x
版本有個 bug,當我地喺 @FeignClient
定義左 path
attribute(以免喺 request mapping annotations 度重複 common path prefix),而 target upstream app 既 URL 係喺 application 配置檔度定義,佢就會出 exception target values must be absolute.
,呢個 bug 應該會喺後期版本修復。
參考資料:
3 參考資料