🔔 本文 5000+ 字深度原创,含完整代码示例和生产级落地方案。创作不易,如果对你有帮助,请点赞 👍 收藏 ⭐ 关注 🔥 三连支持,你的认可是我持续输出的最大动力!
本文是「设计模式实战解读」系列第七篇。系列文章统一按照 定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ 的结构展开,每篇聚焦一个模式讲透。
一句话定义
适配器模式(Adapter):将一个类的接口转换成调用方期望的另一个接口,使原本接口不兼容的类可以一起工作。
归属:结构型模式。
一、没有适配器时的痛点
假设你在做一个统一消息通知系统,需要对接多个第三方通知渠道:
java
public class NotificationService {
public void notify(String userId, String content) {
// 钉钉 SDK
DingTalkClient dingClient = new DingTalkClient();
dingClient.sendMarkdown(userId, "通知", content, false);
// 企业微信 SDK
WeCom wecom = new WeCom(corpId, secret);
wecom.message().send(new TextMessage(userId, content));
// 飞书 SDK
LarkSender lark = LarkSender.getInstance();
lark.pushText(LarkMsg.builder().openId(userId).text(content).build());
}
}
三个 SDK 的接口完全不一样:
| SDK | 发送方法 | 参数形式 | 返回值 |
|---|---|---|---|
| 钉钉 | sendMarkdown(userId, title, content, isAtAll) |
4 个散参 | void |
| 企微 | message().send(TextMessage) |
链式调用 + 包装对象 | SendResult |
| 飞书 | pushText(LarkMsg) |
Builder 对象 | Response |
问题:
- 调用方和具体 SDK 强耦合------换一个 SDK 版本,调用方要跟着改
- 无法统一处理------想加个"发送失败重试"的通用逻辑,要给三个 SDK 各写一遍
- 难以扩展------新加一个渠道(如 Slack),要在 NotificationService 里再加一坨代码
- 无法多态------三个 SDK 没有公共接口,无法用 List 统一遍历发送
这就是经典的"接口不兼容"问题------你的系统期望的是统一的发送接口,但第三方 SDK 各有各的设计。
二、模式结构
┌───────────────────────────────────┐
│ Target(目标接口) │
│ + send(userId, content): Result │ ← 调用方期望的统一接口
└──────────────────┬────────────────┘
│ 实现
↓
┌───────────────────────────────────┐
│ Adapter(适配器) │
│ - adaptee: DingTalkClient │ ← 持有被适配对象
│ + send(userId, content): Result │ ← 把调用转换成 adaptee 的格式
└───────────────────────────────────┘
│ 委托
↓
┌───────────────────────────────────┐
│ Adaptee(被适配者) │
│ + sendMarkdown(...) │ ← 原始的不兼容接口
└───────────────────────────────────┘
三个角色:
- Target(目标接口):调用方期望的标准接口
- Adaptee(被适配者):已有的、接口不兼容的类(通常是第三方 SDK 或旧系统)
- Adapter(适配器) :实现 Target 接口,内部持有 Adaptee,负责把 Target 的调用翻译成 Adaptee 能理解的调用
一句话:适配器就是"翻译官"------把外语翻译成你能听懂的话。
三、核心实现
3.1 对象适配器(推荐)
通过组合持有被适配对象:
java
// 目标接口:统一的消息发送能力
public interface MessageNotifier {
NotifyResult send(String userId, String content);
String channel(); // 标识渠道名
}
// 适配器1:适配钉钉 SDK
@Component
public class DingTalkNotifierAdapter implements MessageNotifier {
private final DingTalkClient dingTalkClient;
public DingTalkNotifierAdapter(DingTalkClient dingTalkClient) {
this.dingTalkClient = dingTalkClient;
}
@Override
public String channel() {
return "dingtalk";
}
@Override
public NotifyResult send(String userId, String content) {
try {
// 将统一接口的参数"翻译"成钉钉SDK的参数格式
dingTalkClient.sendMarkdown(userId, "系统通知", content, false);
return NotifyResult.success();
} catch (DingTalkException e) {
return NotifyResult.fail(e.getErrCode(), e.getErrMsg());
}
}
}
// 适配器2:适配企业微信 SDK
@Component
public class WeComNotifierAdapter implements MessageNotifier {
private final WeCom wecom;
public WeComNotifierAdapter(WeCom wecom) {
this.wecom = wecom;
}
@Override
public String channel() {
return "wecom";
}
@Override
public NotifyResult send(String userId, String content) {
try {
SendResult result = wecom.message().send(new TextMessage(userId, content));
return result.isSuccess()
? NotifyResult.success()
: NotifyResult.fail(result.getErrCode(), result.getErrMsg());
} catch (Exception e) {
return NotifyResult.fail("WECOM_ERROR", e.getMessage());
}
}
}
// 适配器3:适配飞书 SDK
@Component
public class LarkNotifierAdapter implements MessageNotifier {
private final LarkSender larkSender;
public LarkNotifierAdapter(LarkSender larkSender) {
this.larkSender = larkSender;
}
@Override
public String channel() {
return "lark";
}
@Override
public NotifyResult send(String userId, String content) {
try {
LarkMsg msg = LarkMsg.builder().openId(userId).text(content).build();
Response<Void> resp = larkSender.pushText(msg);
return resp.isSuccess()
? NotifyResult.success()
: NotifyResult.fail(String.valueOf(resp.getCode()), resp.getMsg());
} catch (Exception e) {
return NotifyResult.fail("LARK_ERROR", e.getMessage());
}
}
}
3.2 统一调用
java
@Service
public class NotificationService {
// Spring 自动注入所有适配器,构建渠道 Map
private final Map<String, MessageNotifier> notifierMap;
public NotificationService(List<MessageNotifier> notifiers) {
this.notifierMap = notifiers.stream()
.collect(Collectors.toMap(MessageNotifier::channel, Function.identity()));
}
public NotifyResult notify(String channel, String userId, String content) {
MessageNotifier notifier = notifierMap.get(channel);
if (notifier == null) {
throw new BizException("Unsupported channel: " + channel);
}
return notifier.send(userId, content);
}
// 群发所有渠道
public List<NotifyResult> notifyAll(String userId, String content) {
return notifierMap.values().stream()
.map(n -> n.send(userId, content))
.collect(Collectors.toList());
}
}
新增一个渠道(Slack),只需要加一个 SlackNotifierAdapter,实现 MessageNotifier 接口即可------Service 层零改动。
3.3 类适配器(了解即可)
通过继承被适配类,同时实现目标接口:
java
// 类适配器:继承 Adaptee + 实现 Target(Java 只能单继承,有局限)
public class DingTalkNotifierClassAdapter extends DingTalkClient implements MessageNotifier {
@Override
public String channel() {
return "dingtalk";
}
@Override
public NotifyResult send(String userId, String content) {
// 直接调用父类方法(继承来的)
this.sendMarkdown(userId, "通知", content, false);
return NotifyResult.success();
}
}
实际项目中很少用类适配器------Java 单继承限制太大,且对象适配器更灵活。
四、真实应用场景
4.1 框架级应用
| 框架 | 适配器 | 被适配者 → 目标接口 |
|---|---|---|
| Spring MVC | HandlerAdapter | 各种 Controller → 统一的处理器接口 |
| SLF4J | slf4j-log4j12 / logback-classic | Log4j/Logback → SLF4J API |
| Spring Security | UserDetailsService | 任意用户存储 → Spring Security 用户体系 |
| JDBC | Driver | 各数据库协议 → JDBC 标准接口 |
| Jackson | JsonSerializer / Deserializer | 自定义类型 → JSON 序列化格式 |
4.2 SLF4J------最经典的适配器架构
┌────────────┐ ┌──────────────────┐ ┌─────────────┐
│ 你的代码 │────→│ SLF4J API │←────│ slf4j-api │
│ log.info() │ │ (Target接口) │ │ (接口JAR) │
└────────────┘ └────────┬─────────┘ └─────────────┘
│ 适配
┌─────────────┼─────────────┐
↓ ↓ ↓
logback-classic slf4j-log4j12 slf4j-jdk14
(Logback适配器) (Log4j适配器) (JUL适配器)
│ │ │
↓ ↓ ↓
Logback引擎 Log4j引擎 JUL引擎
你的代码只依赖 SLF4J 接口,底层切换日志框架只需要换一个适配器 JAR------适配器模式的教科书案例。
4.3 业务场景
| 场景 | 被适配者 | 适配为 | 动因 |
|---|---|---|---|
| 老系统对接 | 老 SOAP 接口 | REST 接口 | 新系统只认 REST |
| 多渠道支付 | 各支付 SDK | 统一 PayChannel | 接口不一致 |
| 数据导入 | Excel/CSV/JSON | 统一 DataRow | 格式不同 |
| 消息推送 | 钉钉/企微/飞书 | 统一 Notifier | 接口不兼容 |
| 云存储 | 阿里OSS/S3/MinIO | 统一 FileStore | SDK 不一致 |
4.4 iPaaS 连接器的适配器体系
在 iPaaS 平台中,每个连接器就是一个"适配器"------将千差万别的第三方 API 适配为平台统一的执行接口:
java
// 平台统一的连接器执行接口(Target)
public interface ConnectorExecutor {
ExecuteResult execute(ConnectorRequest request);
String connectorType();
}
// 钉钉连接器适配器
@Component
public class DingTalkConnectorAdapter implements ConnectorExecutor {
@Override
public String connectorType() {
return "dingtalk";
}
@Override
public ExecuteResult execute(ConnectorRequest request) {
// 1. 从统一请求中提取参数
String action = request.getAction(); // "send_message" / "create_task"
Map<String, Object> params = request.getParams();
// 2. 翻译为钉钉 API 的格式
DingTalkApiRequest dingReq = DingTalkRequestBuilder.build(action, params);
// 3. 调用钉钉 API
DingTalkApiResponse dingResp = dingTalkHttpClient.call(dingReq);
// 4. 将钉钉响应翻译回统一格式
return ExecuteResult.builder()
.success(dingResp.getErrcode() == 0)
.data(dingResp.getBody())
.errorMsg(dingResp.getErrmsg())
.build();
}
}
// 企业微信连接器适配器
@Component
public class WeComConnectorAdapter implements ConnectorExecutor {
@Override
public String connectorType() {
return "wecom";
}
@Override
public ExecuteResult execute(ConnectorRequest request) {
// 相同的翻译逻辑,但对接企微的 API 格式
WeComRequest wecomReq = WeComRequestBuilder.build(
request.getAction(), request.getParams()
);
WeComResponse wecomResp = wecomClient.invoke(wecomReq);
return ExecuteResult.from(wecomResp);
}
}
iPaaS 平台对接 600+ 应用,每个连接器本质上就是一个适配器------将各种不兼容的第三方 API 翻译为平台统一的 ConnectorRequest/ExecuteResult 协议。
五、常见变种
5.1 双向适配器
同时实现两边的接口,可以在两个系统之间双向转换:
java
// 既能把 NewLogger 当 OldLogger 用,也能把 OldLogger 当 NewLogger 用
public class LoggerBidirectionalAdapter implements OldLogger, NewLogger {
private OldLogger oldLogger;
private NewLogger newLogger;
// 当作 OldLogger 使用时
@Override
public void writeLog(String msg) {
if (newLogger != null) {
newLogger.log(Level.INFO, msg, null);
}
}
// 当作 NewLogger 使用时
@Override
public void log(Level level, String msg, Throwable t) {
if (oldLogger != null) {
oldLogger.writeLog("[" + level + "] " + msg);
}
}
}
适用场景:新老系统并行期间,需要双向兼容。
5.2 默认适配器(缺省适配器)
当接口方法太多、但只想实现其中几个时,用一个抽象类提供空实现:
java
// 接口有 10 个方法
public interface EventListener {
void onStart(Event e);
void onPause(Event e);
void onResume(Event e);
void onStop(Event e);
void onError(Event e);
// ... 更多方法
}
// 默认适配器:所有方法空实现
public abstract class EventListenerAdapter implements EventListener {
@Override public void onStart(Event e) {}
@Override public void onPause(Event e) {}
@Override public void onResume(Event e) {}
@Override public void onStop(Event e) {}
@Override public void onError(Event e) {}
}
// 使用时只覆写关心的方法
public class MyListener extends EventListenerAdapter {
@Override
public void onError(Event e) {
alertService.send("发生异常: " + e.getMessage());
}
}
Spring 的 WebMvcConfigurerAdapter(已废弃)就是这个思路,现在 Java 8 接口默认方法取代了它。
5.3 适配器 + 工厂
用工厂根据类型选择合适的适配器:
java
@Component
public class NotifierAdapterFactory {
private final Map<String, MessageNotifier> adapterMap;
public NotifierAdapterFactory(List<MessageNotifier> adapters) {
this.adapterMap = adapters.stream()
.collect(Collectors.toMap(MessageNotifier::channel, Function.identity()));
}
public MessageNotifier getAdapter(String channel) {
MessageNotifier adapter = adapterMap.get(channel);
if (adapter == null) {
throw new BizException("No adapter found for channel: " + channel);
}
return adapter;
}
}
六、优缺点
| 优点 | 缺点 |
|---|---|
| 让不兼容的类协同工作 | 增加了一层间接性(适配层) |
| 复用已有类,不修改其代码 | 适配器过多时,系统结构变复杂 |
| 符合单一职责(适配逻辑集中在 Adapter) | 类适配器受 Java 单继承限制 |
| 符合开闭原则(新增适配器不改已有代码) | 过度使用适配器可能是接口设计有问题的信号 |
| 解耦调用方和第三方 SDK | 适配器本身可能变成"胖类"(转换逻辑复杂时) |
七、避坑指南
坑 1:适配器内做业务逻辑
java
// ❌ 适配器不应该包含业务逻辑
public class DingTalkNotifierAdapter implements MessageNotifier {
@Override
public NotifyResult send(String userId, String content) {
// 这些是业务逻辑,不该放在适配器里!
if (isNightTime()) {
return NotifyResult.skip("夜间免打扰");
}
content = sensitiveWordFilter(content); // 敏感词过滤
dingTalkClient.sendMarkdown(...);
}
}
// ✓ 适配器只做接口转换,业务逻辑在 Service 层
public class DingTalkNotifierAdapter implements MessageNotifier {
@Override
public NotifyResult send(String userId, String content) {
// 只做"翻译":统一参数 → 钉钉参数
dingTalkClient.sendMarkdown(userId, "通知", content, false);
return NotifyResult.success();
}
}
原则:适配器只负责"格式转换",不做"业务决策"。
坑 2:异常没有统一转换
java
// ❌ 适配器直接抛出 SDK 特有异常
public NotifyResult send(String userId, String content) {
dingTalkClient.sendMarkdown(...); // 抛 DingTalkException
// 调用方还要 catch DingTalkException,耦合没有解除
}
// ✓ 统一转换为平台异常
public NotifyResult send(String userId, String content) {
try {
dingTalkClient.sendMarkdown(...);
return NotifyResult.success();
} catch (DingTalkException e) {
// 翻译异常,对外暴露统一的错误模型
return NotifyResult.fail(e.getErrCode(), e.getErrMsg());
}
}
坑 3:适配器数据丢失
第三方 API 返回了 10 个字段,但你的 Target 接口只定义了 3 个字段------其他 7 个信息就丢了。
解法 :Target 接口设计时预留 Map<String, Object> extra 扩展字段,或用泛型支持扩展。
java
public class ExecuteResult {
private boolean success;
private Object data;
private String errorMsg;
private Map<String, Object> extra; // 预留扩展,放不进标准字段的信息
}
坑 4:适配器与 Adaptee 版本耦合
第三方 SDK 升级后方法签名变了,适配器编译失败。
解法:
- 适配器模块单独维护,不要散落在业务代码中
- 适配器内加版本判断或抽取
SdkVersion策略 - 用 Maven/Gradle 模块隔离适配器依赖
八、常见问题(FAQ)
Q:适配器模式和装饰器模式都是"包一层",有什么区别?
A:核心意图不同:
- 适配器 :接口不兼容,要把 A 接口转换成 B 接口------改变接口形态
- 装饰器 :接口相同,要给现有能力添加新功能------增强同一个接口
适配器的 wrapped 对象和 Target 接口不同 ;装饰器的 wrapped 对象和自身相同接口。
Q:适配器模式和外观模式(Facade)有什么区别?
A:
- 适配器:一对一转换(一个 Adaptee 适配为一个 Target)
- 外观:多对一封装(多个子系统组合为一个简洁接口)
外观模式侧重"简化复杂子系统的使用",适配器侧重"兼容不同接口"。
Q:什么时候该用适配器,什么时候该重新设计接口?
A:
- 用适配器:Adaptee 是你无法修改的(第三方 SDK、旧系统、已发布的 API)
- 重新设计:如果是自己团队的代码,且接口设计本身有问题,应该直接改接口,而不是堆适配器掩盖设计缺陷
Q:Spring MVC 的 HandlerAdapter 为什么叫适配器?
A:Spring MVC 支持多种 Controller 写法(@Controller 注解、实现 Controller 接口、实现 HttpRequestHandler 接口)。DispatcherServlet 需要一个统一的方式来调用它们------HandlerAdapter 就是适配器,把不同类型的 Controller 适配为统一的 handle(request, response, handler) 调用。
Q:一个适配器可以适配多个 Adaptee 吗?
A:可以,但不推荐。一个适配器适配多个 Adaptee 会导致适配器变成"胖类",违反单一职责。推荐一个 Adaptee 对应一个 Adapter。
九、小结
适配器模式的核心价值:让已有的、接口不兼容的代码能无缝接入你的系统,不修改任何一方的原始代码。
三个实践要点:
- 适配器只做"翻译",不做业务逻辑------格式转换、异常转换、字段映射,仅此而已
- 异常必须统一转换------不能让 SDK 特有异常穿透适配层泄露给调用方
- 优先用对象适配器(组合)而非类适配器(继承)------更灵活,不受单继承限制
下一篇我们聊代理模式------当你需要控制对一个对象的访问(权限校验、懒加载、远程调用、限流),而不想让调用方感知这层控制时,代理是最佳选择。
标签:#设计模式 #适配器模式 #Adapter #结构型模式 #Java #Spring #接口兼容 #第三方SDK #连接器适配 #SLF4J #iPaaS #面向对象 #软件工程