📌 PDF :AI人工智能 --- AI Agent智能体实战项目
大白话说Java设计模式-14-适配器模式(业务实战篇):大白商城多端 API 兼容的"翻译官"
📌 一句话本质:适配器模式就是"翻译官",让不兼容的接口能一起工作。
🏷️ 标签:适配器模式 / Java 设计模式 / 第三方对接 / Spring MVC / 大白商城 🎯 适合:初中级后端 / 经常对接第三方 API 的工程师
目录
- 一、业务场景引入:为什么需要"翻译官"?
- 二、反面教材:不兼容接口的"灾难现场"
- [三、模式原理:适配器的"两种实现 + 一张图"](#三、模式原理:适配器的"两种实现 + 一张图")
- 四、实战代码:大白商城第三方对接完整实现
- [五、3 种适配器写法对比](#五、3 种适配器写法对比)
- [六、适配器 vs 装饰器 / 代理 / 桥接](#六、适配器 vs 装饰器 / 代理 / 桥接)
- [七、Spring MVC 的 HandlerAdapter 怎么借鉴?](#七、Spring MVC 的 HandlerAdapter 怎么借鉴?)
- [八、工程决策 Checklist](#八、工程决策 Checklist)
- 九、与其他模式协作
- [十、本篇小结 + 下篇预告](#十、本篇小结 + 下篇预告)
一、业务场景引入:为什么需要"翻译官"?
大白商城 2025 年对接了 5 家物流公司(顺丰/中通/京东/韵达/邮政),踩了一个大坑。事情是这样的:
老板要求"订单发货后必须自动同步物流轨迹到商城"。我作为后端负责人,吭哧吭哧对接 5 家物流公司,写了 5 套完全不同的调用代码。
打开代码一看,心里凉了:
java
/**
* 大白商城老的"多物流公司"代码
*/
public class LogisticsService {
public void shipGoods(Long orderId, String company) {
if ("shunfeng".equals(company)) {
// 顺丰的接口
ShunFengClient sf = new ShunFengClient("app_key_xxx");
ShunFengRequest req = new ShunFengRequest();
req.setOrderNo("ORDER_" + orderId);
req.setCustomerName("大白商城");
ShunFengResponse resp = sf.createOrder(req);
System.out.println("顺丰单号: " + resp.getMailNo());
} else if ("zhongtong".equals(company)) {
// 中通的接口
ZTOClient zto = new ZTOClient("partner_id_xxx");
ZTORequest req = new ZTORequest();
req.setOrderId("ORDER_" + orderId);
req.setSendSite("杭州站点");
ZTOResponse resp = zto.submitOrder(req);
System.out.println("中通单号: " + resp.getOrderNo());
} else if ("jd".equals(company)) {
// 京东的接口
JDLogisticsClient jd = new JDLogisticsClient("token_xxx");
JDRequest req = new JDRequest();
req.setOrderId(orderId);
req.setSkuList(/* ... */);
JDResponse resp = jd.createOrder(req);
System.out.println("京东单号: " + resp.getWaybillNo());
} else if ("yunda".equals(company)) {
// 韵达的接口
// ... 又是一套完全不同的调用
} else if ("youzheng".equals(company)) {
// 邮政的接口
// ... 又是一套
}
}
}
这种代码的痛点:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 5 套不同的接口 | 每个物流公司调用方式都不同 |
| ② | if-else 堆叠 | 加一家物流公司要改 if-else |
| ③ | 业务代码被污染 | 看不到核心业务(发货) |
| ④ | 第三方接口变更 | 一家物流公司改接口,要全改 |
老板要的"加物流公司不动业务代码",怎么搞?
答案就是------适配器模式。
1.1 大白话讲透适配器
继续打比方:
场景:大白商城要和 5 家物流公司(顺丰/中通/京东/韵达/邮政)对接。每家公司的接口都不同(参数名、调用方式、返回格式都不同)。
- 错误做法:业务代码里写 5 个 if-else,每个分支调不同的接口
- 正确做法 :
- 大白商城定义统一的"物流接口" (
LogisticsAdapter)- 每家物流公司写一个**"翻译官"**(
ShunFengAdapter、ZTOAdapter、JDAdapter...)- 翻译官把"大白商城的统一请求"翻译成"物流公司能识别的请求"
- 业务代码只调统一接口,不管有多少家物流公司
适配器模式 = 把一个类的接口转换成客户希望的另一个接口,让原本不兼容的类能一起工作。
1.2 适配器模式的 3 个真实场景
大白商城里,适配器模式用在:
| 场景 | "被适配者" | "目标接口" |
|---|---|---|
| 多物流公司对接 | 顺丰/中通/京东各自的 API | 大白商城统一的物流接口 |
| 多支付渠道 | 支付宝/微信/银联 SDK | 大白商城统一的支付接口 |
| 多短信服务 | 阿里云/腾讯云/华为云短信 | 大白商城统一的短信接口 |
| 老系统对接 | 老系统 API | 新系统接口规范 |
| Spring MVC | 各种 Controller | HandlerAdapter 统一处理 |
任何"对接不兼容的第三方接口"的场景,都用适配器。
二、反面教材:不兼容接口的"灾难现场"
我们看 4 个反面教材,看它们是怎么一步步崩的。
2.1 反面教材 v1:业务代码直接调第三方
java
/**
* ❌ 反面教材 v1:业务代码直接调第三方
*/
public class LogisticsServiceV1 {
public void ship(Long orderId, String company) {
if ("shunfeng".equals(company)) {
// 直接调顺丰
new ShunFengClient("app_key").createOrder(/* 顺丰参数 */);
} else if ("zhongtong".equals(company)) {
// 直接调中通
new ZTOClient("partner_id").submitOrder(/* 中通参数 */);
}
// ... 每家一套
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 业务被污染 | 业务代码看不到核心逻辑 |
| ② | 加新公司难 | 改 if-else |
| ③ | 第三方变更易受影响 | 顺丰改 API,全改 |
2.2 反面教材 v2:包装一层但仍硬编码
java
/**
* ❌ 反面教材 v2:包装一层但仍硬编码
*/
public class LogisticsServiceV2 {
public void ship(Long orderId, String company) {
if ("shunfeng".equals(company)) {
// 即使包装了,仍然 if-else
ShunFengAdapter adapter = new ShunFengAdapter();
adapter.ship(orderId);
} else if ("zhongtong".equals(company)) {
ZTOAdapter adapter = new ZTOAdapter();
adapter.ship(orderId);
}
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 仍有 if-else | 换汤不换药 |
| ② | 业务不知道用哪个适配器 | 业务方要知道每家公司的存在 |
2.3 反面教材 v3:用 Map 但 Key 是字符串
java
/**
* ❌ 反面教材 v3:用 Map 但 Key 是字符串
*/
public class LogisticsServiceV3 {
private static final Map<String, LogisticsAdapter> ADAPTER_MAP = new HashMap<>();
static {
ADAPTER_MAP.put("shunfeng", new ShunFengAdapter());
ADAPTER_MAP.put("zhongtong", new ZTOAdapter());
}
public void ship(Long orderId, String company) {
LogisticsAdapter adapter = ADAPTER_MAP.get(company);
if (adapter == null) {
throw new IllegalArgumentException("不支持的物流公司: " + company);
}
adapter.ship(orderId);
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 字符串 Key 易错 | 写错 Key 编译能过运行崩 |
| ② | 加新公司要改 Map | 不算"开闭原则" |
| ③ | 业务方要知道公司编码 | 不够抽象 |
2.4 反面教材 v4:完全硬编码
java
/**
* ❌ 反面教材 v4:完全硬编码
*/
public class LogisticsServiceV4 {
public void ship(Long orderId) {
// 硬编码:永远只发顺丰
new ShunFengClient("app_key").createOrder(/* ... */);
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 绑死一家 | 客户想换物流公司就崩 |
| ② | 无法支持多公司 | 业务受限 |
2.5 4 个反面教材的共同病根
| 痛点 | 反模式方案能不能解决? |
|---|---|
| 业务不依赖具体公司 | ❌ 全部绑死 |
| 加新公司不动业务 | ❌ 全部要改 |
| 第三方变更不影响业务 | ❌ 全部要改业务 |
| 统一调用入口 | ❌ 全部散乱 |
必须上适配器模式。
三、模式原理:适配器的"两种实现 + 一张图"
3.1 适配器的 4 个核心角色
| 角色 | 职责 | 例子 |
|---|---|---|
| 目标接口(Target) | 客户期望的接口 | LogisticsAdapter |
| 被适配者(Adaptee) | 已有的、但接口不兼容的类 | ShunFengClient |
| 适配器(Adapter) | 把被适配者转换成目标接口 | ShunFengAdapter |
| 客户端(Client) | 使用目标接口 | LogisticsService |
关键点 :客户端只调目标接口,适配器负责"翻译"。
3.2 一张图看懂适配器
业务方(LogisticsService)
↓ 调用
目标接口(LogisticsAdapter)
↓
├─ ShunFengAdapter → ShunFengClient(顺丰)
├─ ZTOAdapter → ZTOClient(中通)
├─ JDAdapter → JDLogisticsClient(京东)
├─ YunDaAdapter → YunDaClient(韵达)
└─ YouZhengAdapter → YouZhengClient(邮政)
每个 Adapter 把"大白商城的统一请求"
翻译成"对应物流公司的请求格式"
3.3 适配器的"灵魂三问"
Q1:适配器 vs 装饰器,区别是什么?
答:
- 适配器 :改变接口(让不兼容的兼容),包装第三方
- 装饰器 :不改变接口(增强原有功能),包装自己
- 适配器侧重"翻译" ,装饰器侧重"增强"
Q2:类适配器 vs 对象适配器,怎么选?
答:
- 类适配器 :用继承(Adapter extends Adaptee),Java 单继承限制
- 对象适配器 :用组合(Adapter 持有 Adaptee),更灵活
- 优先用对象适配器(组合优于继承)
Q3:什么时候用适配器,什么时候用桥接?
答:
- 适配器 :已有类 (第三方)接口不兼容,事后补救
- 桥接 :设计时 就考虑抽象和实现分离,事前规划
- 适配器是"打补丁" ,桥接是"从零设计"
3.4 适配器的 3 种写法
| 写法 | 适用场景 | 缺点 |
|---|---|---|
| 类适配器(继承) | 简单场景 | Java 单继承限制 |
| 对象适配器(组合) | 通用场景 | 多一个对象 |
| 接口适配器(抽象类) | 多个方法只想实现部分 | 不适用于第三方 |
大白商城主推对象适配器(组合 + 灵活)。
四、实战代码:大白商城第三方对接完整实现
下面是大白商城生产环境在用的适配器实现,全套代码可直接复制到 IDEA 跑。
4.1 项目环境与依赖
pom.xml:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.dabai.mall</groupId>
<artifactId>mall-design-pattern-14</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>mall-design-pattern-14</name>
<description>大白商城 - 设计模式 14 适配器模式</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2 目标接口:LogisticsAdapter
java
package com.dabai.mall.logistics;
import java.math.BigDecimal;
/**
* ✅ 目标接口:大白商城统一物流接口
* <p>
* 所有物流公司的适配器都实现这个接口
*
* @author 大白商城技术团队
*/
public interface LogisticsAdapter {
/**
* ✅ 发货
*/
ShipResult ship(ShipRequest request);
/**
* ✅ 查询物流轨迹
*/
TrackResult track(String waybillNo);
/**
* ✅ 取消发货
*/
CancelResult cancel(String waybillNo);
/**
* ✅ 获取物流公司编码
*/
String getCompanyCode();
}
配套 DTO:
java
package com.dabai.mall.logistics;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 大白商城统一发货请求
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShipRequest {
private String orderNo;
private String receiverName;
private String receiverPhone;
private String receiverAddress;
private String senderName;
private String senderPhone;
private String senderAddress;
private BigDecimal weight;
private BigDecimal volume;
private String remark;
}
/**
* 大白商城统一发货结果
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class ShipResult {
private boolean success;
private String waybillNo; // 运单号
private String errorCode;
private String errorMessage;
}
/**
* 物流轨迹
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class TrackResult {
private boolean success;
private String waybillNo;
private java.util.List<TrackPoint> tracks;
private String errorCode;
}
/**
* 轨迹点
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class TrackPoint {
private String time;
private String location;
private String description;
}
/**
* 取消结果
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class CancelResult {
private boolean success;
private String errorCode;
private String errorMessage;
}
4.3 被适配者:3 个第三方物流客户端
ShunFengClient(顺丰):
java
package com.dabai.mall.logistics.thirdparty;
import lombok.extern.slf4j.Slf4j;
/**
* ✅ 被适配者:顺丰 SDK(模拟)
* <p>
* 这是第三方 SDK,我们不能改它的接口
*/
@Slf4j
public class ShunFengClient {
private final String appKey;
public ShunFengClient(String appKey) {
this.appKey = appKey;
}
/**
* 顺丰的接口:参数和返回都和大白商城不一样
*/
public ShunFengResponse createOrder(ShunFengRequest req) {
log.info("【顺丰】创建订单, appKey={}, orderNo={}", appKey, req.getOrderNo());
// 实际调用顺丰 API
return new ShunFengResponse("SF" + System.currentTimeMillis(), "success");
}
/**
* 顺丰的轨迹查询
*/
public ShunFengTrackResponse queryRoute(String mailNo) {
log.info("【顺丰】查询轨迹, mailNo={}", mailNo);
return new ShunFengTrackResponse(mailNo, "已揽收", "2026-07-25 10:00:00");
}
}
/**
* 顺丰请求(参数名都不一样)
*/
class ShunFengRequest {
private String orderNo; // 大白商城叫 orderNo
private String customerName; // 顺丰叫 customerName
private String customerPhone; // 顺丰叫 customerPhone
private String address; // 顺丰叫 address(不区分收发)
private Double weight; // 顺丰是 Double
public String getOrderNo() { return orderNo; }
public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
public String getCustomerName() { return customerName; }
public void setCustomerName(String customerName) { this.customerName = customerName; }
public String getCustomerPhone() { return customerPhone; }
public void setCustomerPhone(String customerPhone) { this.customerPhone = customerPhone; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public Double getWeight() { return weight; }
public void setWeight(Double weight) { this.weight = weight; }
}
/**
* 顺丰响应
*/
class ShunFengResponse {
private String mailNo; // 顺丰叫 mailNo(运单号)
private String resultCode; // 顺丰叫 resultCode
public ShunFengResponse(String mailNo, String resultCode) {
this.mailNo = mailNo;
this.resultCode = resultCode;
}
public String getMailNo() { return mailNo; }
public String getResultCode() { return resultCode; }
}
class ShunFengTrackResponse {
private String mailNo;
private String routeInfo;
private String scanTime;
public ShunFengTrackResponse(String mailNo, String routeInfo, String scanTime) {
this.mailNo = mailNo;
this.routeInfo = routeInfo;
this.scanTime = scanTime;
}
public String getMailNo() { return mailNo; }
public String getRouteInfo() { return routeInfo; }
public String getScanTime() { return scanTime; }
}
ZTOClient(中通):
java
package com.dabai.mall.logistics.thirdparty;
import lombok.extern.slf4j.Slf4j;
/**
* ✅ 被适配者:中通 SDK(模拟)
*/
@Slf4j
public class ZTOClient {
private final String partnerId;
public ZTOClient(String partnerId) {
this.partnerId = partnerId;
}
/**
* 中通的接口:参数和返回又不一样
*/
public ZTOResponse submitOrder(ZTORequest req) {
log.info("【中通】提交订单, partnerId={}, orderId={}", partnerId, req.getOrderId());
return new ZTOResponse("ZTO" + System.currentTimeMillis(), true);
}
public ZTOTrackResponse getRoute(String orderNo) {
log.info("【中通】查询轨迹, orderNo={}", orderNo);
return new ZTOTrackResponse(orderNo, "已揽收", "2026-07-25 11:00:00");
}
}
class ZTORequest {
private String orderId; // 中通叫 orderId
private String sendSite; // 中通叫 sendSite
private String receiveName;
private String receivePhone;
private String receiveAddress;
private Double weight;
public String getOrderId() { return orderId; }
public void setOrderId(String orderId) { this.orderId = orderId; }
public String getSendSite() { return sendSite; }
public void setSendSite(String sendSite) { this.sendSite = sendSite; }
public String getReceiveName() { return receiveName; }
public void setReceiveName(String receiveName) { this.receiveName = receiveName; }
public String getReceivePhone() { return receivePhone; }
public void setReceivePhone(String receivePhone) { this.receivePhone = receivePhone; }
public String getReceiveAddress() { return receiveAddress; }
public void setReceiveAddress(String receiveAddress) { this.receiveAddress = receiveAddress; }
public Double getWeight() { return weight; }
public void setWeight(Double weight) { this.weight = weight; }
}
class ZTOResponse {
private String orderNo;
private boolean success;
public ZTOResponse(String orderNo, boolean success) {
this.orderNo = orderNo;
this.success = success;
}
public String getOrderNo() { return orderNo; }
public boolean isSuccess() { return success; }
}
class ZTOTrackResponse {
private String orderNo;
private String status;
private String scanTime;
public ZTOTrackResponse(String orderNo, String status, String scanTime) {
this.orderNo = orderNo;
this.status = status;
this.scanTime = scanTime;
}
public String getOrderNo() { return orderNo; }
public String getStatus() { return status; }
public String getScanTime() { return scanTime; }
}
JDLogisticsClient(京东):
java
package com.dabai.mall.logistics.thirdparty;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class JDLogisticsClient {
private final String token;
public JDLogisticsClient(String token) {
this.token = token;
}
public JDResponse createOrder(JDRequest req) {
log.info("【京东】创建订单, token={}, orderId={}", token, req.getOrderId());
return new JDResponse("JD" + System.currentTimeMillis(), 0);
}
public String queryTrack(String waybillNo) {
log.info("【京东】查询轨迹, waybillNo={}", waybillNo);
return "京东轨迹: 已揽收";
}
}
class JDRequest {
private Long orderId;
private String receiverName;
private String receiverPhone;
private String receiverAddress;
private Double weight;
public Long getOrderId() { return orderId; }
public void setOrderId(Long orderId) { this.orderId = orderId; }
public String getReceiverName() { return receiverName; }
public void setReceiverName(String receiverName) { this.receiverName = receiverName; }
public String getReceiverPhone() { return receiverPhone; }
public void setReceiverPhone(String receiverPhone) { this.receiverPhone = receiverPhone; }
public String getReceiverAddress() { return receiverAddress; }
public void setReceiverAddress(String receiverAddress) { this.receiverAddress = receiverAddress; }
public Double getWeight() { return weight; }
public void setWeight(Double weight) { this.weight = weight; }
}
class JDResponse {
private String waybillNo;
private Integer code;
public JDResponse(String waybillNo, Integer code) {
this.waybillNo = waybillNo;
this.code = code;
}
public String getWaybillNo() { return waybillNo; }
public Integer getCode() { return code; }
}
4.4 适配器实现:3 个 Adapter
ShunFengAdapter(顺丰适配器):
java
package com.dabai.mall.logistics.adapter;
import com.dabai.mall.logistics.*;
import com.dabai.mall.logistics.thirdparty.*;
/**
* ✅ 适配器:顺丰适配器(对象适配器)
* <p>
* 核心:把大白商城的统一请求"翻译"成顺丰的请求
*
* @author 大白商城技术团队
*/
public class ShunFengAdapter implements LogisticsAdapter {
private final ShunFengClient shunFengClient;
public ShunFengAdapter(String appKey) {
this.shunFengClient = new ShunFengClient(appKey);
}
@Override
public ShipResult ship(ShipRequest request) {
// ✅ 翻译:统一请求 → 顺丰请求
ShunFengRequest sfReq = new ShunFengRequest();
sfReq.setOrderNo(request.getOrderNo());
sfReq.setCustomerName(request.getReceiverName());
sfReq.setCustomerPhone(request.getReceiverPhone());
sfReq.setAddress(request.getReceiverAddress());
sfReq.setWeight(request.getWeight() == null ? null : request.getWeight().doubleValue());
// 调用顺丰 SDK
ShunFengResponse sfResp = shunFengClient.createOrder(sfReq);
// ✅ 翻译:顺丰响应 → 统一响应
return ShipResult.builder()
.success("success".equals(sfResp.getResultCode()))
.waybillNo(sfResp.getMailNo())
.build();
}
@Override
public TrackResult track(String waybillNo) {
ShunFengTrackResponse sfResp = shunFengClient.queryRoute(waybillNo);
// 翻译轨迹
return TrackResult.builder()
.success(true)
.waybillNo(sfResp.getMailNo())
.tracks(java.util.List.of(
TrackPoint.builder()
.time(sfResp.getScanTime())
.location("未知")
.description(sfResp.getRouteInfo())
.build()
))
.build();
}
@Override
public CancelResult cancel(String waybillNo) {
// 顺丰的取消接口(模拟)
return CancelResult.builder().success(true).build();
}
@Override
public String getCompanyCode() {
return "shunfeng";
}
}
ZTOAdapter(中通适配器):
java
package com.dabai.mall.logistics.adapter;
import com.dabai.mall.logistics.*;
import com.dabai.mall.logistics.thirdparty.*;
/**
* ✅ 适配器:中通适配器
*/
public class ZTOAdapter implements LogisticsAdapter {
private final ZTOClient ztoClient;
public ZTOAdapter(String partnerId) {
this.ztoClient = new ZTOClient(partnerId);
}
@Override
public ShipResult ship(ShipRequest request) {
// ✅ 翻译:统一 → 中通
ZTORequest ztoReq = new ZTORequest();
ztoReq.setOrderId(request.getOrderNo());
ztoReq.setSendSite("杭州站点");
ztoReq.setReceiveName(request.getReceiverName());
ztoReq.setReceivePhone(request.getReceiverPhone());
ztoReq.setReceiveAddress(request.getReceiverAddress());
ztoReq.setWeight(request.getWeight() == null ? null : request.getWeight().doubleValue());
ZTOResponse ztoResp = ztoClient.submitOrder(ztoReq);
// ✅ 翻译:中通 → 统一
return ShipResult.builder()
.success(ztoResp.isSuccess())
.waybillNo(ztoResp.getOrderNo())
.build();
}
@Override
public TrackResult track(String waybillNo) {
ZTOTrackResponse ztoResp = ztoClient.getRoute(waybillNo);
return TrackResult.builder()
.success(true)
.waybillNo(ztoResp.getOrderNo())
.tracks(java.util.List.of(
TrackPoint.builder()
.time(ztoResp.getScanTime())
.location("未知")
.description(ztoResp.getStatus())
.build()
))
.build();
}
@Override
public CancelResult cancel(String waybillNo) {
return CancelResult.builder().success(true).build();
}
@Override
public String getCompanyCode() {
return "zhongtong";
}
}
JDAdapter(京东适配器):
java
package com.dabai.mall.logistics.adapter;
import com.dabai.mall.logistics.*;
import com.dabai.mall.logistics.thirdparty.*;
/**
* ✅ 适配器:京东适配器
*/
public class JDAdapter implements LogisticsAdapter {
private final JDLogisticsClient jdClient;
public JDAdapter(String token) {
this.jdClient = new JDLogisticsClient(token);
}
@Override
public ShipResult ship(ShipRequest request) {
// ✅ 翻译:统一 → 京东
JDRequest jdReq = new JDRequest();
jdReq.setOrderId(Long.parseLong(request.getOrderNo().replace("ORDER_", "")));
jdReq.setReceiverName(request.getReceiverName());
jdReq.setReceiverPhone(request.getReceiverPhone());
jdReq.setReceiverAddress(request.getReceiverAddress());
jdReq.setWeight(request.getWeight() == null ? null : request.getWeight().doubleValue());
JDResponse jdResp = jdClient.createOrder(jdReq);
// ✅ 翻译:京东 → 统一
return ShipResult.builder()
.success(jdResp.getCode() == 0)
.waybillNo(jdResp.getWaybillNo())
.build();
}
@Override
public TrackResult track(String waybillNo) {
String trackInfo = jdClient.queryTrack(waybillNo);
return TrackResult.builder()
.success(true)
.waybillNo(waybillNo)
.tracks(java.util.List.of(
TrackPoint.builder()
.time("2026-07-25 12:00:00")
.location("未知")
.description(trackInfo)
.build()
))
.build();
}
@Override
public CancelResult cancel(String waybillNo) {
return CancelResult.builder().success(true).build();
}
@Override
public String getCompanyCode() {
return "jd";
}
}
4.5 业务调用方:LogisticsService
java
package com.dabai.mall.logistics;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* ✅ 物流服务:适配器的调用方
* <p>
* 业务代码完全干净,只调统一接口
*
* @author 大白商城技术团队
*/
@Service
public class LogisticsService {
/**
* 公司编码 → 适配器
*/
private final Map<String, LogisticsAdapter> adapterMap;
@Autowired
public LogisticsService(List<LogisticsAdapter> adapters) {
this.adapterMap = adapters.stream()
.collect(Collectors.toMap(LogisticsAdapter::getCompanyCode, a -> a));
log.info("【LogisticsService】注册 {} 个物流适配器: {}",
adapterMap.size(), adapterMap.keySet());
}
/**
* 发货
*/
public ShipResult ship(ShipRequest request) {
// 关键:业务方只调统一接口
// 找到对应的适配器(业务方不需要知道是哪家)
LogisticsAdapter adapter = adapterMap.get(request.getLogisticsCompany());
if (adapter == null) {
throw new BusinessException("不支持的物流公司: " + request.getLogisticsCompany());
}
return adapter.ship(request);
}
/**
* 查询轨迹
*/
public TrackResult track(String waybillNo, String company) {
LogisticsAdapter adapter = adapterMap.get(company);
if (adapter == null) {
throw new BusinessException("不支持的物流公司: " + company);
}
return adapter.track(waybillNo);
}
}
关键点:
| 关键点 | 解释 |
|---|---|
List<LogisticsAdapter> |
Spring 自动注入所有实现 |
Map<String, LogisticsAdapter> |
按公司编码索引 |
业务代码只调 adapter.ship() |
完全不知道是哪家公司 |
加新物流公司 3 步走:
第 1 步:新建 XAdapter implements LogisticsAdapter
第 2 步:把 XAdapter 标 @Component
第 3 步:完事。LogisticsService 一行不改。
4.6 单元测试
java
package com.dabai.mall.logistics;
import com.dabai.mall.logistics.adapter.JDAdapter;
import com.dabai.mall.logistics.adapter.ShunFengAdapter;
import com.dabai.mall.logistics.adapter.ZTOAdapter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* 适配器完整单元测试
*/
class LogisticsAdapterTest {
private LogisticsService logisticsService;
@BeforeEach
void setUp() {
// 注册 3 个适配器
logisticsService = new LogisticsService(List.of(
new ShunFengAdapter("sf_app_key"),
new ZTOAdapter("zto_partner_id"),
new JDAdapter("jd_token")
));
}
@Test
void testShunFengShip() {
ShipRequest request = ShipRequest.builder()
.orderNo("ORDER_001")
.receiverName("张三")
.receiverPhone("13800138000")
.receiverAddress("北京市朝阳区")
.weight(new BigDecimal("2.5"))
.logisticsCompany("shunfeng")
.build();
ShipResult result = logisticsService.ship(request);
assertTrue(result.isSuccess());
assertNotNull(result.getWaybillNo());
assertTrue(result.getWaybillNo().startsWith("SF"));
}
@Test
void testZTOShip() {
ShipRequest request = ShipRequest.builder()
.orderNo("ORDER_002")
.receiverName("李四")
.receiverPhone("13800138001")
.receiverAddress("上海市浦东")
.weight(new BigDecimal("1.0"))
.logisticsCompany("zhongtong")
.build();
ShipResult result = logisticsService.ship(request);
assertTrue(result.isSuccess());
assertTrue(result.getWaybillNo().startsWith("ZTO"));
}
@Test
void testJDShip() {
ShipRequest request = ShipRequest.builder()
.orderNo("ORDER_003")
.receiverName("王五")
.receiverPhone("13800138002")
.receiverAddress("广州市天河")
.weight(new BigDecimal("3.0"))
.logisticsCompany("jd")
.build();
ShipResult result = logisticsService.ship(request);
assertTrue(result.isSuccess());
assertTrue(result.getWaybillNo().startsWith("JD"));
}
@Test
void testUnsupportedCompany() {
ShipRequest request = ShipRequest.builder()
.orderNo("ORDER_004")
.receiverName("测试")
.receiverPhone("13800138000")
.receiverAddress("测试地址")
.logisticsCompany("unsupported")
.build();
assertThrows(BusinessException.class, () -> logisticsService.ship(request));
}
@Test
void testTrack() {
ShipRequest request = ShipRequest.builder()
.orderNo("ORDER_005")
.receiverName("测试")
.logisticsCompany("shunfeng")
.build();
ShipResult shipResult = logisticsService.ship(request);
TrackResult trackResult = logisticsService.track(shipResult.getWaybillNo(), "shunfeng");
assertNotNull(trackResult);
assertNotNull(trackResult.getTracks());
assertFalse(trackResult.getTracks().isEmpty());
}
}
五、3 种适配器写法对比
5.1 类适配器(继承)
java
/**
* ❌ 类适配器:Adapter extends Adaptee
* <p>
* 问题:Java 单继承,Adapter 不能再继承其他
*/
public class ShunFengClassAdapter extends ShunFengClient implements LogisticsAdapter {
public ShunFengClassAdapter() {
super("app_key");
}
@Override
public ShipResult ship(ShipRequest request) {
// 直接调父类(继承)
ShunFengRequest sfReq = new ShunFengRequest();
// ...
ShunFengResponse sfResp = super.createOrder(sfReq);
// ...
}
@Override
public TrackResult track(String waybillNo) {
return null;
}
@Override
public CancelResult cancel(String waybillNo) {
return null;
}
@Override
public String getCompanyCode() {
return "shunfeng";
}
}
问题:
| 问题 | 解释 |
|---|---|
| 单继承 | Adapter 不能再继承其他 |
| 紧耦合 | Adapter 强依赖 Adaptee |
| 难测试 | Adaptee 的逻辑也跟着走 |
5.2 对象适配器(组合)✅ 推荐
java
/**
* ✅ 对象适配器:Adapter 持有 Adaptee
* <p>
* 灵活、推荐
*/
public class ShunFengAdapter implements LogisticsAdapter {
private final ShunFengClient shunFengClient; // 组合
public ShunFengAdapter(String appKey) {
this.shunFengClient = new ShunFengClient(appKey);
}
@Override
public ShipResult ship(ShipRequest request) {
// 调持有的 Adaptee
ShunFengResponse sfResp = shunFengClient.createOrder(sfReq);
// ...
}
}
优势:
| 优势 | 解释 |
|---|---|
| 灵活 | 可以组合任意 Adaptee |
| 弱耦合 | Adapter 和 Adaptee 通过接口组合 |
| 易测试 | Adaptee 可以 Mock |
5.3 接口适配器(抽象类)
java
/**
* ✅ 接口适配器:当目标接口有很多方法时,只关心部分方法
*/
public abstract class AbstractLogisticsAdapter implements LogisticsAdapter {
// 把 LogisticsAdapter 的方法都空实现
@Override
public ShipResult ship(ShipRequest request) {
throw new UnsupportedOperationException("ship not supported");
}
@Override
public TrackResult track(String waybillNo) {
throw new UnsupportedOperationException("track not supported");
}
@Override
public CancelResult cancel(String waybillNo) {
throw new UnsupportedOperationException("cancel not supported");
}
@Override
public String getCompanyCode() {
return "abstract";
}
}
// 只想实现 ship 方法
public class OnlyShipAdapter extends AbstractLogisticsAdapter {
@Override
public ShipResult ship(ShipRequest request) {
// 只实现这个方法,其他继承空实现
return new ShipResult();
}
}
适用场景:
| 场景 | 适用 | 原因 |
|---|---|---|
| 目标接口有 10+ 方法 | ✅ | 避免空实现所有方法 |
| 只想实现部分方法 | ✅ | 抽象类提供空实现 |
| 多 Adapter 共享部分逻辑 | ✅ | 抽象类抽取公共代码 |
5.4 3 种写法对比表
| 维度 | 类适配器 | 对象适配器 | 接口适配器 |
|---|---|---|---|
| 实现方式 | 继承 | 组合 | 抽象类 + 继承 |
| 灵活性 | 差 | 高 | 中 |
| 紧耦合 | 高 | 低 | 中 |
| 适用场景 | 简单 | 通用 | 多个方法部分实现 |
| 推荐度 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
大白商城主推对象适配器。
六、适配器 vs 装饰器 / 代理 / 桥接
6.1 4 个模式对比
| 模式 | 关注点 | 是否改变接口 | 是否包装自己 | 适用场景 |
|---|---|---|---|---|
| 适配器 | 翻译接口 | ✅ 改变 | ❌ 包装第三方 | 对接不兼容接口 |
| 装饰器 | 增强功能 | ❌ 不变 | ✅ 包装自己 | 动态添加职责 |
| 代理 | 控制访问 | ❌ 不变 | ✅ 包装自己 | 权限/远程/延迟 |
| 桥接 | 抽象分离 | ✅ 改变 | ❌ 拆分实现 | 抽象和实现分离 |
6.2 一个例子区分 4 个模式
大白商城订单:
| 模式 | 例子 |
|---|---|
| 适配器 | 把顺丰/中通/京东 API 包装成统一物流接口 |
| 装饰器 | 给订单 VO 包装价格计算(满减/折扣/运费) |
| 代理 | Spring AOP 给 Service 包装日志/权限/事务 |
| 桥接 | 订单状态机把"状态"和"行为"分离 |
6.3 决策树
你包装的是"第三方类"还是"自己的类"?
├── 第三方类
│ └── ✅ 适配器
└── 自己的类
├── 改变接口?
│ ├── 是 → ✅ 桥接
│ └── 否
│ ├── 增强功能?
│ │ ├── 是 → ✅ 装饰器
│ │ └── 否
│ │ └── 控制访问?
│ │ ├── 是 → ✅ 代理
│ │ └── 否 → ❌ 不用模式
七、Spring MVC 的 HandlerAdapter 怎么借鉴?
7.1 HandlerAdapter 是什么?
Spring MVC 处理请求的"翻译官":
HTTP 请求
↓
DispatcherServlet
↓
HandlerAdapter(适配器)
↓
- RequestMappingHandlerAdapter:处理 @RequestMapping
- HttpRequestHandlerAdapter:处理 HttpRequestHandler
- SimpleControllerHandlerAdapter:处理 Controller 接口
- SimpleServletHandlerAdapter:处理 Servlet
每个 HandlerAdapter 都是"翻译官"------把 HTTP 请求翻译成不同类型的 Controller 能处理的形式。
7.2 完整流程
HTTP 请求 → DispatcherServlet
↓
遍历所有 HandlerAdapter
↓
找到 supports() 返回 true 的 Adapter
↓
调用 handler()
↓
执行真正的 Controller 方法
↓
返回 ModelAndView
7.3 大白商城的"抄作业"
java
/**
* 借鉴 Spring HandlerAdapter:统一消息处理
*/
public interface MessageHandlerAdapter<T> {
/**
* 是否支持该消息类型
*/
boolean supports(Object message);
/**
* 处理消息
*/
Object handle(T message);
}
/**
* 文本消息处理
*/
@Component
public class TextMessageHandler implements MessageHandlerAdapter<String> {
@Override
public boolean supports(Object message) {
return message instanceof String;
}
@Override
public Object handle(String message) {
return "处理文本: " + message;
}
}
/**
* 订单消息处理
*/
@Component
public class OrderMessageHandler implements MessageHandlerAdapter<Order> {
@Override
public boolean supports(Object message) {
return message instanceof Order;
}
@Override
public Object handle(Order message) {
return "处理订单: " + message.getId();
}
}
调用方:
java
@Service
public class MessageDispatcher {
private final List<MessageHandlerAdapter<?>> adapters;
public MessageDispatcher(List<MessageHandlerAdapter<?>> adapters) {
this.adapters = adapters;
}
public Object dispatch(Object message) {
return adapters.stream()
.filter(adapter -> adapter.supports(message))
.findFirst()
.map(adapter -> adapter.handle(message))
.orElseThrow(() -> new RuntimeException("无处理器: " + message.getClass()));
}
}
收益:
| 优化点 | 效果 |
|---|---|
| 统一调用 | 业务方只调 dispatch(message) |
| 自动选择 | 根据消息类型自动选 Adapter |
| 加新类型 | 新建 Adapter,dispatcher 不动 |
八、工程决策 Checklist
8.1 ✅ 这 5 种情况,强烈建议用适配器
| 序号 | 场景 | 原因 |
|---|---|---|
| ① | 对接第三方 API | 接口不兼容 |
| ② | 老系统集成 | 老接口无法改 |
| ③ | 多渠道统一 | 物流/支付/短信 |
| ④ | 统一消息处理 | 不同类型消息不同处理 |
| ⑤ | Spring MVC HandlerAdapter | 标准做法 |
8.2 ❌ 这 5 种情况,绝对不要用适配器
| 序号 | 场景 | 原因 |
|---|---|---|
| ① | 可以改第三方代码 | 直接改更简单 |
| ② | 只有 1 个实现 | 不需要适配 |
| ③ | 追求极致性能 | 适配器有调用开销 |
| ④ | 接口差异小 | 直接重写更简单 |
| ⑤ | 业务逻辑复杂 | 适配器只翻译不加工 |
8.3 ⚠️ 适配器的 6 大常见坑
| 序号 | 坑 | 表现 | 解决方案 |
|---|---|---|---|
| ① | 适配器过多 | 100 个 Adapter 类 | 抽象公共逻辑 |
| ② | 翻译逻辑错误 | 字段映射错 | 单元测试覆盖 |
| ③ | 异常被吃掉 | 第三方异常丢失 | 统一异常处理 |
| ④ | 线程不安全 | Adapter 持有可变状态 | Adapter 不可变 |
| ⑤ | 性能损耗 | 适配器有调用开销 | 缓存适配器 |
| ⑥ | 不支持新公司 | if-else 没写 | Spring 自动注入 |
8.4 面试官视角:适配器高频追问
Q1:适配器 vs 装饰器,区别是什么?
答:适配器改变接口 (翻译),包装第三方 ;装饰器不改变接口 (增强),包装自己。适配器侧重"兼容",装饰器侧重"增强"。
Q2:类适配器 vs 对象适配器,怎么选?
答:优先对象适配器 (组合)。类适配器受 Java 单继承限制,紧耦合。组合优于继承。
Q3:Spring MVC 的 HandlerAdapter 怎么工作的?
答:每个 HandlerAdapter 是"翻译官",把 HTTP 请求翻译成不同 Controller 能处理的形式。DispatcherServlet 遍历所有 Adapter,找到 supports() 为 true 的那个,调用 handle()。
九、与其他模式协作
9.1 适配器 + 工厂方法 = 工厂创建适配器
java
/**
* 适配器 + 工厂:按公司编码创建适配器
*/
public class LogisticsAdapterFactory {
public static LogisticsAdapter create(String companyCode) {
return switch (companyCode) {
case "shunfeng" -> new ShunFengAdapter("app_key");
case "zhongtong" -> new ZTOAdapter("partner_id");
case "jd" -> new JDAdapter("token");
default -> throw new IllegalArgumentException("不支持: " + companyCode);
};
}
}
9.2 适配器 + 策略 = 多渠道动态切换
java
/**
* 适配器 + 策略:物流策略
*/
@Service
public class LogisticsStrategy {
private final Map<String, LogisticsAdapter> adapterMap;
public LogisticsStrategy(List<LogisticsAdapter> adapters) {
this.adapterMap = adapters.stream()
.collect(Collectors.toMap(LogisticsAdapter::getCompanyCode, a -> a));
}
public LogisticsAdapter getStrategy(String company) {
return adapterMap.get(company);
}
}
9.3 适配器 + 外观 = 统一门面
java
/**
* 适配器 + 外观:物流门面
*/
@Service
public class LogisticsFacade {
@Autowired
private LogisticsService logisticsService;
/**
* 统一发货
*/
public ShipResult ship(Order order) {
// 把订单转换为发货请求
ShipRequest request = ShipRequest.builder()
.orderNo(order.getOrderNo())
.receiverName(order.getReceiverName())
// ...
.build();
return logisticsService.ship(request);
}
}
9.4 大白商城模式协作全景图
┌──────────────┐
│ 适配器 │ ← 本篇
└──────┬───────┘
│
┌───────────┬───────┼───────┬───────────┐
│ │ │ │ │
┌───▼───┐ ┌────▼───┐ ┌▼────┐ ┌▼─────┐ ┌───▼────┐
│工厂方法│ │ 策略 │ │外观 │ │装饰器│ │ 桥接 │
│(创建) │ │(切换) │ │(门面)│ │(增强)│ │(分离) │
└───────┘ └────────┘ └──────┘ └──────┘ └───────┘
04 篇 25 篇 17 篇 15 篇 21 篇
十、本篇小结 + 下篇预告
10.1 本篇小结(5 个核心要点)
- 本质 :适配器 = 翻译官,让不兼容接口能一起工作,对老代码最友好。
- 场景 :第三方对接 / 老系统集成 / 多渠道统一,都该用适配器。
- 对象适配器 :组合优于继承,首选。
- Spring MVC:HandlerAdapter 是适配器模式的工业级实现。
- 避坑:适配器过多 / 翻译逻辑错误 / 异常被吃掉。
10.2 一句话总结
适配器不是"换种方式调方法",是"在不动第三方代码的前提下让它们兼容"。大白商城 5 家物流公司对接,从 5 套 if-else 变成 5 个 Adapter + 1 个统一接口------业务代码完全不知道有几家物流公司。
10.3 知识脑图
适配器模式
├── 4 大角色
│ ├── 目标接口(LogisticsAdapter)
│ ├── 被适配者(顺丰/中通/京东 SDK)
│ ├── 适配器(ShunFengAdapter / ZTOAdapter / JDAdapter)
│ └── 客户端(LogisticsService)
├── 3 种写法
│ ├── 类适配器(继承)❌
│ ├── 对象适配器(组合)✅
│ └── 接口适配器(抽象类)⚠️
├── 模式对比
│ ├── vs 装饰器(翻译 vs 增强)
│ ├── vs 代理(翻译 vs 控制)
│ └── vs 桥接(翻译 vs 分离)
├── Spring HandlerAdapter
│ ├── RequestMappingHandlerAdapter
│ ├── HttpRequestHandlerAdapter
│ └── SimpleControllerHandlerAdapter
└── 模式协作
├── + 工厂方法(创建)
├── + 策略(动态切换)
└── + 外观(统一门面)
10.4 下篇预告
第 15 篇【适配器模式 - 源码剖析篇】:JDK / Spring 中的适配器实现
下一篇我们会深入源码,回答三个问题:
- JDK 的
InputStreamReader/OutputStreamWriter怎么用适配器包装字节流和字符流? - Spring MVC 的
HandlerAdapter怎么实现"多 Controller 统一处理"? - MyBatis 的
Log接口怎么用适配器兼容多种日志框架?
并附完整的源码解读 + 流程图 + 大白商城的"抄作业"实践。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯