从源码到实践:我对 Spring 设计模式的理解

举报
yd_236998400 发表于 2026/10/25 09:28:44 2026/10/25
【摘要】 从源码到实践:我对 Spring 设计模式的理解Spring 并不是把 GoF 设计模式逐个实现一遍,而是把多个模式组合成了一套可扩展的运行时。理解这些组合关系,比背诵“JdbcTemplate 是模板方法、AOP 是代理模式”更重要。 一、先建立一个正确的观察角度很多文章会直接列出 Spring 使用的设计模式:工厂模式、单例模式、代理模式、模板方法、观察者模式、适配器模式、策略模式、责...

从源码到实践:我对 Spring 设计模式的理解

Spring 并不是把 GoF 设计模式逐个实现一遍,而是把多个模式组合成了一套可扩展的运行时。理解这些组合关系,比背诵“JdbcTemplate 是模板方法、AOP 是代理模式”更重要。

一、先建立一个正确的观察角度

很多文章会直接列出 Spring 使用的设计模式:工厂模式、单例模式、代理模式、模板方法、观察者模式、适配器模式、策略模式、责任链模式……这份清单没有错,但只停留在清单层面,实际开发帮助不大。

更有价值的问题是:

  1. 这个模式解决了什么变化?
  2. Spring 把变化点放在哪里了?
  3. 这个模式和其他模式如何协作?
  4. 在业务代码中,什么时候应该复用这种思想,什么时候不应该照搬?

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 代码“不生效”时,可以按这条路径排查:

  1. Bean 是否真的被容器管理?
  2. 调用对象是否是代理对象?
  3. 调用是否经过代理边界?
  4. 代理类型是否支持当前类和方法?
  5. 责任链或拦截器顺序是否正确?
  6. 事务、事件和异步操作的边界是否符合预期?

注解只是元数据,真正执行行为的是容器注册的后置处理器、拦截器和代理。只看注解不看运行时对象,很容易产生“代码写了,框架应该懂”的错觉。

十三、我的总结

Spring 的设计模式理解,可以收敛成四句话:

  1. 工厂和 IoC 解决对象如何创建、组装和管理。
  2. 代理和装饰器 解决不修改业务代码也能增加横切能力。
  3. 模板、策略、适配器 解决流程、算法和外部接口的变化。
  4. 观察者和责任链 解决模块协作与流程编排。

真正值得学习的不是某个类到底属于哪一种 GoF 模式,而是 Spring 如何把这些模式组合起来,让变化被隔离在合适的边界内。

当你设计业务代码时,可以先问一句:

这里未来最可能变化的是什么?我能不能只让它变化,而不牵动其他代码?

能准确回答这个问题,设计模式就不再是面试名词,而会变成一把真正能落地的工程工具。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。