Java 23 种设计模式:从踩坑到精通 | 适配器模式 —— 让不兼容的接口也能一起工作

Java 23 种设计模式:从踩坑到精通 | 适配器模式 ------ 让不兼容的接口也能一起工作

摘要 :对接第三方 SDK 时发现接口不兼容?接手老项目时发现 API 格式对不上?直接改源码风险大,重写又成本太高。适配器模式通过一个"转换头",让原本不兼容的接口协同工作,无需修改原有代码。本文从真实的"物流轨迹推送"场景出发,完整讲解对象适配器、类适配器、缺省适配器三种形态,深入 Spring MVC HandlerAdapter 源码,结合 JDK I/O、异步与响应式适配、泛型适配器等现代 Java 实践,帮你掌握"接口转换"的设计精髓。
📖 《Java 23 种设计模式:从踩坑到精通》

开篇:系列介绍与目录 | 上一篇:原型模式 | 当前:适配器模式 | 下一篇:桥接模式

🔗 返回系列总目录


1. 从"物流轨迹对接"的水土不服说起

某天,你们公司接入了第三方的物流轨迹服务,对方提供了一个功能强大的 LogisticsTracker 类:

java 复制代码
// 第三方物流 SDK,不能修改源码
public class LogisticsTracker {
    public String queryTrajectory(String waybillNo, String carrierCode) {
        // 查询物流轨迹,返回 JSON 字符串
        return "{\"status\":\"运输中\",\"location\":\"北京分拨中心\"}";
    }
}

这个方法参数是两个字符串,返回的是 JSON。但你整个 WMS 系统里统一用的是 TrajectoryService 接口------参数是一个 TrajectoryQuery 对象,返回的是 TrajectoryResult 对象。格式完全对不上。

直接改调用方代码?几十个业务模块都在用 TrajectoryService,改起来风险极大。修改第三方 SDK?不可能。放弃不用重新造轮子?更不可能。

适配器模式就是用来解决这种"接口水土不服"的:它像一个转接头,把一个类的接口转换成客户端期望的另一个接口,让原本不兼容的两端可以协同工作。

1.1 你的场景该不该用适配器?

判断标准 是 → 用适配器 否 → 用其他方式
需要对接的类源码不能修改(第三方SDK、老系统)
已有系统有统一的接口规范,新组件接口不兼容
只是想给现有类增加功能,接口本身没变 用装饰器模式
只是想控制访问,接口本身没变 用代理模式

2. 模式定义与 UML 结构

适配器模式 将一个类的接口转换成客户期望的另一个接口,使得原本因接口不兼容而无法一起工作的类可以合作。它属于 结构型设计模式

适配器模式有三种形态:类适配器(继承)对象适配器(组合)缺省适配器(空实现)

2.1 类适配器(继承 Adaptee)

角色说明

  • Target:客户端期望的目标接口;
  • Adaptee:原有的类,接口不兼容,需要被适配;
  • Adapter :适配器,继承 Adaptee 并实现 Target 接口,在 request() 中调用 specificRequest()

2.2 对象适配器(持有 Adaptee 实例)

✅ 对象适配器通过组合代替继承,更灵活,是实际开发中的首选。

2.3 缺省适配器

Target 是一个有很多方法的接口,而你只想对其中的个别方法感兴趣时,先用一个抽象类提供所有方法的空实现,子类只需重写关心的方法。


3. 代码实现:物流轨迹适配器(对象适配器,推荐)

3.1 系统已有的目标接口(Target)

java 复制代码
/**
 * WMS 系统统一的物流轨迹接口
 */
public interface TrajectoryService {
    TrajectoryResult query(TrajectoryQuery query);
}

3.2 已有系统的参数和返回值类型

java 复制代码
// 查询参数
public class TrajectoryQuery {
    private String waybillNo;   // 运单号
    private String carrier;     // 承运商编码
    // getters/setters...
}

// 查询结果
public class TrajectoryResult {
    private String status;      // 状态
    private String location;    // 当前位置
    // getters/setters...
}

3.3 第三方 SDK(Adaptee,不可修改)

java 复制代码
// 第三方提供的物流轨迹查询类,不可修改源码
public class LogisticsTracker {
    public String queryTrajectory(String waybillNo, String carrierCode) {
        // 实际会调用第三方 API
        return "{\"status\":\"运输中\",\"location\":\"北京分拨中心\"}";
    }
}

注意:这个方法参数是两个字符串,返回的是 JSON。与系统统一的 TrajectoryService 接口完全不兼容。

3.4 对象适配器(推荐)

java 复制代码
/**
 * 物流轨迹适配器
 * 实现系统统一的 TrajectoryService 接口
 * 内部持有第三方 LogisticsTracker 实例
 */
public class LogisticsTrackerAdapter implements TrajectoryService {
    private LogisticsTracker tracker;  // 持有 Adaptee

    public LogisticsTrackerAdapter(LogisticsTracker tracker) {
        this.tracker = tracker;
    }

    @Override
    public TrajectoryResult query(TrajectoryQuery query) {
        // 1. 参数转换:把 WMS 统一参数转为第三方需要的格式
        String waybillNo = query.getWaybillNo();
        String carrierCode = query.getCarrier();

        // 2. 调用第三方方法
        String jsonResponse = tracker.queryTrajectory(waybillNo, carrierCode);

        // 3. 结果转换:把 JSON 转为 WMS 统一的结果对象
        return parseJsonToResult(jsonResponse);
    }

    private TrajectoryResult parseJsonToResult(String json) {
        // 简化的 JSON 解析(实际项目中用 Jackson/Gson)
        TrajectoryResult result = new TrajectoryResult();
        if (json.contains("运输中")) {
            result.setStatus("运输中");
        }
        if (json.contains("北京分拨中心")) {
            result.setLocation("北京分拨中心");
        }
        return result;
    }
}

✅ 适配器完成了"参数转换 + 方法调用 + 结果转换"三个动作,让第三方 SDK 无缝融入到 WMS 统一接口中。

3.5 客户端调用

java 复制代码
// 业务层代码,只依赖 TrajectoryService 接口
public class WaybillService {
    private TrajectoryService trajectoryService;

    public WaybillService(TrajectoryService trajectoryService) {
        this.trajectoryService = trajectoryService;
    }

    public void trackWaybill(String waybillNo, String carrier) {
        TrajectoryQuery query = new TrajectoryQuery();
        query.setWaybillNo(waybillNo);
        query.setCarrier(carrier);

        TrajectoryResult result = trajectoryService.query(query);
        System.out.println("物流状态:" + result.getStatus() + ",位置:" + result.getLocation());
    }
}

// 使用适配器
LogisticsTracker thirdPartyTracker = new LogisticsTracker();
TrajectoryService adapter = new LogisticsTrackerAdapter(thirdPartyTracker);
WaybillService service = new WaybillService(adapter);
service.trackWaybill("YD123456789", "YTO");
// 输出:物流状态:运输中,位置:北京分拨中心

✅ 业务代码只依赖 TrajectoryService 接口,完全不知道底层是第三方 SDK 还是自己的实现。后续换物流商,只需新增一个 Adapter 类,业务代码零修改。


4. 类适配器实现(需要时可用)

java 复制代码
// 继承第三方类,同时实现系统统一接口
public class LogisticsTrackerClassAdapter extends LogisticsTracker implements TrajectoryService {

    @Override
    public TrajectoryResult query(TrajectoryQuery query) {
        String json = queryTrajectory(
            query.getWaybillNo(), 
            query.getCarrier()
        );
        return parseJsonToResult(json);
    }

    private TrajectoryResult parseJsonToResult(String json) { /* ... */ }
}

局限 :Java 单继承,Adapter 继承了 LogisticsTracker 就无法再继承其他类。如果有多个 Adaptee 需要适配,类适配器无法处理。

日常开发首选对象适配器,除非你需要重写 Adaptee 的某些方法。


5. 进阶:Spring MVC 源码剖析 ------ HandlerAdapter 为什么是适配器?

适配器模式在顶级框架中扮演着"统一接口"的关键角色。Spring MVC 的 DispatcherServlet 需要调用各种不同类型的 Handler,但每种 Handler 的调用方式不同。它用 HandlerAdapter 来消除这些差异。

5.1 核心类图

5.2 DispatcherServlet 核心逻辑(简化版)

java 复制代码
public class DispatcherServlet {
    private List<HandlerAdapter> handlerAdapters;  // 所有适配器

    // 核心调度方法
    protected void doDispatch(HttpServletRequest req, HttpServletResponse resp) {
        // 1. 获取处理器
        Object handler = getHandler(req);
        
        // 2. 找到支持该处理器的适配器
        HandlerAdapter adapter = getHandlerAdapter(handler);
        
        // 3. 统一调用
        ModelAndView mv = adapter.handle(req, resp, handler);
        
        // 4. 渲染视图...
    }

    private HandlerAdapter getHandlerAdapter(Object handler) {
        for (HandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new ServletException("No adapter for handler");
    }
}

5.3 不同处理器的适配实现

java 复制代码
// 适配 @RequestMapping 注解方法
public class RequestMappingHandlerAdapter implements HandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return handler instanceof HandlerMethod;
    }

    @Override
    public ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
        // 反射调用 Controller 方法,处理 @RequestParam、@PathVariable 等
        return ((HandlerMethod) handler).invokeAndHandle(req, resp);
    }
}

// 适配 HttpRequestHandler 接口
public class HttpRequestHandlerAdapter implements HandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return handler instanceof HttpRequestHandler;
    }

    @Override
    public ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
        ((HttpRequestHandler) handler).handleRequest(req, resp);
        return null;
    }
}

💡 核心价值 :如果没有 HandlerAdapterDispatcherServlet 必须写一长串 if-else 来判断 handler 的类型并分别调用。有了适配器模式,新增一种处理器类型只需新增一个 Adapter 实现,核心调度逻辑零修改,完美遵循开闭原则。


6. 扩展:缺省适配器与接口隔离原则

6.1 经典案例:AWT 的 WindowAdapter(遗留场景)

Java AWT 中的 WindowListener 有 7 个方法,而开发者往往只关心窗口关闭事件。缺省适配器提供了所有方法的空实现:

java 复制代码
// 原始接口(7 个方法)------这是典型的"胖接口"
public interface WindowListener {
    void windowOpened();
    void windowClosing();
    void windowClosed();
    void windowIconified();
    void windowDeiconified();
    void windowActivated();
    void windowDeactivated();
}

// 缺省适配器(所有方法空实现)
public abstract class WindowAdapter implements WindowListener {
    @Override public void windowOpened() {}
    @Override public void windowClosing() {}
    @Override public void windowClosed() {}
    @Override public void windowIconified() {}
    @Override public void windowDeiconified() {}
    @Override public void windowActivated() {}
    @Override public void windowDeactivated() {}
}

// 客户端只需继承适配器,重写关心的方法
public class MyWindowListener extends WindowAdapter {
    @Override
    public void windowClosing() {
        System.out.println("窗口正在关闭,保存数据...");
    }
}

6.2 现代 Java 的最佳实践

⚠️ 局限性 :缺省适配器是"先污染后治理"------在设计接口时过于臃肿,再用适配器去补救。在现代 Java 开发中,应该遵循接口隔离原则(ISP),在设计阶段就把大接口拆分为多个小接口。

更好的方案:使用 default 方法 + 函数式接口

java 复制代码
// 使用函数式接口 + default 方法替代胖接口
public interface ModernWindowListener {
    default void onOpen() {}
    default void onClose() {}
    default void onFocus() {}

    // 客户端只需实现关心的回调
    static ModernWindowListener onClose(Consumer<Void> callback) {
        return new ModernWindowListener() {
            @Override
            public void onClose() { callback.accept(null); }
        };
    }
}

现代最佳实践:如果接口是新设计的,尽量遵循接口隔离原则拆分为小接口。只有当对接遗留的"胖接口"时(如第三方 SDK 或老框架),才使用缺省适配器作为补救手段。


7. 现代 Java:异步与泛型适配

7.1 异步适配:把回调转为 CompletableFuture

现代 Java 应用越来越多使用异步编程。假设第三方 SDK 提供的是回调式接口,而你的系统统一使用 CompletableFuture

java 复制代码
// 第三方物流 SDK(回调风格)
public class AsyncLogisticsTracker {
    public void queryTrajectoryAsync(String waybillNo, TrajectoryCallback callback) {
        // 异步查询,完成后回调
        new Thread(() -> {
            String result = "{\"status\":\"运输中\"}";
            callback.onSuccess(result);
        }).start();
    }
}

public interface TrajectoryCallback {
    void onSuccess(String json);
    void onError(Exception e);
}

异步适配器:将回调转为 CompletableFuture

java 复制代码
import java.util.concurrent.CompletableFuture;

/**
 * 异步适配器:将回调式接口适配为 CompletableFuture
 */
public class AsyncTrajectoryAdapter {
    private AsyncLogisticsTracker asyncTracker;

    public AsyncTrajectoryAdapter(AsyncLogisticsTracker asyncTracker) {
        this.asyncTracker = asyncTracker;
    }

    // 核心:把回调转成 CompletableFuture
    public CompletableFuture<TrajectoryResult> queryAsync(TrajectoryQuery query) {
        CompletableFuture<TrajectoryResult> future = new CompletableFuture<>();

        asyncTracker.queryTrajectoryAsync(
            query.getWaybillNo(),
            new TrajectoryCallback() {
                @Override
                public void onSuccess(String json) {
                    future.complete(parseJsonToResult(json));
                }

                @Override
                public void onError(Exception e) {
                    future.completeExceptionally(e);
                }
            }
        );

        return future;
    }

    private TrajectoryResult parseJsonToResult(String json) { /* ... */ }
}

客户端调用

java 复制代码
AsyncTrajectoryAdapter adapter = new AsyncTrajectoryAdapter(new AsyncLogisticsTracker());
adapter.queryAsync(query)
    .thenAccept(result -> System.out.println("物流状态:" + result.getStatus()))
    .exceptionally(e -> { System.out.println("查询失败"); return null; });

💡 适配器在这里不仅仅是"参数格式"的转换,更是"调用模型"的转换------从回调模型适配为 Future 模型,这正是适配器在现代 Java 中的深度应用。

7.2 泛型适配器:一次编写,到处适配

利用 Java 泛型 + 函数式接口,可以编写通用的适配器基类:

java 复制代码
import java.util.function.Function;

/**
 * 泛型适配器
 * @param <S> 源类型(Adaptee 返回的类型)
 * @param <T> 目标类型(客户端期望的类型)
 */
public class GenericAdapter<S, T> implements TrajectoryService {
    private Function<TrajectoryQuery, S> sourceCaller;  // 如何调用 Adaptee
    private Function<S, T> converter;                    // 如何转换结果

    public GenericAdapter(Function<TrajectoryQuery, S> sourceCaller, Function<S, T> converter) {
        this.sourceCaller = sourceCaller;
        this.converter = converter;
    }

    @Override
    @SuppressWarnings("unchecked")
    public TrajectoryResult query(TrajectoryQuery query) {
        S sourceResult = sourceCaller.apply(query);
        T targetResult = converter.apply(sourceResult);
        return (TrajectoryResult) targetResult;
    }
}

// 使用泛型适配器(无需为每个平台写一个 Adapter 类)
LogisticsTracker tracker = new LogisticsTracker();
GenericAdapter<String, TrajectoryResult> adapter = new GenericAdapter<>(
    query -> tracker.queryTrajectory(query.getWaybillNo(), query.getCarrier()),
    json -> parseJsonToResult(json)
);

✅ 泛型适配器将"调用方式"和"转换逻辑"参数化,减少了大量样板类,在需要适配多种格式时特别实用。


8. 优缺点一览

优点 缺点
解耦:目标类与适配者类解耦,通过引入适配器重用现有类 类适配器受限于 Java 单继承
透明:客户端通过同一接口操作,无需关心内部适配细节 过多的适配器会让系统变得凌乱
灵活:对象适配器可以适配多个不同的 Adaptee 及其子类 对象适配器若需替换 Adaptee 的方法,比较困难
高复用:不需要修改原有类,完全符合开闭原则 对接口差异巨大的场景,适配器可能写得较复杂

9. 适配器模式 vs 装饰器模式 vs 代理模式

对比维度 适配器模式 装饰器模式 代理模式
目的 转换接口 增强功能 控制访问
是否改变接口 ✅ 改变 ❌ 不改变 ❌ 不改变
与被包装对象关系 一对一 可层层包装 一对一
典型应用 InputStreamReader BufferedInputStream Spring AOP

💡 简单记忆:适配器是"翻译官"(换接口),装饰器是"加料师"(不换接口加功能),代理是"中介人"(不换接口控访问)。


10. 框架与实践中的应用

10.1 JDK I/O:InputStreamReader / OutputStreamWriter

InputStream 是字节流,Reader 是字符流。InputStreamReader 内部持有 InputStream,将其适配成 Reader,完成字节到字符的转换。

java 复制代码
Reader reader = new InputStreamReader(
    new FileInputStream("data.txt"), StandardCharsets.UTF_8
);

10.2 Spring MVC:HandlerAdapter(详见第 5 节)

10.3 电子面单对接

《电商多平台电子面单对接实战》电子面单项目中,物流轨迹同样涉及多平台适配:抖音、淘宝、拼多多各有自己的轨迹查询 API。通过适配器模式,为每个平台实现一个 Adapter,业务层只依赖统一的 TrajectoryService 接口,新增平台只需新增一个 Adapter 类。

详细请关注《电商多平台电子面单对接实战》系列文章,更多设计模式参见【电子面单对接实战|开篇】从"能跑就行"到"整洁架构"------WMS多平台发货系统重构手记 目录


11. AI 时代的适配器模式

在 AI 辅助编程时代,适配器模式的代码可以由 AI 快速生成。你只需要清晰地描述需求:

推荐的 Prompt 模板

"请帮我写一个适配器,实现 Target 接口,内部持有 Adaptee 实例。调用 Target.request() 时,把参数 A 转换为参数 B,调用 Adaptee.specificMethod(B),最后把返回值转换为 Target 期望的返回类型。使用对象适配器方式(组合),不要修改 Adaptee 源码。"

有了这个 Prompt,AI 可以在几秒内生成你需要的适配器骨架代码,你只需填充转换逻辑即可。


12. 常见误区与面试高频题

❌ 误区1:适配器模式与装饰器模式一模一样

适配器改变接口,装饰器增强功能但不改变接口。InputStreamReader 是把字节流接口转成字符流接口(适配器),BufferedInputStream 是给字节流加上缓冲功能(装饰器)。

❌ 误区2:有不同接口就应该用适配器

如果是新开发的系统,且两个模块都由你控制,更好的方案是统一接口设计,而不是后期加适配器。适配器的核心价值在于"不修改已有代码"。

💡 面试高频追问

  • 适配器模式分为哪几种?区别在哪? → 类适配器(继承)、对象适配器(组合)、缺省适配器(抽象空实现)。对象适配器更灵活,是实际开发首选。
  • InputStreamReader 用了哪种适配器? → 对象适配器。它内部持有 InputStream 实例。
  • Spring MVC 中哪里用了适配器模式?HandlerAdapter 将不同类型的 Handler 适配给 DispatcherServlet 统一调用。
  • 适配器模式和策略模式的区别? → 适配器用于解决接口不兼容,策略用于算法的封装和替换。

🎉 恭喜 :如果你能立刻说出"InputStreamReader 是适配器模式,BufferedInputStream 是装饰器模式",并理解 Spring 为什么用 HandlerAdapter 而不是 if-else,你已经掌握了结构型模式中最核心的区分点和框架设计思想。


13. 六大设计原则在适配器模式中的体现

设计原则 体现
单一职责(SRP) 适配器只负责接口转换,不涉及业务逻辑变更
开闭原则(OCP) 通过新增适配器类扩展兼容性,无需修改原有类或第三方 SDK
里氏替换(LSP) 客户端使用 Target 接口,任何 Adapter 实现都可替换
依赖倒置(DIP) 客户端依赖抽象 Target 接口,不依赖具体适配器
接口隔离(ISP) 缺省适配器避免强制实现不需要的方法;新设计应拆分大接口
迪米特法则(LoD) 客户端只需知道 Target 接口,无需了解 Adaptee 的存在

🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。

📦 福利预告 :全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。

🚀 下一篇桥接模式 ------ 类爆炸?试试分离抽象与实现!已发布,欢迎前去阅读相关设计细节。
📌 除了设计模式,我也在深挖智能物流实战 (WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》《电商多平台电子面单对接实战》。技术相通,思路可鉴。

相关推荐
小高学习java2 小时前
事务的边界问题,如何判断数据回滚时机。
java·数据库·后端
何极光2 小时前
Maven安装与配置
java·maven
Ting.~2 小时前
在java中接入百度地图
java·开发语言·dubbo
敲个大西瓜2 小时前
加密算法小解
java
阿维的博客日记2 小时前
怎么样才算是用到了反射呢?有什么关键特征吗
java
葡萄皮sandy3 小时前
NestJS + Mongoose 全栈开发面试总结
mongodb·面试
wuminyu3 小时前
Java世界中StringTable源码剖析
java·linux·c语言·jvm·c++
一个做软件开发的牛马3 小时前
Spring Boot 自动配置原理揭秘:从 @SpringBootApplication 到手写自定义 Starter
java·后端
人道领域3 小时前
【LeetCode刷题日记】47.全排列Ⅱ
java·开发语言·算法·leetcode
心软小念3 小时前
2026软件测试高频面试题
软件测试·面试·职场和发展