3、接口适配实战:遗留系统接口适配、第三方SDK适配、多数据源统一适配

接口适配这件事,说白了就是「让不听话的接口听话」。我做了十几年系统集成,见过太多「接口能用但不好用」的案例。这一章咱们就聊聊三种最常见的适配场景:老系统的倔强接口、第三方SDK的黑盒、以及多数据源的混乱局面。

核心观点:适配不是改造对方,而是在中间层做翻译。你改变不了遗留系统的SQL,也改不了SDK的签名算法,但你可以让调用方感觉不到这些差异。

3.1 遗留系统接口适配

老系统为什么难搞?因为它们往往没有接口文档,或者文档早就过期了。我记得有一次对接一个2003年的ERP系统,对方说「我们支持SOAP」,结果连WSDL都找不到了。

遗留系统接口的常见问题:

  • 协议老旧:RMI、CORBA、私有TCP协议,甚至直接读数据库表
  • 数据格式混乱:固定长度报文、CSV、自定义XML
  • 无状态或状态管理诡异:需要手动维护Session
  • 性能瓶颈:单线程处理,并发一高就挂

我的做法是:在遗留系统前面加一层「防腐层」(Anti-Corruption Layer)。这个层只做三件事——协议转换、数据清洗、异常隔离。

// 遗留系统适配器示例:固定长度报文解析
public class LegacyOrderAdapter {
    
    public OrderDTO parseFixedLengthMessage(String message) {
        // 报文格式:订单号(20) + 客户名(30) + 金额(12) + 日期(8)
        if (message == null || message.length() != 70) {
            throw new AdapterException("报文长度不合法,期望70位,实际" + 
                (message == null ? 0 : message.length()));
        }
        
        OrderDTO order = new OrderDTO();
        order.setOrderId(message.substring(0, 20).trim());
        order.setCustomerName(message.substring(20, 50).trim());
        
        // 金额处理:老系统用分存储,我们转成元
        String amountStr = message.substring(50, 62).trim();
        order.setAmount(new BigDecimal(amountStr).divide(new BigDecimal("100")));
        
        // 日期格式:yyyymmdd -> yyyy-MM-dd
        String dateStr = message.substring(62, 70);
        order.setOrderDate(dateStr.substring(0,4) + "-" + 
                          dateStr.substring(4,6) + "-" + 
                          dateStr.substring(6,8));
        
        return order;
    }
}
我的经验:遗留系统适配最容易踩的坑是「隐式规则」。比如某个字段看似是可选,但传空会导致老系统崩溃。我曾经因为一个空格问题排查了三天——老系统的COBOL程序对全角空格会直接抛异常。

3.2 第三方SDK适配

第三方SDK适配,核心难点在于「你控制不了它」。SDK可能版本升级不兼容,可能内部有线程安全问题,甚至可能偷偷往你的classpath里塞依赖。

我一般会做三层隔离:

  1. 接口抽象层:定义业务需要的接口,不暴露SDK的任何类型
  2. 适配器层:实现接口,内部调用SDK,做异常转换和重试
  3. 健康检查层:监控SDK的调用状态,超时或异常时自动降级
// 第三方支付SDK适配器
public class PaymentGatewayAdapter implements PaymentService {
    
    private final ThirdPartySdk sdk;
    private final RetryTemplate retryTemplate;
    
    @Override
    public PaymentResult charge(PaymentRequest request) {
        // 适配器内部做参数转换
        SdkPayRequest sdkRequest = new SdkPayRequest();
        sdkRequest.setAmount(request.getAmount().longValue()); // SDK用分,我们用元
        sdkRequest.setOrderNo(request.getOrderId());
        sdkRequest.setNotifyUrl(config.getNotifyUrl());
        
        try {
            // 用重试模板包裹,SDK偶尔会网络抖动
            SdkPayResponse response = retryTemplate.execute(
                ctx -> sdk.pay(sdkRequest)
            );
            return convertToResult(response);
        } catch (SdkTimeoutException e) {
            // 超时不等于失败,需要查询最终状态
            return handleTimeout(request);
        } catch (SdkBusinessException e) {
            // 业务异常直接返回,不需要重试
            return PaymentResult.failed(e.getCode(), e.getMessage());
        }
    }
    
    private PaymentResult convertToResult(SdkPayResponse response) {
        // 把SDK的响应转成统一格式
        PaymentResult result = new PaymentResult();
        result.setSuccess(response.isSuccess());
        result.setTransactionId(response.getTradeNo());
        result.setMessage(response.getMsg());
        return result;
    }
}
避坑指南:我曾经遇到一个SDK,它的HttpClient内部用了连接池,但没做空闲连接回收。系统运行三天后,所有请求都卡在获取连接上。从那以后,我接手第三方SDK的第一件事就是检查它的资源管理方式——连接池、线程池、文件句柄,一个都不能放过。

3.3 多数据源统一适配

多数据源适配,说白了就是「让不同的数据库说同一种话」。MySQL、Oracle、Redis、Elasticsearch,它们的查询语法、事务模型、连接方式都不一样。但业务层不想关心这些,它只想要数据。

我常用的方案是「数据源路由 + 统一查询接口」:

数据源类型 适配策略 典型场景 注意事项
关系型数据库 JDBC + 连接池 订单、用户等核心数据 事务一致性、分页方言差异
缓存 RedisTemplate / Jedis 热点数据、会话 序列化方式、过期策略
搜索引擎 RestHighLevelClient 全文检索、聚合分析 索引映射、分词器
文件存储 OSS SDK / FTP 图片、日志、备份 分片上传、断点续传

来看一个统一查询接口的实现思路:

// 统一数据查询接口
public interface DataQueryService {
    <T> T query(String dataSource, String queryKey, Map<String, Object> params);
}

// 适配器注册中心
@Component
public class DataSourceAdapterRegistry {
    
    private final Map<String, DataSourceAdapter> adapters = new ConcurrentHashMap<>();
    
    public void register(String type, DataSourceAdapter adapter) {
        adapters.put(type, adapter);
    }
    
    public DataSourceAdapter getAdapter(String dataSource) {
        DataSourceAdapter adapter = adapters.get(dataSource);
        if (adapter == null) {
            throw new AdapterNotFoundException("未找到数据源适配器: " + dataSource);
        }
        return adapter;
    }
}

// MySQL适配器示例
@Component
public class MysqlAdapter implements DataSourceAdapter {
    
    private final JdbcTemplate jdbcTemplate;
    
    @Override
    public Object execute(String queryKey, Map<String, Object> params) {
        // 根据queryKey找到对应的SQL模板
        String sql = SqlRegistry.getSql(queryKey);
        // 参数绑定,防止SQL注入
        return jdbcTemplate.queryForList(sql, params);
    }
}
我的习惯:多数据源适配最容易出问题的是「事务边界」。比如你从MySQL查了数据,写到Redis,再写到ES——任何一个步骤失败,都需要回滚。我一般会用「本地消息表 + 补偿任务」来保证最终一致性,而不是强依赖分布式事务。

3.4 适配层架构总览

这三种适配场景,其实可以抽象成一个统一的架构模式。我画了一张图,你看完应该就明白了:

接口适配层架构总览 业务调用方 统一接口层(Facade) 适配器层(Adapter) 遗留系统适配器 协议转换 / 数据清洗 第三方SDK适配器 异常隔离 / 重试降级 多数据源适配器 路由 / 方言转换 遗留系统(ERP/CRM) 第三方SDK(支付/地图) 多数据源(MySQL/Redis/ES) 核心原则:适配层对上层暴露统一接口,对下层封装差异 业务代码不直接依赖任何具体实现,只依赖接口抽象

你看这个架构,从上到下分了三层。业务调用方只跟统一接口层打交道,根本不知道底层是MySQL还是老掉牙的COBOL系统。适配器层负责把「脏活累活」都干了——协议转换、异常处理、重试降级。

总结一下:接口适配的核心不是技术,是「边界意识」。你要清楚哪些是你能控制的,哪些是你控制不了的。能控制的,做好抽象和封装;控制不了的,做好隔离和降级。这样系统才能经得起折腾。