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智能调度实战》《电商多平台电子面单对接实战》。技术相通,思路可鉴。

相关推荐
维天说2 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json
Zane19943 小时前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端
黄敬峰3 小时前
从零搞懂 useState 与 Fragment:React 函数式组件的基石
面试
杨充4 小时前
11.DDD与战术建模
设计模式·开源·代码规范
杨充4 小时前
12.综合实战图片框架
设计模式·开源·代码规范
噢,我明白了4 小时前
java中Excel的导入和导出(EasyExcel)
java·开发语言·excel
吠品4 小时前
Zabbix Web界面误报Server未运行的排查与解决
java·服务器·数据库
啊湘4 小时前
天气查询API接口 按月Token鉴权 实时天气 物联网可用 文档齐全
java·后端·struts
Java内核笔记4 小时前
告别十亿美元的错误 : Spring Boot 4 空安全 (JSpecify) 实战
java·spring boot·后端
糖果店的幽灵5 小时前
langgraph的 MessagesState 解读
java·开发语言·人工智能·windows·langgraph