Adapter 适配器模式

你的团队正在将一个运行了 10 年的单体系统迁移到 Kubernetes。这个老系统使用自定义的 TCP 协议通信,消息格式是公司早年定义的二进制格式,没有任何 REST 接口。当它需要与 Kubernetes 环境中新建的微服务通信时,双方就像说不同语言的人——老系统说「二进制码」,新服务说「JSON」。

怎么办?总不能为了兼容性把 10 年的老代码重写一遍吧。

Adapter 适配器模式正是为这种场景而生:不需要修改老系统,也不要求新服务学习老协议,在中间加一层「翻译官」——适配器——让双方都能用自己熟悉的方式通信。

适配器模式的核心思想

适配器模式将一个类的接口转换成客户端期望的另一个接口,使原本由于接口不兼容而无法一起工作的类可以协同工作。

flowchart TB
    subgraph Client["客户端\n(期望标准接口)"]
        C["业务代码"]
    end

    subgraph Adapter["适配器"]
        A["接口转换层\n协议适配\n数据转换"]
    end

    subgraph Adaptee["被适配者\n(现有系统)"]
        S["遗留系统\n自定义协议"]
    end

    C -->|"标准调用"| A
    A -->|"协议转换"| S

在容器化场景中,适配器通常以 Sidecar 的形式部署在同一个 Pod 中,拦截并转换主应用与外部系统的通信。

Protocol Adapter:协议互转

gRPC/HTTP/REST 互转

企业内部常存在多种协议共存的情况:老系统用 SOAP,新系统用 REST,更新的服务用 gRPC。通过 Protocol Adapter 可以实现协议间的透明转换。

flowchart LR
    subgraph Client["HTTP/REST 客户端"]
        HTTP["REST API"]
    end

    subgraph Adapter["Protocol Adapter"]
        G["gRPC\n处理器"]
        R["REST\n处理器"]
        S["SOAP\n处理器"]
    end

    subgraph Backend["后端服务"]
        GRPC["gRPC 服务"]
        DB["数据库"]
    end

    HTTP -->|"HTTP/JSON"| R
    R -->|"内部格式"| G
    G -->|"gRPC"| GRPC
    GRPC -->|"数据"| DB

实现示例

public class HttpToGrpcAdapter {
    private final UserServiceGrpc.UserServiceBlockingStub grpcStub;

    public HttpToGrpcAdapter(ManagedChannel channel) {
        this.grpcStub = UserServiceGrpc.newBlockingStub(channel);
    }

    // REST 接口实现
    @PostMapping("/users")
    public UserResponse createUser(CreateUserRequest request) {
        // 将 REST 请求转换为 gRPC 请求
        GrpcCreateUserRequest grpcRequest = GrpcCreateUserRequest.newBuilder()
            .setName(request.getName())
            .setEmail(request.getEmail())
            .setDepartmentId(request.getDepartmentId())
            .build();

        // 调用 gRPC 服务
        GrpcUserResponse grpcResponse = grpcStub.createUser(grpcRequest);

        // 将 gRPC 响应转换回 REST 格式
        return UserResponse.builder()
            .id(grpcResponse.getId())
            .name(grpcResponse.getName())
            .email(grpcResponse.getEmail())
            .createdAt(Instant.ofEpochSecond(grpcResponse.getCreatedAt()))
            .build();
    }
}

数据格式适配:JSON/XML/Protobuf 互转

数据格式的不一致是系统集成的另一个痛点。老系统可能使用 XML,新系统使用 JSON,更高性能的场景使用 Protobuf。适配器需要在这几种格式之间自由转换。

public class DataFormatAdapter {
    private final ObjectMapper jsonMapper;
    private final JAXBContext xmlContext;

    public DataFormatAdapter() throws JAXBException {
        this.jsonMapper = new ObjectMapper();
        this.jsonMapper.registerModule(new JavaTimeModule());
        this.xmlContext = JAXBContext.newInstance(LegacyXmlEntity.class);
    }

    // XML 转 JSON
    public String xmlToJson(String xmlPayload) throws Exception {
        StringReader reader = new StringReader(xmlPayload);
        LegacyXmlEntity entity = (LegacyXmlEntity) xmlContext.createUnmarshaller()
            .unmarshal(reader);

        ModernJsonEntity modernEntity = ModernJsonEntity.builder()
            .id(entity.getId())
            .name(entity.getFullName())
            .status(entity.getState())
            .metadata(entity.getExtendedInfo())
            .build();

        return jsonMapper.writeValueAsString(modernEntity);
    }

    // JSON 转 Protobuf
    public byte[] jsonToProtobuf(String jsonPayload) throws Exception {
        ModernJsonEntity jsonEntity = jsonMapper.readValue(jsonPayload, ModernJsonEntity.class);

        ProtobufEntity protoEntity = ProtobufEntity.newBuilder()
            .setId(jsonEntity.getId())
            .setName(jsonEntity.getName())
            .setStatus(jsonEntity.getStatus())
            .putAllMetadata(jsonEntity.getMetadata())
            .build();

        return protoEntity.toByteArray();
    }
}

容器适配器模式:Sidecar 适配器

在 Kubernetes 环境中,适配器常以 Sidecar 的形式部署。这种模式充分利用了 Sidecar 的优势:与主应用解耦、独立升级、语言无关。

flowchart TB
    subgraph Pod["Kubernetes Pod"]
        subgraph Main["主应用容器"]
            M["遗留系统\n(二进制协议)"]
        end
        subgraph Adapter["Adapter Sidecar"]
            A["协议转换\n数据转换\n格式适配"]
        end
    end

    External["K8s 新服务\nJSON/REST"] <-->|"标准协议"| A
    A <-->|"二进制协议"| M

Kubernetes Adapter Sidecar 实现

public class ProtocolAdapterSidecar {
    private final Server grpcServer;
    private final BinaryProtocolClient legacyClient;
    private final int listenPort;

    public ProtocolAdapterSidecar(String legacyHost, int legacyPort) {
        // 连接遗留系统的二进制协议
        this.legacyClient = new BinaryProtocolClient(legacyHost, legacyPort);

        // 启动 gRPC 服务监听
        this.grpcServer = NettyServerBuilder.forPort(listenPort)
            .addService(new AdapterServiceImpl())
            .build();
    }

    private class AdapterServiceImpl extends AdapterServiceGrpc.AdapterServiceImplBase {
        @Override
        public void getUser(GetUserRequest request, StreamObserver<UserResponse> observer) {
            try {
                // 1. 将 gRPC 请求转换为二进制协议
                byte[] binaryRequest = legacyClient.marshal(
                    "GET_USER",
                    request.getUserId()
                );

                // 2. 调用遗留系统
                byte[] binaryResponse = legacyClient.send(binaryRequest);

                // 3. 将二进制响应转换为 gRPC 响应
                UserResponse response = legacyClient.unmarshalUser(binaryResponse);

                observer.onNext(response);
                observer.onCompleted();
            } catch (Exception e) {
                observer.onError(Status.INTERNAL
                    .withDescription(e.getMessage())
                    .asRuntimeException());
            }
        }
    }
}

对应的 Kubernetes 配置:

apiVersion: v1
kind: Pod
metadata:
  name: legacy-app-with-adapter
spec:
  containers:
  - name: legacy-app
    image: legacy-system:v10.2
    ports:
    - containerPort: 5000
  - name: protocol-adapter
    image: adapter-sidecar:v2.0
    ports:
    - containerPort: 9090
    env:
    - name: LEGACY_HOST
      value: "localhost"
    - name: LEGACY_PORT
      value: "5000"

Adapter vs Ambassador vs Sidecar 对比

这三个模式都涉及「代理」,但解决的问题和使用场景不同:

维度AdapterAmbassadorSidecar
核心职责接口转换、协议适配封装远程调用细节透明拦截所有流量
关注点让异构系统能通信简化客户端调用逻辑统一治理基础设施
典型场景遗留系统接入新环境跨语言服务调用服务网格
是否改写接口是,暴露新接口是,封装旧接口否,流量透明
与主应用关系可以是 Sidecar可以是 Sidecar主应用的透明代理
flowchart TB
    subgraph 三者关系["三模式对比"]
        AD["Adapter\n协议转换"] 
        AM["Ambassador\n封装调用"]
        SI["Sidecar\n透明拦截"]
    end

    AD -.->|"解决\n接口不兼容"| AM
    AM -.->|"封装\n远程调用"| SI

适配器的设计原则

单一职责

适配器只负责转换,不应该包含业务逻辑。如果转换逻辑过于复杂,考虑拆分为主适配器和多个细粒度的转换器。

双向适配

好的适配器应该支持双向转换。如果只支持单向(老→新),当新系统需要回调老系统时会遇到麻烦。

public interface BidirectionalAdapter<Source, Target> {
    Target adapt(Source source);
    Source reverse(Target target);
}

错误处理与日志

适配器是系统间的桥梁,任何一端出问题都会影响另一端。适配器需要完善的错误处理和日志记录,便于排查问题。

public <T, R> R safeAdapt(T input, Adaptor<T, R> adaptor, String operation) {
    try {
        log.debug("开始适配操作: {}, 输入: {}", operation, input);
        R result = adaptor.adapt(input);
        log.debug("适配操作完成: {}, 输出: {}", operation, result);
        return result;
    } catch (Exception e) {
        log.error("适配操作失败: {}, 输入: {}, 错误: {}", 
            operation, input, e.getMessage(), e);
        throw new AdapterException("适配失败: " + operation, e);
    }
}

容器化场景下的最佳实践

用 Istio VirtualService 做协议路由:对于 HTTP/gRPC 的互转,可以直接配置 Istio 的 DestinationRuleVirtualService,而不是自己写适配器。

Protocol Buffer 作为内部标准:在适配器内部使用 Protobuf 作为统一的中间表示,可以简化多格式转换的复杂度。

适配器独立部署:将适配器作为独立服务或 Sidecar 部署,而不是嵌入主应用,便于独立升级和扩展。

健康检查:适配器需要同时检查自身健康和下游服务的健康,当下游不可用时应该及时反馈给调用方。

思考题

问题 1:适配器模式和装饰器模式(Decorator)有什么区别?

参考答案

适配器模式解决的是接口不兼容问题,通过转换接口让本不能一起工作的类可以协同工作;装饰器模式解决的是功能增强问题,在不改变接口的前提下动态添加功能。适配器是「翻译官」,让双方用不同的语言交流;装饰器是「增强器」,在原有功能上叠加新能力。在实现上,适配器通常不改变被适配者的行为,只是格式转换;装饰器会调用被装饰者的方法,并在此前后添加逻辑。

问题 2:在微服务架构中,Protocol Adapter 放在哪一层更合理?

参考答案

这取决于适配的场景。如果是为单个遗留系统适配,放置在该服务的 Pod 中(Sidecar Adapter)更合理,便于该服务的所有调用方都能透明使用。如果适配的是多个服务的协议转换,放在 API Gateway 层更合理,避免重复建设。如果适配的是数据格式(如 ETL 场景),放在数据流处理层(如 Kafka Connect)更合理。原则是:适配器应该尽可能靠近「变化源」,让变更的影响范围最小。

问题 3:适配器模式如何保证数据转换的完整性?

参考答案

数据转换完整性需要考虑三个方面:1)字段映射的完整性,使用 Schema Registry 或 JSON Schema 验证输入输出;2)版本兼容性,设计向前向后兼容的字段演进策略;3)转换校验,对关键字段进行范围、非空、格式校验,异常时明确报错而不是静默丢弃。对于关键业务场景,应该编写双向转换的单测,并使用 Property-Based Testing 覆盖各种边界情况。