从源码到实践:我对 Spring 设计模式的理解
从源码到实践:我对 Spring 设计模式的理解
Spring 并不是把 GoF 设计模式逐个实现一遍,而是把多个模式组合成了一套可扩展的运行时。理解这些组合关系,比背诵“
JdbcTemplate是模板方法、AOP 是代理模式”更重要。
一、先建立一个正确的观察角度
很多文章会直接列出 Spring 使用的设计模式:工厂模式、单例模式、代理模式、模板方法、观察者模式、适配器模式、策略模式、责任链模式……这份清单没有错,但只停留在清单层面,实际开发帮助不大。
更有价值的问题是:
- 这个模式解决了什么变化?
- Spring 把变化点放在哪里了?
- 这个模式和其他模式如何协作?
- 在业务代码中,什么时候应该复用这种思想,什么时候不应该照搬?
Spring 的核心价值,正是把对象创建、依赖组装、横切逻辑、事件分发和请求处理这些变化频繁的部分,从业务代码中抽离出来,交给容器和基础设施统一管理。
★ Insight ─────────────────────────────────────
- 设计模式不是类名,而是对变化的隔离方式。看到
BeanFactory、ProxyFactory这些名字只是入口,真正要看的是它们替谁承担了变化。 - Spring 很少单独使用一种模式,通常是“工厂创建对象 + 代理织入行为 + 策略选择实现 + 责任链执行流程”的组合。
- Spring 中的模式大多服务于扩展性和解耦,而不是为了让代码看起来更“面向对象”。模式太多、抽象太深,同样会把项目整得像一锅炖好的面条。
─────────────────────────────────────────────────
本文以 Spring Framework 6 / Spring Boot 3 的常见使用方式为背景,类名和实现细节可能随着版本调整,但背后的设计思想相对稳定。
二、IoC 与 DI:工厂模式的基础设施化
2.1 BeanFactory:对象创建交给容器
传统代码通常直接创建依赖:
public class OrderService {
private final OrderRepository repository = new JdbcOrderRepository();
}
这种写法把三个决策绑定在了一起:
- 使用什么实现;
- 什么时候创建对象;
- 对象之间如何组装。
Spring 则把创建过程交给容器:
@Service
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
}
@Repository
public class JdbcOrderRepository implements OrderRepository {
// persistence logic
}
BeanFactory 和它的常用实现 DefaultListableBeanFactory,承担了 Bean 定义注册、对象创建、依赖解析和生命周期管理等工作。ApplicationContext 在此基础上又增加了事件发布、国际化、资源加载等能力。
这里可以把 Spring 看成一个功能更完整的工厂:调用方只声明“我要什么”,不再关心“这个对象怎么生产”。这就是 IoC 的核心:控制权从业务对象转移到了容器。
2.2 FactoryBean:把复杂创建过程封装起来
当某类对象的创建过程复杂,或者需要根据配置动态生成对象时,可以使用 FactoryBean<T>:
public class ClientFactoryBean implements FactoryBean<ApiClient> {
private final String baseUrl;
public ClientFactoryBean(String baseUrl) {
this.baseUrl = baseUrl;
}
@Override
public ApiClient getObject() {
return ApiClient.builder()
.baseUrl(baseUrl)
.timeout(Duration.ofSeconds(3))
.build();
}
@Override
public Class<?> getObjectType() {
return ApiClient.class;
}
}
FactoryBean 本身是工厂 Bean,而 getObject() 返回的对象才是业务代码真正拿到的对象。MyBatis、Feign 等基础设施都会使用类似的思路,把代理对象或复杂客户端隐藏在容器内部。
2.3 这不是“全局工厂大杂烩”
业务代码不应该到处注入 ApplicationContext,再通过字符串查 Bean:
// 不推荐:业务代码主动向容器索取依赖
var service = applicationContext.getBean(OrderService.class);
这样会把依赖关系藏起来,测试也会变麻烦。推荐使用构造器注入,让依赖直接出现在类型和构造函数上。容器负责组装,业务对象负责业务。
三、单例模式:Spring 管理的单例不是经典 Singleton
Spring 默认 Bean scope 是 singleton:在同一个 ApplicationContext 中,一个 Bean 定义通常对应一个共享实例。
@Component
public class PriceCalculator {
}
这和经典单例模式不同。经典单例往往把构造函数私有化,并通过静态方法暴露全局实例:
public final class GlobalConfig {
private static final GlobalConfig INSTANCE = new GlobalConfig();
private GlobalConfig() {
}
public static GlobalConfig getInstance() {
return INSTANCE;
}
}
Spring 的单例由容器管理,具有几个明显差异:
- 单例范围通常是一个容器,而不是整个 JVM;
- 实例可以被替换、代理和装饰;
- 生命周期由容器控制;
- 测试时可以创建新的容器或替换 Bean;
- Bean 本身不需要知道自己是单例。
因此,Spring 的单例更准确地说是“容器作用域内的共享实例”。
3.1 单例 Bean 的线程安全问题
单例只保证实例数量,不保证线程安全。Web 应用中一个单例 Service 通常会被多个请求并发调用,下面这种写法就有风险:
@Service
public class ReportService {
private String currentReportName;
public void generate(String reportName) {
currentReportName = reportName;
// long-running work
}
}
优先使用局部变量和不可变对象,把请求级状态放在方法内部,而不是放在单例字段中。
四、代理模式:AOP、事务和权限控制的共同底座
代理模式的核心是:不直接修改目标对象,而是在目标对象外面包一层,让代理负责额外行为。
Spring AOP 的典型场景包括:
@Transactional事务管理;@Cacheable缓存;@Async异步执行;- 方法级权限校验;
- 日志、监控和链路追踪。
业务代码只写核心逻辑:
@Service
public class PaymentService {
@Transactional
public void pay(Long orderId) {
// update order status
// create payment record
}
}
容器实际注入的对象可能不是 PaymentService 原始实例,而是一个代理。代理在调用目标方法前后加入事务边界:
调用方
-> Spring Proxy
-> 开启事务
-> PaymentService.pay()
-> 提交或回滚事务
4.1 JDK Dynamic Proxy 与 CGLIB
Spring 常见的两种代理方式:
| 代理方式 | 适用对象 | 主要特点 |
|---|---|---|
| JDK Dynamic Proxy | 实现了接口的类 | 基于接口代理,调用类型通常是接口 |
| CGLIB | 没有接口的具体类 | 通过生成子类代理,不能代理 final 类或 final 方法 |
代理的重点不是“用了哪种字节码技术”,而是调用是否经过代理。下面的自调用不会经过 Spring 代理:
@Service
public class AccountService {
public void transfer() {
audit();
}
@Transactional
public void audit() {
// transaction may not take effect for this internal call
}
}
this.audit() 是对象内部的直接调用,不会重新绕回代理,因此 @Transactional 可能不生效。常见处理方式是拆分 Bean,或者通过外部对象调用带注解的方法。别把注解当魔法符,代理没接住,注解就是墙上的装饰画。
五、模板方法:把固定流程和可变步骤分开
模板方法模式定义一套固定算法流程,把其中部分步骤交给子类或回调实现。
JdbcTemplate 是非常典型的例子。数据库连接、异常转换、资源关闭等流程基本固定,开发者只需要提供 SQL 和结果映射:
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Optional<User> findById(long id) {
String sql = "select id, name from users where id = ?";
return jdbcTemplate.query(sql, rs -> {
if (!rs.next()) {
return Optional.empty();
}
return Optional.of(new User(
rs.getLong("id"),
rs.getString("name")
));
}, id);
}
}
调用方不需要重复处理:
- 获取和释放连接;
- 创建和关闭 Statement;
- 捕获底层 JDBC 异常并转换;
- 处理事务同步。
5.1 模板方法的现代形态:回调与 Lambda
传统模板方法常通过继承覆盖钩子方法。Spring 更常用组合和回调,让可变步骤通过 Lambda 传入。这种方式减少了继承层次,也避免为了一个小变化创建很多子类。
这也是一个很值得借鉴的业务设计:当流程固定、局部步骤变化时,先抽出流程,再把变化作为参数传入,不要复制整段流程代码。
六、策略模式:把“选择哪种算法”从调用方拿走
策略模式把一组可互换的算法封装成同一接口,再根据上下文选择具体实现。
比如订单支付:
public interface PaymentStrategy {
String channel();
void pay(PaymentCommand command);
}
@Component
public class AlipayStrategy implements PaymentStrategy {
@Override
public String channel() {
return "alipay";
}
@Override
public void pay(PaymentCommand command) {
// call Alipay API
}
}
@Component
public class WechatPayStrategy implements PaymentStrategy {
@Override
public String channel() {
return "wechat";
}
@Override
public void pay(PaymentCommand command) {
// call WeChat Pay API
}
}
由 Spring 注入所有策略,再构建一个索引:
@Service
public class PaymentApplicationService {
private final Map<String, PaymentStrategy> strategies;
public PaymentApplicationService(List<PaymentStrategy> strategyList) {
this.strategies = strategyList.stream()
.collect(Collectors.toUnmodifiableMap(
PaymentStrategy::channel,
Function.identity()
));
}
public void pay(String channel, PaymentCommand command) {
PaymentStrategy strategy = strategies.get(channel);
if (strategy == null) {
throw new IllegalArgumentException("Unsupported payment channel: " + channel);
}
strategy.pay(command);
}
}
这样增加支付渠道时,主要是增加一个实现,不需要持续修改一大串 if-else。但策略模式也不是越多越好:只有当算法存在真实变化,或者变化点需要独立测试、独立扩展时,抽策略才划算。一个只有两行逻辑、永远不会变化的分支,硬抽接口就是给项目增加家具。
七、观察者模式:事件发布者不再依赖监听者
Spring 的事件机制将事件发布者和监听者解耦:
public record OrderPaidEvent(Long orderId) {
}
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public OrderService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@Transactional
public void pay(Long orderId) {
// update order state
eventPublisher.publishEvent(new OrderPaidEvent(orderId));
}
}
@Component
public class CouponListener {
@TransactionalEventListener
public void onOrderPaid(OrderPaidEvent event) {
// issue coupon after the transaction commits
}
}
发布者只知道“订单已支付”这件事,不需要知道优惠券、积分、通知分别由谁处理。
7.1 事件不是万能的消息队列
Spring Application Event 通常发生在当前应用进程内,默认也不是可靠的跨服务消息机制。需要可靠投递、重试、积压处理和跨服务消费时,应使用 Kafka、RabbitMQ 等消息系统。
另外,事务事件要明确触发时机:
- 事务提交前触发:监听器可能读到未提交状态;
- 事务提交后触发:业务状态已经落库,但进程崩溃时仍需考虑可靠投递;
- 事务回滚后不应执行:用合适的事务事件阶段表达这一点。
模式解决的是解耦,不会自动替你解决一致性、幂等和重试。
八、适配器模式:把不同接口接到统一流程上
Spring MVC 中的 HandlerAdapter 是很典型的适配器。Controller 方法、Servlet、函数式处理器的调用方式不同,但框架需要用统一流程执行它们:
请求
-> HandlerMapping 找到处理器
-> HandlerAdapter 适配并调用处理器
-> 返回 ModelAndView 或响应体
适配器把“框架统一调度”和“具体处理器接口”隔离开来。框架主流程不需要写成一堆类型判断:
if (handler instanceof Controller) {
// call Controller
} else if (handler instanceof Function) {
// call Function
}
当要支持一种新的处理器类型时,增加对应的 Adapter 即可。
业务开发中也常用适配器对接外部系统:
public interface NotificationSender {
void send(String receiver, String content);
}
@Component
public class SmsSenderAdapter implements NotificationSender {
private final ThirdPartySmsClient client;
public SmsSenderAdapter(ThirdPartySmsClient client) {
this.client = client;
}
@Override
public void send(String receiver, String content) {
client.sendMessage(receiver, content);
}
}
上层只依赖 NotificationSender,第三方 SDK 的参数、异常和返回值都被隔离在适配器内部。
九、责任链模式:请求不是一次处理,而是一串处理
责任链把多个处理节点串起来,每个节点都可以处理请求、修改请求、终止流程,或者把请求交给下一个节点。
Spring 里常见的责任链包括:
- Servlet
FilterChain; - Spring MVC 的
HandlerInterceptor链; - Spring Security 的 Security Filter Chain;
- Bean 创建过程中的多个
BeanPostProcessor。
一个请求可能经历:
认证 -> CORS -> 日志 -> 限流 -> 权限 -> Controller -> 异常处理
每个节点只关心自己的职责,顺序则由框架配置管理。
9.1 责任链最容易踩的坑:顺序
责任链不是把组件放进 List 里就结束了。顺序会改变语义:
- 认证没完成就做权限校验,权限组件拿不到身份;
- 限流放在昂贵的鉴权之后,资源已经被浪费;
- 异常处理节点放错位置,异常无法被统一捕获;
- 安全过滤器链配置错误,可能出现“看起来配置了,实际没经过”的情况。
设计责任链时,应该明确每一环的前置条件、输出状态和终止条件。
十、装饰器与外观:Spring 还有很多“组合型”模式
10.1 装饰器:在不改原对象的情况下增加能力
装饰器和代理结构相似,区别更多体现在意图:代理通常强调控制访问,装饰器强调叠加能力。
在 Spring 生态中,事务代理、缓存代理、监控代理可以层层组合:
调用方
-> 监控代理
-> 缓存代理
-> 事务代理
-> 目标 Service
这也是为什么排查问题时不能只看源码里的目标类:真正执行的对象可能已经被多层基础设施包装。
10.2 外观模式:ApplicationContext 的统一入口
ApplicationContext 对底层多个能力提供统一入口:
- Bean 获取和生命周期管理;
- 事件发布;
- 资源加载;
- 国际化消息;
- 环境与配置访问。
调用方不需要直接操作各种底层对象,这就是外观模式的价值:隐藏子系统复杂性,提供更容易使用的接口。
不过外观也有边界。ApplicationContext 过于万能时,业务代码很容易把它当成 Service Locator。建议把容器能力留在启动和基础设施层,业务层继续使用明确的接口依赖。
十一、把这些模式串成一条完整链路
以一次带事务的 HTTP 请求为例,可以看到多个模式如何协作:
HTTP 请求
-> Filter Chain 责任链
-> HandlerMapping 策略/映射
-> HandlerAdapter 适配器
-> Controller 代理 代理
-> Application Service 业务对象
-> Transaction Interceptor 模板化的事务流程
-> Repository / JdbcTemplate 模板方法
-> ApplicationEventPublisher 观察者
-> Event Listener 观察者
调用方只看到一个 Controller 方法,背后却是一套由容器组装的协作网络。这种设计让业务代码保持相对干净,同时把框架的扩展点集中在基础设施层。
十二、在业务项目中如何选择设计模式
12.1 先找变化点,再决定模式
不要因为“这个项目需要体现设计模式”就强行加抽象。可以按下面的信号判断:
| 代码信号 | 可以考虑的模式 |
|---|---|
| 对象创建过程复杂且变化 | 工厂、抽象工厂、FactoryBean |
| 多种算法需要互换 | 策略 |
| 多个调用方都要执行相同流程 | 模板方法、模板回调 |
| 需要给现有对象增加横切行为 | 代理、装饰器 |
| 一个事件需要通知多个模块 | 观察者、领域事件 |
| 多种外部接口需要统一 | 适配器 |
| 请求需要依次经过多个处理步骤 | 责任链 |
| 子系统复杂但调用方只需要一个入口 | 外观 |
12.2 优先组合,谨慎继承
Spring 本身就大量使用组合:
- 容器组合 Bean;
- 代理组合横切行为;
- 责任链组合处理器;
- 策略组合可替换算法。
业务代码也应优先通过接口、委托和组合解决变化,避免建立深层继承树。继承一旦铺开,父类的隐式行为就会变成团队所有人的心理负担。
12.3 不要为了消灭 if-else 而消灭所有 if-else
简单分支通常比策略工厂更清楚。只有当分支逻辑持续增长、实现需要独立测试、增加新类型会频繁修改旧代码时,策略模式才真正有价值。
12.4 关注运行时行为,不只看注解
遇到 Spring 代码“不生效”时,可以按这条路径排查:
- Bean 是否真的被容器管理?
- 调用对象是否是代理对象?
- 调用是否经过代理边界?
- 代理类型是否支持当前类和方法?
- 责任链或拦截器顺序是否正确?
- 事务、事件和异步操作的边界是否符合预期?
注解只是元数据,真正执行行为的是容器注册的后置处理器、拦截器和代理。只看注解不看运行时对象,很容易产生“代码写了,框架应该懂”的错觉。
十三、我的总结
Spring 的设计模式理解,可以收敛成四句话:
- 工厂和 IoC 解决对象如何创建、组装和管理。
- 代理和装饰器 解决不修改业务代码也能增加横切能力。
- 模板、策略、适配器 解决流程、算法和外部接口的变化。
- 观察者和责任链 解决模块协作与流程编排。
真正值得学习的不是某个类到底属于哪一种 GoF 模式,而是 Spring 如何把这些模式组合起来,让变化被隔离在合适的边界内。
当你设计业务代码时,可以先问一句:
这里未来最可能变化的是什么?我能不能只让它变化,而不牵动其他代码?
能准确回答这个问题,设计模式就不再是面试名词,而会变成一把真正能落地的工程工具。
- 点赞
- 收藏
- 关注作者
评论(0)