一、生活场景引入(5秒理解概念)
1️⃣ 手机充电器:万能转换头的秘密
想象你带着中国插头的手机到英国旅行------两国的插座形状完全不同!这时一个转换头(Adapter)就能让手机充电器适配英国插座,整个过程:
- 旧设备:中国标准插头(需要三方接口)
- 新插座:英国标准接口(现有系统规范)
- 转换头:把中国插头转换成英国插座兼容的形态
这就是适配器的本质:在不修改原有插头和插座的情况下,通过中间层解决兼容问题。
2️⃣ 翻译官:跨语言沟通的桥梁
当中国工程师与美国客户开会时,双方语言不通。翻译官(Adapter)的介入:
- 中方说中文 → 翻译官接收 → 转译为英文 → 美方理解
- 美方说英文 → 翻译官接收 → 转译为中文 → 中方理解
技术映射:两个系统使用不同数据格式时,适配器就像翻译官,在中间做协议转换。
3️⃣ 技术世界的接口冲突(Mermaid图表解读)
开发者常遇到的三大接口冲突场景:
具体场景解释:
- 第三方库接口不对 (45%)
👉 案例:引入了一个性能优秀的日志库,但它的方法名是writeLog()
,而你的系统统一调用log()
- 旧系统改造 (30%)
👉 案例:老系统使用XML传输数据,新模块要求JSON格式 - 多支付渠道接入 (25%)
👉 案例:支付宝用alipayPay()
,微信用wechatPayment()
,银联用unionPay()
,业务层不想写if-else
🎯 一句话总结适配模式
"转换器" ------ 像胶水一样粘合两个不兼容的接口,让它们无需修改就能协作。
二、什么是适配器模式
🔥 一句话定义
适配器模式 就像代码世界的"多功能转换插头"------它通过包装不兼容的接口,让原本无法协作的两个类能够一起工作,且不修改双方原始代码。
🛠️ 两种实现方式对比(含代码)
1️⃣ 类适配器(继承实现)
核心原理:让适配器同时继承目标接口和被适配者
Java代码示例:
java
// 目标接口(你的系统标准)
interface USB {
void connect();
}
// 被适配者(第三方设备)
class TypeC {
public void typeCConnect() {
System.out.println("Type-C 连接成功");
}
}
// 适配器(继承实现)
class TypeCToUSBAdapter extends TypeC implements USB {
@Override
public void connect() {
super.typeCConnect(); // 调用被适配者的方法
System.out.println("转换为USB协议通信");
}
}
// 使用端
public class Client {
public static void main(String[] args) {
USB usb = new TypeCToUSBAdapter();
usb.connect(); // 统一调用USB接口
}
}
✅ 优点 :直接重写方法,代码简洁
❌ 缺点:Java单继承限制,无法适配多个类
2️⃣ 对象适配器(组合实现)
核心原理:在适配器中持有被适配者的对象引用
Java代码示例:
java
class TypeCToUSBAdapter implements USB {
private TypeC typeC; // 组合被适配对象
public TypeCToUSBAdapter(TypeC typeC) {
this.typeC = typeC;
}
@Override
public void connect() {
typeC.typeCConnect(); // 委托调用
System.out.println("转换为USB协议通信");
}
}
// 使用端
public class Client {
public static void main(String[] args) {
USB usb = new TypeCToUSBAdapter(new TypeC());
usb.connect();
}
}
✅ 优点 :解耦更彻底,支持适配多个对象
✅ 推荐场景:Java项目首选(规避单继承问题)
💡 关键决策点:该选哪种实现?
对比维度 | 类适配器 | 对象适配器 |
---|---|---|
实现方式 | 继承被适配类 | 组合被适配对象 |
灵活性 | 只能适配一个父类 | 可适配多个不同对象 |
代码侵入性 | 需要知道被适配类细节 | 仅依赖接口,更松耦合 |
适用场景 | 明确单适配源时 | 多适配源或未来扩展时 |
行业实践:对象适配器使用率超过80%(《设计模式:可复用面向对象软件的基础》统计)
🚨 避坑指南:新手常见错误
-
过度适配:不要为每个微小差异都创建适配器
java// 错误示范:仅仅方法名不同时不需要适配器 class UnnecessaryAdapter implements NewInterface { private OldClass old; public void newMethod() { old.oldMethod(); // 直接改调用方更简单 } }
-
忽略协议转换:仅调用方法不等于适配
java// 错误案例:微信支付金额单位是分,未做转换 class WechatPayAdapter implements Payment { public void pay(BigDecimal amount) { wechatPay.pay(amount); // 应该 amount*100 } }
🌰 回到支付场景(强化理解)
假设你的系统需要同时支持支付宝(金额单位元)和微信支付(金额单位分):
java
// 对象适配器实现单位转换
class WechatPayAdapter implements Payment {
private WechatPay wechatPay;
public void pay(BigDecimal yuan) {
BigDecimal fen = yuan.multiply(new BigDecimal(100));
wechatPay.pay(fen.intValue()); // 转换为分
}
}
这才是真正的适配------不仅转换接口,还要处理协议差异!
三、实战场景:支付系统对接(优化增强版)
场景痛点升级:不只是接口名称不同!
假设遇到更复杂的差异:
- 支付宝:金额单位元 ,方法
alipayPay(BigDecimal yuan)
- 微信支付:金额单位分 ,方法
wechatPay(int fen)
- 银联支付:要求字符串类型金额 ,方法
unionPay(String amount)
代码演进(完整商业级实现)
1️⃣ 统一定义支付接口
java
public interface Payment {
/**
* 统一支付方法(单位:元)
* @param amount 金额(元,支持小数点后两位)
* @throws PaymentException 支付失败时抛出
*/
void pay(BigDecimal amount) throws PaymentException;
}
2️⃣ 支付宝适配器(直接转换)
java
public class AlipayAdapter implements Payment {
private final Alipay alipay = new Alipay();
@Override
public void pay(BigDecimal yuan) {
// 支付宝无需单位转换,直接调用
alipay.alipayPay(yuan.setScale(2, RoundingMode.HALF_UP));
}
}
3️⃣ 微信支付适配器(单位转换)
java
public class WechatPayAdapter implements Payment {
private final WechatPay wechatPay = new WechatPay();
@Override
public void pay(BigDecimal yuan) throws PaymentException {
// 元转分(乘以100后取整)
int fen = yuan.multiply(BigDecimal.valueOf(100))
.setScale(0, RoundingMode.HALF_UP)
.intValueExact();
if (fen <= 0) {
throw new PaymentException("金额不能小于0.01元");
}
wechatPay.wechatPay(fen); // 调用微信SDK
}
}
4️⃣ 银联支付适配器(格式转换)
java
public class UnionPayAdapter implements Payment {
private final UnionPay unionPay = new UnionPay();
@Override
public void pay(BigDecimal yuan) {
// 保留两位小数并转为字符串(如12.34元)
String amount = yuan.setScale(2, RoundingMode.HALF_UP)
.toPlainString();
unionPay.unionPay(amount + "元"); // 银联要求带单位
}
}
5️⃣ 业务方调用(Spring Boot风格)
java
@RestController
public class OrderController {
// 通过@Qualifier指定支付方式
private final Payment payment;
public OrderController(@Qualifier("wechatPayAdapter") Payment payment) {
this.payment = payment;
}
@PostMapping("/pay")
public String createOrder(@RequestParam BigDecimal amount) {
try {
payment.pay(amount);
return "支付成功";
} catch (PaymentException e) {
return "支付失败:" + e.getMessage();
}
}
}
🔥 关键增强点解析
-
精确金额处理
- 使用
BigDecimal
避免浮点数精度问题 setScale(2)
强制保留两位小数RoundingMode.HALF_UP
银行家舍入法
- 使用
-
异常处理标准化
javapublic class PaymentException extends Exception { public PaymentException(String message) { super(message); } }
-
Spring集成示例
java@Configuration public class PaymentConfig { @Bean("alipayAdapter") public Payment alipayAdapter() { return new AlipayAdapter(); } @Bean("wechatPayAdapter") public Payment wechatPayAdapter() { return new WechatPayAdapter(); } }
-
防御性编程
- 金额最小值校验
intValueExact()
防止溢出- 不可变对象(字段用final修饰)
📊 支付方式切换示意图
💼 商业项目经验
-
动态策略选择
javapublic class PaymentStrategy { private Map<PayType, Payment> strategies = new HashMap<>(); public PaymentStrategy() { strategies.put(PayType.ALIPAY, new AlipayAdapter()); strategies.put(PayType.WECHAT, new WechatPayAdapter()); } public void pay(PayType type, BigDecimal amount) { strategies.get(type).pay(amount); } }
四、模式应用时机(含深度决策指南)
✅ 应该使用适配器模式的三大信号
1️⃣ 历史代码整合
👉 场景特征:
- 需要接入遗留系统/第三方库
- 对方接口不符合当前系统规范
- 典型案例:
java
// 老旧报表系统(无法修改)
class LegacyReport {
void generate(Date from, Date to) { /* XML格式输出 */ }
}
// 适配器转换为JSON接口
class ReportAdapter implements ModernReportService {
private LegacyReport legacyReport;
public String generateJsonReport(LocalDate start, LocalDate end) {
Date from = convertToDate(start);
Date to = convertToDate(end);
String xml = legacyReport.generate(from, to);
return xmlToJson(xml); // 关键转换逻辑
}
}
2️⃣ 多源异构系统对接
👉 场景特征:
- 同时对接多个提供相同功能但接口不同的系统
- 需要统一对外暴露标准接口
- 典型案例:
3️⃣ 协议转换需求
👉 场景特征:
- 数据格式转换(XML↔JSON)
- 单位转换(元↔分↔美元)
- 编码转换(GBK↔UTF-8)
- 典型案例:
java
// 温度传感器适配器(华氏度转摄氏度)
class FahrenheitSensorAdapter implements CelsiusSensor {
private FahrenheitSensor sensor;
public double getTemperature() {
return (sensor.getFahrenheit() - 32) * 5 / 9;
}
}
❌ 不该使用适配器的危险信号
1️⃣ 简单重命名方法
java
// 错误示范:仅方法名不同时
class UnnecessaryAdapter {
private OldService old;
// 没有实际转换逻辑
public void newName() {
old.oldName();
}
}
正确做法:直接修改调用方代码或使用IDE重命名重构
2️⃣ 临时性对接需求
👉 场景特征:
- 确定只使用一次的外部服务
- 后期不会扩展其他实现
- 解决方案:在调用方直接做简单转换
java
// 在业务代码中直接转换
void process() {
BigDecimal yuan = calculateAmount();
int fen = yuan.multiply(100).intValue();
wechatPay.pay(fen); // 无需专门写适配器
}
3️⃣ 架构层级混乱
👉 错误案例:在领域层编写适配器调用基础设施层
正确架构:
🔍 适配器 vs 其他模式(决策树)
🛠️ 代码坏味道检测清单
当出现以下情况时,考虑引入适配器:
-
业务代码中存在大量接口转换代码
java// 坏味道示例 void pay(String type, BigDecimal amount) { if ("alipay".equals(type)) { alipay.alipayPay(amount); } else if ("wechat".equals(type)) { wechat.wechatPayment(amount.multiply(BigDecimal.TEN)); } // 每新增一个支付方式都要修改此处 }
-
对外部系统的调用散落在多个类中
java// OrderService.java wechat.pay(amount.multiply(100).intValue()); // RefundService.java wechat.refund(amount.multiply(100).intValue()); // 重复单位转换
-
单元测试需要大量Mock外部接口
java@Test void testPayment() { WechatPay mockWechat = mock(WechatPay.class); // 需要mock原始接口而不是业务接口 service.pay(new BigDecimal("100")); verify(mockWechat).wechatPayment(10000); }
🎯 终极决策原则
当修改调用方 的成本 > 编写适配器的成本时,才使用适配器模式
-
配置化扩展
在
application.yml
中配置启用哪些支付方式:yamlpayment: enabled: - alipay - wechat
-
适配器+工厂模式结合
javapublic class PaymentFactory { public static Payment create(String type) { switch (type.toLowerCase()) { case "alipay": return new AlipayAdapter(); case "wechat": return new WechatPayAdapter(); default: throw new IllegalArgumentException("不支持的支付类型"); } } }
五、Spring框架中的经典案例(源码级解析)
🔥 场景痛点:Controller的多样性
Spring MVC需要处理多种类型的Controller:
- 基于
@Controller
注解的处理器 - 实现
Servlet
接口的传统方式 - 实现
Controller
接口的旧版Spring MVC写法 - 其他自定义处理器(如gRPC、WebSocket)
问题 :如何让DispatcherServlet
用统一的方式调用这些不同结构的Controller?
💡 解决方案:HandlerAdapter
Spring通过适配器模式将各种Controller统一成标准处理流程:
🌰 代码级演示(模拟简化版)
1. 定义HandlerAdapter接口
java
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception;
}
2. 实现注解控制器适配器
java
public class AnnotationHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler instanceof MyAnnotationController;
}
@Override
public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
MyAnnotationController controller = (MyAnnotationController) handler;
// 解析@RequestMapping等注解
String result = controller.processRequest(request);
return new ModelAndView(result);
}
}
3. 实现Servlet适配器
java
public class ServletHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler instanceof Servlet;
}
@Override
public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
Servlet servlet = (Servlet) handler;
servlet.service(request, response); // 直接调用Servlet原生方法
return null; // Servlet自己处理响应
}
}
🔍 源码解析:RequestMappingHandlerAdapter
Spring实际源码中的关键设计:
java
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter {
// 核心处理方法
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response,
HandlerMethod handlerMethod) throws Exception {
// 1. 参数解析(@RequestParam等)
Object[] args = getMethodArgumentValues(request, response, handlerMethod);
// 2. 调用Controller方法
Object returnValue = invokeForRequest(request, response, handlerMethod, args);
// 3. 返回值处理(@ResponseBody等)
return handleReturnValue(returnValue, handlerMethod, request, response);
}
// 判断是否支持该Handler
public boolean supports(Object handler) {
return (handler instanceof HandlerMethod);
}
}
关键流程:
- 参数解析 :通过
HandlerMethodArgumentResolver
处理各种参数注解 - 方法调用:通过反射执行Controller方法
- 返回值处理 :使用
HandlerMethodReturnValueHandler
处理@ResponseBody等
🛠️ Spring MVC处理流程(适配器视角)
💼 实际项目中的应用
场景:统一处理自定义协议控制器
假设需要支持通过@ProtobufController
注解处理Protobuf格式请求:
java
// 1. 定义自定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ProtobufController {
}
// 2. 实现适配器
public class ProtobufHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler.getClass().isAnnotationPresent(ProtobufController.class);
}
@Override
public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// Protobuf反序列化逻辑
byte[] data = request.getInputStream().readAllBytes();
Message requestObj = parseProtobuf(data);
// 反射调用处理方法
Method method = findHandlerMethod(handler);
Object result = method.invoke(handler, requestObj);
// Protobuf序列化响应
writeProtobuf(response, result);
return null;
}
}
🎯 设计启示
- 开闭原则:新增Controller类型只需添加适配器,无需修改DispatcherServlet
- 单一职责:每个适配器只负责一种处理逻辑
- 协议转换:适配器不仅做接口适配,还处理数据格式转换
六、动手作业:日志框架适配(商业级实现)
需求升级:完整日志门面实现
- 支持主流日志框架:Log4j 1.x/2.x、Logback
- 统一日志接口功能:
- 支持不同日志级别(DEBUG/INFO/WARN/ERROR)
- 支持占位符(
{}
替换) - 支持异常堆栈打印
代码实现(生产级规范)
1️⃣ 统一定义日志接口
java
public interface Logger {
void debug(String format, Object... args);
void info(String format, Object... args);
void warn(String format, Object... args);
void error(String format, Object... args);
void error(String msg, Throwable t);
}
2️⃣ Log4j 1.x适配器
java
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
public class Log4jAdapter implements Logger {
private final Logger log;
public Log4jAdapter(String name) {
this.log = Logger.getLogger(name);
}
@Override
public void debug(String format, Object... args) {
if (log.isDebugEnabled()) {
log.debug(formatMessage(format, args));
}
}
@Override
public void error(String msg, Throwable t) {
log.error(msg, t);
}
// 其他级别方法实现类似...
private String formatMessage(String format, Object... args) {
return String.format(format.replace("{}", "%s"), args);
}
}
3️⃣ Logback适配器
java
import ch.qos.logback.classic.Logger;
public class LogbackAdapter implements Logger {
private final Logger log;
public LogbackAdapter(org.slf4j.Logger logger) {
this.log = (Logger) logger;
}
@Override
public void info(String format, Object... args) {
if (log.isInfoEnabled()) {
log.info(format, args);
}
}
// 其他方法实现类似Log4jAdapter...
}
4️⃣ 日志工厂(自动检测环境)
java
public class LoggerFactory {
private static final String LOG_TYPE = detectLogFramework();
public static Logger getLogger(Class<?> clazz) {
String name = clazz.getName();
switch (LOG_TYPE) {
case "log4j":
return new Log4jAdapter(name);
case "logback":
return new LogbackAdapter(org.slf4j.LoggerFactory.getLogger(clazz));
default:
throw new IllegalStateException("未检测到支持的日志框架");
}
}
private static String detectLogFramework() {
try {
Class.forName("org.apache.log4j.Logger");
return "log4j";
} catch (ClassNotFoundException e1) {
try {
Class.forName("ch.qos.logback.classic.Logger");
return "logback";
} catch (ClassNotFoundException e2) {
throw new RuntimeException("请添加日志框架依赖");
}
}
}
}
5️⃣ 业务使用示例
java
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public void createOrder() {
log.info("开始创建订单,用户ID:{}", 12345);
try {
// 业务逻辑
} catch (Exception e) {
log.error("订单创建失败", e);
}
}
}
🔥 关键实现细节
-
性能优化
- 日志级别判断(
isDebugEnabled()
)避免不必要的字符串拼接
java// 错误写法(即使不输出也会拼接字符串) log.debug("User info: " + user); // 正确写法 if (log.isDebugEnabled()) { log.debug("User info: {}", user.toString()); }
- 日志级别判断(
-
占位符统一处理
- 将
{}
转换为String.format
的%s
javaString message = "用户:{} 余额:{}"; Object[] args = {"张三", 100.50}; // 格式化为:用户:张三 余额:100.50
- 将
-
异常堆栈处理
- 单独提供带Throwable参数的error方法
java// 正确保留堆栈信息 catch (Exception e) { log.error("支付失败", e); // 而不仅是e.getMessage() }
📦 Maven依赖配置
xml
<!-- Log4j 1.x适配 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- Logback适配 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.6</version>
</dependency>
🌍 架构图:日志门面工作原理
🚀 高级扩展方向
-
动态切换日志实现
java// 运行时指定日志框架 System.setProperty("logging.framework", "logback");
-
配置热更新
javapublic class Log4jAdapter implements Logger { private volatile LogLevel currentLevel; public void setLevel(LogLevel level) { this.currentLevel = level; // 更新底层Log4j配置 } }
-
日志链路追踪
javapublic void info(String format, Object... args) { String traceId = MDC.get("traceId"); String newFormat = "[traceId:{}] " + format; Object[] newArgs = ArrayUtils.addFirst(args, traceId); log.info(newFormat, newArgs); }