大使設計模式簡介
1. 簡介
使用雲端應用程式時的常見任務是編寫透過網路與外部應用程式通訊的程式碼。
為此,我們可以利用 Ambassador 模式建立包含所有必要網路元件的單一程式碼,從而有利於可重複使用性。
在本教程中,我們將深入研究大使模式,探索它何時適合我們的系統以及如何用 Java 實現它。
2.什麼是大使模式?
大使模式是一種結構化設計模式,可作為客戶端和伺服器之間的網路代理。我們可以將其設想為一個庫,它封裝了以程式設計方式呼叫外部服務時涉及的大部分網路活動。
其主要目的是抽象網路路由、可觀察性、重試和斷路器機制,以及快取和安全性工作流程。
此外,當在單獨的容器中實現時,它可以作為與其他客戶端進行語言無關的通訊方式,因為所有工作都是透過網路介面完成的。
因此,我們可以將網路用戶端邏輯封裝到一個單獨的庫或容器中,稱為 Ambassador,並將其作為依賴項公開,以便我們可以整合到我們的程式碼中,或者作為我們可以透過網路請求調用的 API。
3.用Java實現大使模式
在本節中,我們將建立一個Ambassador 實現,作為外部 HTTP 呼叫的代理,結合重試、逾時和標準輸出機制:
值得注意的是,Ambassador 程式碼與用戶端程式碼位於同一個容器中。因此,客戶端可以透過簡單的方法呼叫 Ambassador 客戶端,從names-api
取得結果。
3.1. 增加必要的配置
為了說明 Ambassador 的實現,我們首先使用spring-web
依賴項透過RestTemplate
bean 進行 HTTP 呼叫:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>6.2.7</version>
</dependency>
我們還需要spring-retry
依賴項來說明 Ambassador 配置中可能的重試邏輯:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>2.0.12</version>
</dependency>
然後,我們需要在application.properties
檔案中輸入連接和讀取逾時值以及 HTTP 呼叫的 API URL:
http.client.connect-timeout-seconds=2000
http.client.read-timeout-seconds=3000
names-api-url=https://domain.com/names/api
此外,我們需要配置RestTemplate
bean 來使用預先定義的逾時:
@Configuration
public class RestTemplateConfig {
private final int connectTimeoutSeconds;
private final int readTimeoutSeconds;
private final RestTemplateBuilder restTemplateBuilder;
// all-args constructor
@Bean
public RestTemplate restTemplate() {
return restTemplateBuilder.setConnectTimeout(Duration.ofMillis(connectTimeoutSeconds))
.setReadTimeout(Duration.ofMillis(readTimeoutSeconds))
.build();
}
}
最後,我們需要在 Spring Boot 的主類別中加入@EnableRetry
和@EnableCaching
註解:
@EnableRetry
@EnableCaching
@SpringBootApplication
public class AmbassadorPatternApplication {
public static void main(String[] args) {
SpringApplication.run(AmbassadorPatternApplication.class, args);
}
}
3.2. 實作 Ambassador REST 用戶端
一切準備就緒後,我們可以創建作為 Ambassador 的 HTTP 用戶端:
@Component
public class HttpAmbassadorNamesApiClient {
private final RestTemplate restTemplate;
private final Logger logger = LoggerFactory.getLogger(HttpAmbassadorNamesApiClient.class);
public final String apiUrl;
public HttpAmbassadorNamesApiClient(RestTemplate restTemplate, @Value("${names-api-url}") String apiUrl) {
this.restTemplate = restTemplate;
this.apiUrl = apiUrl;
}
@Cacheable(value = "httpResponses", key = "#root.target.apiUrl", unless = "#result == null")
@Retryable(value = { HttpServerErrorException.class }, maxAttempts = 5, backoff = @Backoff(delay = 1000))
public String getResponse() {
try {
String result = restTemplate.getForObject(apiUrl, String.class);
logger.info("HTTP call completed successfully to url={}", apiUrl);
return result;
} catch (HttpClientErrorException e) {
logger.error("HTTP Client Error error_code={} message={}", e.getStatusCode(), e.getMessage());
throw e;
}
}
@Recover
public String recover(Exception e) {
final String defaultResponse = "default";
logger.error("Too many retry attempts. Falling back to default. error={} default={}", e.getMessage(), defaultResponse);
return defaultResponse;
}
}
HttpAmbassadorNamesApiClient
類別的職責是提供完整配置的客戶端程式碼,以便從外部/names/api
取得名稱。為此,我們注入了預先配置的RestTemplate
,並在applications.properties
中設定了連接和讀取逾時。此外,我們也注入了names-api-url
中定義的apiUrl
。
然後,我們定義getResponse()
方法來執行 REST 呼叫。這個方法有一些關鍵的細微差別,可以很好地解釋 Ambassador 模式:
- 首先,它使用
@Cacheable
註解定義了一個緩存,用於將結果儲存在記憶體中。為了簡單起見,我們定義了一個沒有設定生存時間 (TTL) 值的緩存,而儲存值的鍵是apiUrl
。這裡面可能存在一些機會。 - 其次,我們使用
@Retryable
為外部 REST 呼叫定義重試策略,建議對於HttpServerErrorExceptions
,每秒最多重試五次。因此,我們也定義了一個恢復策略,當所有重試嘗試都完成後,傳回預設字串並輸出結果。 - 最後,我們使用
RestTemplate
中的getForObject()
方法從路由apiUrl
中定義的 API 取得資源。如果成功,我們將使用 SLF4J 記錄成功訊息並傳回結果。否則,如果發生HttpClientErrorException
錯誤,我們將記錄錯誤訊息並將錯誤拋出給呼叫者。
這樣,我們可以在客戶端程式碼中註入HttpAmbassadorNamesApiClient
,並呼叫getResponse()
方法來完成 REST 呼叫。
4. Ambassador 作為 Sidecar 容器
我們還可以在單獨的容器中實現 Ambassador,並透過暴露的 REST API 使其可用。因此,我們可以創建一個與語言無關的邏輯來呼叫外部 API,並實作前面提到的所有配置,例如重試、快取和可觀察性:
由於客戶端和 Ambassador 位於不同的容器中,我們需要在它們之間實現網路通訊以獲取names-api
結果。
例如,假設我們遇到一個問題:兩個客戶端服務使用不同的語言編寫,並且都依賴同一個伺服器 API。在這種情況下,我們需要用不同的語言為同一個網路客戶端分別編寫兩次相同的程式碼。因此,透過將 Ambassador 部署在不同的容器中,我們可以讓兩個客戶端都只依賴 Ambassador API 來實現這一需求。
4.1. 公開 Ambassador API
這樣,我們就可以公開一個端點,作為其他客戶端應用程式的大使:
@RestController
@RequestMapping("/v1/http-ambassador/names")
public class HttpAmbassadorNamesController {
private final HttpAmbassadorNamesApiClient httpAmbassadorNamesApiClient;
public HttpAmbassadorController(HttpAmbassadorNamesApiClient httpAmbassadorNamesApiClient) {
this.httpAmbassadorNamesApiClient = httpAmbassadorNamesApiClient;
}
@GetMapping
public String get() {
return httpAmbassadorNamesApiClient.getResponse();
}
}
這裡我們定義了一個/v1/http-ambassador/names
資源,讓客戶透過Ambassador存取外部資源names/api
。
擁有一個包含所有這些配置並公開端點的 HTTP 用戶端的主要目的是將邏輯壓縮到一個地方,而不是將其複製並貼上到不同的應用程式中。例如,如果兩個應用程式(一個用 Python 編寫,另一個用 Go 編寫)想要存取名稱 API,它們可以呼叫 Ambassador 並利用逾時、重試、快取和日誌記錄策略。
5.優點和缺點
使用這種模式的一個顯著優勢是程式碼的可重用性。無論使用哪種方式,Ambassador 是作為依賴項還是 REST API,所有程式碼只需編寫一次即可供多個客戶端使用,這有利於透過單一、內聚的介面實現可重複使用。
此外,Ambassador 更注重可維護性,因為我們只需在一個地方進行程式碼維護,而無需將相同的邏輯分散到不同的客戶端。例如,更改外部資源 URL 或在回應負載中新增字段,如果只在一個地方(也就是 Ambassador)完成,就更容易實現和測試。
將 Ambassador 視為 REST API 方法的一個缺點是,它增加了一層額外的網絡,這自然會引入延遲。因此,對於延遲至關重要的系統(最終用戶的回應時間應該是最佳的),Ambassador Sidecar 並不適用。此外,由於容器並非始終可靠,它還增加了一個潛在的故障點。因此,Ambassador 也可能對系統的可用性產生負面影響。
6. 結論
在本文中,我們學習如何實現一個配置的 Ambassador 應用程序,該應用程式可作為不同客戶端應用程式的網路代理。
我們探索了重試、快取和超時,並將它們添加到一處,旨在透過 Ambassador 模式提升系統的可維護性。此外,我們還討論了不建議使用 Ambassador 模式的情況,因為這可能會增加延遲並降低可用性。
與往常一樣,原始碼可在 GitHub 上取得。