1. 引言

在云应用开发中,我们经常需要编写代码与外部系统进行网络通信。这时就可以利用 Ambassador 模式创建一个统一组件,封装所有网络相关逻辑,显著提升代码复用性。

本文将深入探讨 Ambassador 模式,分析其适用场景,并通过 Java 实现展示具体用法。

2. 什么是 Ambassador 模式?

Ambassador 模式是一种结构型设计模式,充当客户端与服务端之间的网络代理。可以将其理解为封装了所有网络通信细节的库

它的核心功能包括:

  • ✅ 网络路由抽象
  • ✅ 可观测性(日志/监控)
  • ✅ 重试和熔断机制
  • ✅ 缓存和安全流程

当部署在独立容器时,它还能提供语言无关的通信能力,因为所有交互都通过网络接口完成。我们可以把网络客户端逻辑封装在称为 Ambassador 的独立库或容器中,作为依赖项集成到代码中,或通过 API 暴露给外部调用。

3. Java 实现 Ambassador 模式

本节我们将实现一个作为 HTTP 调用代理的 Ambassador,集成重试、超时和日志输出功能:

客户端代码、Ambassador 模式与外部 API 的交互示意图

注意 Ambassador 代码与客户端代码在同一容器中运行。客户端只需简单调用 Ambassador 方法即可从 names-api 获取结果。

3.1. 基础配置

首先添加必要依赖,使用 spring-web 进行 HTTP 调用:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>6.2.7</version>
</dependency>

再添加 spring-retry 实现重试逻辑:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>2.0.12</version>
</dependency>

application.properties 中配置超时参数和 API 地址:

http.client.connect-timeout-seconds=2000
http.client.read-timeout-seconds=3000
names-api-url=https://api.example.com/names/v1

创建使用预定义超时配置的 RestTemplate Bean:

@Configuration
public class RestTemplateConfig {
    private final int connectTimeoutSeconds;
    private final int readTimeoutSeconds;
    private final RestTemplateBuilder restTemplateBuilder;
    
    // 全参构造器

    @Bean
    public RestTemplate restTemplate() {
        return restTemplateBuilder.setConnectTimeout(Duration.ofMillis(connectTimeoutSeconds))
          .setReadTimeout(Duration.ofMillis(readTimeoutSeconds))
          .build();
    }
}

最后在 Spring Boot 主类添加注解:

@EnableRetry
@EnableCaching
@SpringBootApplication
public class AmbassadorApplication {
    public static void main(String[] args) {
        SpringApplication.run(AmbassadorApplication.class, args);
    }
}

3.2. 实现 Ambassador HTTP 客户端

现在创建作为 Ambassador 的 HTTP 客户端:

@Component
public class HttpAmbassadorNamesClient {

    private final RestTemplate restTemplate;
    private final Logger logger = LoggerFactory.getLogger(HttpAmbassadorNamesClient.class);
    public final String apiUrl;

    public HttpAmbassadorNamesClient(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 调用成功完成 url={}", apiUrl);
            return result;
        } catch (HttpClientErrorException e) {
            logger.error("HTTP 客户端错误 错误码={} 信息={}", e.getStatusCode(), e.getMessage());
            throw e;
        }
    }

    @Recover
    public String recover(Exception e) {
        final String defaultResponse = "default";
        logger.error("重试次数耗尽,回退到默认值. 错误={} 默认值={}", e.getMessage(), defaultResponse);
        return defaultResponse;
    }
}

HttpAmbassadorNamesClient 类的核心职责是提供完整配置的客户端代码来获取外部名称数据。我们注入了预配置的 RestTemplate 和 API 地址。

getResponse() 方法的关键设计点:

  1. 使用 @Cacheable 注解实现内存缓存(为简化演示未设置 TTL,实际生产环境需注意)
  2. 通过 @Retryable 配置重试策略:对 5xx 错误最多重试 5 次,间隔 1 秒
  3. 使用 getForObject() 执行 REST 调用,成功时记录日志并返回结果
  4. 捕获 HttpClientErrorException 并记录错误日志后重新抛出

现在只需在客户端代码中注入 HttpAmbassadorNamesClient,调用 getResponse() 即可完成 REST 调用。

4. Ambassador 作为 Sidecar 容器

我们还可以将 Ambassador 部署在独立容器中,通过暴露的 REST API 提供服务。这样就能实现语言无关的 API 调用,统一管理重试、缓存等逻辑:

Sidecar Ambassador 模式与多客户端容器交互示意图

由于客户端和 Ambassador 在不同容器,需要通过网络通信获取 names-api 数据。

典型场景:当两个不同语言(如 Python 和 Go)的客户端服务都需要调用同一个 API 时,传统方案需要重复实现两套网络客户端逻辑。而 Ambassador sidecar 方案只需让两个客户端调用 Ambassador 接口即可。

4.1. 暴露 Ambassador API

创建 Ambassador 的 REST 接口:

@RestController
@RequestMapping("/v1/ambassador/names")
public class HttpAmbassadorController {
    private final HttpAmbassadorNamesClient httpAmbassadorNamesClient;

    public HttpAmbassadorController(HttpAmbassadorNamesClient httpAmbassadorNamesClient) {
        this.httpAmbassadorNamesClient = httpAmbassadorNamesClient;
    }

    @GetMapping
    public String get() {
        return httpAmbassadorNamesClient.getResponse();
    }
}

这里暴露了 /v1/ambassador/names 接口,所有客户端都可以通过这个 Ambassador 访问外部名称 API。

核心价值:将超时、重试、缓存等复杂逻辑封装在单一位置,避免在不同应用中重复实现。例如 Python 和 Go 应用只需调用 Ambassador 接口,即可获得完整的网络通信能力。

5. 优缺点分析

优势

  1. 代码复用性:无论是作为依赖库还是独立 API,所有网络逻辑只需编写一次即可被多个客户端使用
  2. 维护性提升:所有网络相关修改(如更换 API 地址、调整响应字段)只需在 Ambassador 中单点维护
  3. 解耦能力:客户端无需关心网络通信细节,只需调用 Ambassador 提供的简单接口

劣势

  1. 性能损耗:Sidecar 方案增加额外网络层,必然引入延迟 ⚠️ 不适合:对延迟极其敏感的系统(如实时交易)
  2. 可用性风险:增加 Ambassador 容器作为潜在故障点 ❌ 可能影响:系统整体可用性,需做好容器健康检查

6. 总结

本文展示了如何实现配置完善的 Ambassador 应用,作为不同客户端的网络代理。我们通过单一位置集成了重试、缓存、超时等机制,显著提升了系统可维护性。

同时我们也分析了其局限性——在延迟敏感系统或高可用要求场景中需谨慎使用。简单粗暴地说:Ambassador 模式是中大型系统的网络通信利器,但在简单项目或极端性能场景中可能过度设计。

源码示例:GitHub 仓库


原始标题:Introduction to Ambassador Design Pattern | Baeldung