从 Spring 源码到项目实战:设计模式落地经验与最佳实践
在实际项目开发中,Spring 框架的设计模式并非停留在源码层面的 “理论概念”,而是解决复杂业务问题的 “实用工具”。结合多年的项目经验,我想从创建型、结构型、行为型三大类设计模式入手,分享一些真实场景下的应用心得,以及这些模式如何帮助团队提升代码质量和开发效率。
一、创建型模式:解决 “对象创建” 的痛点,让依赖更可控
创建型模式的核心是 “如何灵活、高效地创建对象”,在实际项目中,对象的创建往往伴随着复杂依赖、资源管理等问题,Spring 的创建型模式恰好能化解这些痛点。
1. 工厂模式:从 “硬编码 new 对象” 到 “容器托管”,解耦依赖关系
场景:在一个电商项目中,订单服务(OrderService)需要依赖库存服务(InventoryService)、支付服务(PaymentService),而这些服务又依赖数据库连接、缓存客户端等资源。如果直接在 OrderService 中用new
创建依赖,会导致:
- 依赖关系硬编码,修改一个服务的实现类(比如从 MySQL 切换到 PostgreSQL),需要修改所有依赖它的类;
- 对象创建逻辑分散,比如库存服务需要初始化缓存连接,这些逻辑散落在各个地方,难以维护。
解决方案:借鉴 Spring 的BeanFactory
思想,用工厂模式统一管理对象创建。
- 定义一个全局的 “服务工厂”(类似
DefaultListableBeanFactory
),在工厂中注册所有服务的创建逻辑(比如InventoryService
的创建需要先初始化 Redis 连接); - 订单服务通过工厂的
getBean("inventoryService")
获取依赖,而非直接new
。
效果:
- 依赖关系由工厂集中管理,修改服务实现时,只需更新工厂中的创建逻辑,无需改动所有依赖类;
- 支持 “延迟加载”,服务在第一次被使用时才创建,减少项目启动时的资源消耗。
2. 单例模式:避免 “重复创建”,节省资源开销
场景:在一个数据同步项目中,有一个ElasticSearchClient
工具类,负责与 ES 集群建立连接。如果每次调用同步方法都new
一个客户端,会导致:
- 频繁创建 TCP 连接,消耗 ES 集群的连接数,甚至触发限流;
- 客户端初始化时需要加载配置(比如集群地址、认证信息),重复加载造成性能浪费。
解决方案:借鉴 Spring 的@Singleton
(默认作用域),将ElasticSearchClient
设计为单例。
- 在服务工厂中,通过
getSingletonBean
方法确保客户端只被创建一次,后续调用直接返回缓存的实例; - 结合 “双重检查锁”(DCL)处理并发场景,避免多线程下重复创建。
注意:单例模式并非 “万能药”,对于有状态的对象(比如包含用户会话信息的UserContext
),绝对不能用单例,否则会导致线程安全问题(多个线程共享同一实例,数据混乱)。Spring 中通过@Scope("prototype")
解决这类问题,我们在项目中也可以借鉴 —— 为有状态对象设置 “原型作用域”,每次获取都创建新实例。
3. 建造者模式:拆解 “复杂对象” 的创建,避免 “参数爆炸”
场景:在一个报表系统中,需要创建ReportQueryParam
对象,用于传递报表查询参数。这个对象包含:时间范围(开始时间、结束时间)、数据维度(用户、地区、产品)、过滤条件(多个Filter
对象)、分页参数(页码、页大小)等 20 + 个参数。如果用构造器或setter
创建,会导致:
- 构造器参数列表过长,可读性差,且容易传错顺序;
- 部分参数是可选的(比如分页参数),但
setter
调用分散,难以确认是否遗漏必填参数(比如开始时间)。
解决方案:借鉴 Spring 的BeanDefinitionBuilder
,用建造者模式封装ReportQueryParam
的创建。
// 建造者类
public class ReportQueryParamBuilder {private ReportQueryParam param = new ReportQueryParam();public ReportQueryParamBuilder timeRange(Date start, Date end) {param.setStartTime(start);param.setEndTime(end);return this;}public ReportQueryParamBuilder addFilter(Filter filter) {param.getFilters().add(filter);return this;}// 其他参数设置方法...public ReportQueryParam build() {// 校验必填参数if (param.getStartTime() == null) {throw new IllegalArgumentException("开始时间不能为空");}return param;}
}// 使用时
ReportQueryParam param = new ReportQueryParamBuilder().timeRange(start, end).addFilter(new Filter("region", "华东")).page(1, 10).build();
效果:
- 创建逻辑链式调用,参数关系清晰,可读性大幅提升;
- 在
build()
方法中统一校验必填参数,避免因参数缺失导致的运行时错误。
二、结构型模式:解决 “对象组合” 的问题,让系统更灵活
结构型模式关注 “如何将对象组合成更大的结构”,在实际项目中,我们常需要集成不同组件、扩展现有功能,这时候结构型模式能派上大用场。
1. 代理模式:无侵入增强,让 “通用逻辑” 与 “业务逻辑” 分离
场景:在一个用户中心项目中,需要为所有接口添加 “日志记录”(记录请求参数、响应结果)和 “权限校验”(检查用户是否有访问权限)。如果直接在每个接口方法中写这些逻辑,会导致:
- 代码冗余:每个方法都有重复的日志和权限代码;
- 业务逻辑被污染:核心功能(比如用户注册、登录)与非核心逻辑混杂,难以维护。
解决方案:借鉴 Spring AOP 的 “动态代理” 思想,用代理模式对接口进行增强。
- 定义一个
UserServiceProxy
,实现UserService
接口,在invoke
方法中先执行日志记录和权限校验,再调用真实的UserServiceImpl
; - 通过 Spring 的
@Aspect
注解(底层是 JDK 动态代理或 CGLIB),甚至可以更简单:直接用Proxy.newProxyInstance
创建代理对象,无需手动编写代理类。
效果:
- 通用逻辑(日志、权限)集中在代理类中,业务类只关注核心功能,符合 “单一职责原则”;
- 后续需要添加新的增强(比如接口耗时统计),只需修改代理类,无需改动所有业务方法。
2. 适配器模式:化解 “接口不兼容”,让异构组件和谐共处
场景:在一个支付系统中,需要集成支付宝、微信支付、银联三种支付方式。这三种支付接口的方法名和参数完全不同:
- 支付宝:
AlipayClient.pay(String orderNo, BigDecimal amount)
; - 微信支付:
WechatPayClient.doPayment(String outTradeNo, double money, String openId)
; - 银联:
UnionPayClient.processPayment(PaymentRequest request)
。
如果直接在业务代码中调用这些接口,会导致支付逻辑与具体的支付厂商强耦合,后续接入新支付方式(比如 PayPal)时,需要修改大量业务代码。
解决方案:借鉴 Spring MVC 的HandlerAdapter
,用适配器模式统一支付接口。
- 定义统一的
PaymentAdapter
接口:void pay(Order order)
; - 为每种支付方式编写适配器:
AlipayAdapter
实现PaymentAdapter
,内部调用AlipayClient.pay
;WechatPayAdapter
调用WechatPayClient.doPayment
; - 业务代码只需调用
PaymentAdapter.pay(order)
,无需关心具体是哪种支付方式。
效果:
- 业务代码与具体支付厂商解耦,接入新支付方式时,只需新增一个适配器,无需修改业务逻辑;
- 统一的接口便于做通用处理(比如支付结果回调的统一封装)。
3. 装饰器模式:“层层包裹” 实现功能叠加,避免继承爆炸
场景:在一个文件上传系统中,需要对上传的文件进行一系列处理:校验文件大小、检查文件类型、添加水印、压缩图片。如果用继承实现(比如WatermarkFileUploader extends FileUploader
,CompressFileUploader extends WatermarkFileUploader
),会导致:
- 继承链越来越长,且功能组合固定(比如 “压缩 + 水印” 和 “水印 + 压缩” 需要两个不同的子类);
- 新增处理步骤(比如病毒扫描)时,需要修改所有相关的子类。
解决方案:借鉴 Spring 对HttpServletRequest
的装饰(HttpServletRequestWrapper
),用装饰器模式动态扩展功能。
- 定义
FileUploader
接口:void upload(File file)
; - 编写基础实现
DefaultFileUploader
(仅负责文件上传到服务器); - 为每个处理步骤编写装饰器:
SizeCheckDecorator
(校验大小)、WatermarkDecorator
(添加水印),每个装饰器持有一个FileUploader
实例,在upload
方法中先执行自己的逻辑,再调用被装饰者的upload
。
使用时:
// 组合“校验大小+添加水印+压缩”功能
FileUploader uploader = new CompressDecorator(new WatermarkDecorator(new SizeCheckDecorator(new DefaultFileUploader()))
);
uploader.upload(file);
效果:
- 功能可以按需组合,无需修改现有类,符合 “开闭原则”;
- 每个装饰器只关注一个处理步骤,代码简洁,易于测试。
三、行为型模式:优化 “对象交互”,让流程更清晰、扩展更灵活
行为型模式聚焦于 “对象之间的协作方式”,在实际项目中,复杂的业务流程(比如订单处理、审批流)、事件通知等场景,都需要通过行为型模式梳理交互逻辑。
1. 模板方法模式:固化 “重复流程”,让变化部分 “按需定制”
场景:在一个数据导出系统中,导出 Excel 的流程是固定的:“查询数据→转换为 Excel 格式→写入文件→发送邮件通知”,但不同业务(比如用户数据导出、订单数据导出)的 “查询数据” 和 “转换格式” 步骤完全不同。如果每个业务都重写完整流程,会导致大量重复代码(比如文件写入、邮件通知)。
解决方案:借鉴 SpringAbstractBeanFactory
的模板方法,定义流程骨架,让子类实现变化部分。
- 定义抽象类
AbstractExcelExporter
,其中export()
方法是模板:java
public abstract class AbstractExcelExporter {// 模板方法:固定流程public final void export(ExportParam param) {List<?> data = queryData(param); // 变化部分:子类实现Workbook workbook = convertToExcel(data); // 变化部分:子类实现String filePath = writeToFile(workbook); // 固定部分sendNotification(param.getUserId(), filePath); // 固定部分}// 抽象方法:留给子类实现protected abstract List<?> queryData(ExportParam param);protected abstract Workbook convertToExcel(List<?> data);// 固定方法:通用逻辑private String writeToFile(Workbook workbook) { ... }private void sendNotification(Long userId, String filePath) { ... } }
- 子类
UserExcelExporter
只需实现queryData
(查询用户数据)和convertToExcel
(用户数据转 Excel)即可。
效果:
- 重复流程(文件写入、邮件通知)被固化在父类,避免代码冗余;
- 子类只关注自己的业务差异,代码量减少,且流程统一,降低出错概率。
2. 观察者模式:解耦 “事件发布” 与 “事件处理”,提升系统响应能力
场景:在一个电商订单系统中,当订单状态变为 “已支付” 时,需要触发一系列操作:扣减库存、生成物流单、发送支付成功短信、添加用户积分。如果在订单支付的方法中直接调用这些操作,会导致:
- 订单服务与库存、物流等服务强耦合,任何一个服务的修改(比如积分规则变更)都可能影响订单服务;
- 新增操作(比如推送支付消息到 APP)时,需要修改订单支付的核心代码,风险高。
解决方案:借鉴 Spring 的ApplicationEvent
和ApplicationListener
,用观察者模式实现事件驱动。
- 定义 “订单支付事件”
OrderPaidEvent
,包含订单 ID、支付金额等信息; - 订单服务在支付成功后,发布事件:
eventPublisher.publishEvent(new OrderPaidEvent(order))
; - 库存服务、物流服务等分别实现
EventListener
接口,监听OrderPaidEvent
,并处理自己的业务(扣库存、生成物流单)。
效果:
- 订单服务只负责发布事件,不关心谁处理事件,耦合度大幅降低;
- 新增事件处理者(比如 “推荐系统根据支付订单推送商品”)时,只需新增一个监听器,无需修改订单服务代码,扩展成本极低。
3. 责任链模式:拆分 “复杂校验 / 处理”,让流程更灵活、可配置
场景:在一个用户注册系统中,注册前需要进行多步校验:“手机号格式校验→验证码校验→密码强度校验→用户是否已存在校验”。如果将这些校验写在一个方法中,会导致:
- 校验逻辑臃肿,一个方法几百行代码,难以维护;
- 校验顺序固定(比如必须先校验手机号再校验验证码),后续需要调整顺序(比如先校验用户是否存在)时,需要大幅修改代码。
解决方案:借鉴 Spring 拦截器链(HandlerInterceptor
),用责任链模式拆分校验步骤。
- 定义校验处理器接口
ValidationHandler
:boolean validate(User user, ValidationChain chain)
; - 每个校验步骤对应一个处理器:
PhoneValidator
、CaptchaValidator
、PasswordValidator
; - 责任链
ValidationChain
持有处理器列表,依次调用每个处理器的validate
方法,处理器可以决定是否继续调用下一个(比如手机号校验失败,直接返回,不再执行后续校验)。
使用时:
// 构建责任链
ValidationChain chain = new ValidationChain().addHandler(new PhoneValidator()).addHandler(new CaptchaValidator()).addHandler(new PasswordValidator());// 执行校验
if (chain.validate(user)) {// 注册用户
}
效果:
- 每个校验逻辑独立,代码清晰,易于测试和修改;
- 校验顺序通过链的组装灵活调整,甚至可以根据业务场景动态增减处理器(比如内部用户注册跳过验证码校验)。
四、经验总结:设计模式不是 “银弹”,但能让代码 “有章可循”
在实际项目中应用 Spring 的设计模式,有几个关键点需要注意:
“为解决问题而用”,而非 “为用而用”:设计模式是工具,不是目的。比如单例模式适合无状态对象,但强行用在有状态对象上会导致线程安全问题;责任链模式适合步骤拆分,但链条过长会增加调试难度(不知道哪一步出了问题)。
“借鉴 Spring,但不盲从 Spring”:Spring 的设计模式是为框架通用性服务的(比如
BeanFactory
需要支持各种 Bean 的创建),但项目中可以简化。比如不需要像 Spring 那样实现完整的BeanDefinition
,一个简单的工厂类可能就足够解决依赖问题。“结合业务复杂度选择模式”:小项目、简单场景(比如一个单表 CRUD 接口),没必要用代理、观察者等模式,否则会增加代码复杂度;但中大型项目、核心流程(比如支付、订单处理),适当引入设计模式能显著提升可维护性。
“通过测试验证模式的合理性”:好的设计模式应该让代码更易测试。比如用代理模式分离的通用逻辑,可以单独测试;用观察者模式的事件处理,能方便地模拟事件进行单元测试。
总之,Spring 框架的设计模式不是 “书本上的概念”,而是无数开发者在解决复杂问题时沉淀的经验。在实际项目中,理解这些模式的 “设计初衷”(比如工厂模式解耦依赖、代理模式实现无侵入增强),并结合自身业务场景灵活运用,才能真正发挥其价值 —— 让代码从 “能跑” 变得 “好维护、可扩展”。
如果这篇文章对大家有帮助可以点赞关注,你的支持就是我的动力😊!