Skip to content

设计模式在 Spring 中的应用 ​

S4-006建议学高级约 60 分钟JDK 21企业开发进阶 · 软件工程与设计模式

1. 本节定位 ​

前五节讲了设计规则与模式;本节把它们映射到你每天都在用的 Spring 里:

text
你以为在"学设计模式",其实你一直在"用设计模式"  ✓
Spring 本身就是"设计模式的教科书级实现"

★ 本节的价值 ✓✓:

既能"看懂框架为什么这么设计",也能"在业务里正确地用同一套模式" ✓

2. 学习目标 ​

  • 能把 Spring 的核心组件对应到具体模式 ★★★;
  • 知道 IoC/DI 就是"依赖倒置 + 工厂"的组合 ★★★;
  • 知道 AOP 的底层是代理,以及"内部调用失效"的原因 ★★★;
  • 知道 JdbcTemplate 是模板方法的教科书实现 ★★;
  • 知道 Spring 事件是观察者的实现 ★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看一个"知道模式之后才看得懂"的 Spring 行为 ✗:

text
现象:在 ShopService 里调用 this.refund(),加在 refund() 上的 @Transactional 不生效 ✗
原因:this 指向"原始对象",不是"代理对象" —— 事务通知根本没被触发  ✗✗

★ 如果你知道"AOP = 代理模式",这个现象是一眼能看穿的 ✓✓ 不知道的人则会以为是"Spring 的 bug" ✗(本模块在 《Spring:AOP 与事务》 实测过这个场景)

5. 核心概念 ​

5.1 Spring 核心组件的"模式对照表" ★★★ ​

Spring 组件用到的模式说明
BeanFactory / ApplicationContext ★★★工厂 + 注册表getBean(name) 就是注册式工厂
单例 Bean(默认) ★★★单例由容器管理生命周期,而不是手写 getInstance()
@Autowired 注入 ★★★依赖倒置业务类只依赖接口,实现由容器装配
AOP(@Transactional) ★★★代理JDK 动态代理或 CGLIB
JdbcTemplate ★★★模板方法固定"连接/异常/释放",开放"SQL 与映射"
ApplicationEvent ★★观察者业务与副作用解耦
ResourceLoader / ViewResolver ★★策略不同实现按配置替换
HandlerInterceptor / Filter ★★责任链请求在链上依次处理
BeanPostProcessor ★模板方法 + 扩展点在 Bean 生命周期里插钩子

★ 一句话 ✓✓:

Spring 的核心思想 = 工厂 + 依赖倒置 + 代理 ✓

其余都是在为这三样服务 ✓

5.2 IoC/DI:依赖倒置 + 工厂 ★★★ ​

没有 Spring 时 ✗:

java
class OrderService {
    private final OrderRepository repo = new MySQLOrderRepository();   // ✗ 写死
}

★ 三个后果 ✓:

#后果
① 无法单元测试 ✗✗必须连真实数据库
② 无法替换实现 ✗换库要改业务类
③ 依赖关系藏在代码里 ✗看类图也看不出全貌

有 Spring 时 ✓:

java
@Service
public class OrderService {
    private final OrderRepository repo;          // ← 接口
    public OrderService(OrderRepository repo) { this.repo = repo; }   // ← 构造注入
}

★ Spring 替你做了两件事 ✓✓:

做的事对应模式
创建与装配对象工厂 + 单例 ✓
让业务依赖接口依赖倒置 ✓

★ 为什么推荐"构造器注入" ✓✓:

理由说明
依赖不可变 ★★final 字段 + 构造器注入 ✓
依赖显式 ★★看构造器就知道它依赖什么(字段注入看不出来)✗
便于测试 ★★★直接 new OrderService(fakeRepo),不需要 Spring 容器 ✓✓

★ 最后一条最实在 ✓ ——本项目的单元测试全是这么做的 ✓

5.3 AOP:代理模式的工业级应用 ★★★ ​

AOP 要解决的问题 ✓:横切关注点(日志、事务、鉴权、重试、限流)✗ —— 它们散布在所有业务方法上,写进业务里就是重复代码 ✗✗

✓ 用 AOP 抽出来:

java
@Transactional
public void createOrder(...) { ... }      // ★ 业务只加一个注解,事务由代理织入

★ 底层就是代理(本节前面讲过) ✓✓:

情况用的代理
目标实现了接口 ✓JDK 动态代理
没有接口CGLIB(生成子类)

★★ 由此推出的三个"必须知道" ✓✓:

#结论原因
①this.method() 内部调用不走 AOP ✗✗this 是原始对象,不是代理 ✓
②final 类/方法无法被 CGLIB 代理 ✗CGLIB 靠继承 ✓
③私有方法上的 AOP 注解无效 ✗代理无法覆盖私有方法 ✓

★ 第 ① 条的三种解法 ✓✓:

解法说明
把方法拆到另一个 Bean ★★★最干净 ✓
注入自己(@Lazy 自注入)能工作但不优雅 ⚠️
用 AopContext.currentProxy()要开 exposeProxy ⚠️

5.4 模板方法:JdbcTemplate 为什么好用 ★★★ ​

对比 JDBC 原生写法 ✗:

java
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
    conn = dataSource.getConnection();
    ps = conn.prepareStatement(sql);
    ps.setLong(1, id);
    rs = ps.executeQuery();
    if (rs.next()) { return mapRow(rs); }        // ★ 只有这两行是"你的逻辑"
    return null;
} catch (SQLException e) {
    throw new DataAccessException(e);            // 要转换异常
} finally {
    closeQuietly(rs); closeQuietly(ps); closeQuietly(conn);   // 要释放资源
}

★ 其中只有两行是业务逻辑,其余都是"固定流程" ✗

✓ 用 JdbcTemplate:

java
return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> mapRow(rs), id);

★ 它把模板方法的三个特点全体现了 ✓✓:

特点在 JdbcTemplate 里
流程固定 ✓获取连接 → 执行 → 转换异常 → 释放
变化点开放 ✓SQL 与 RowMapper 是你的
异常统一 ✓SQLException → DataAccessException

★ 同类还有:RedisTemplate、RestTemplate、TransactionTemplate、KafkaTemplate ✓ ——"XXXTemplate"这个名字本身就是提示 ✓✓

5.5 观察者:Spring 事件 ★★ ​

用法 ✓:

java
// 发布
applicationEventPublisher.publishEvent(new OrderCreatedEvent(orderNo));

// 监听
@EventListener
public void onOrderCreated(OrderCreatedEvent event) { pointsService.add(event.orderNo()); }

★ 相比"业务里直接调用"的收益 ✓✓:

收益说明
业务不认下游 ✓✓加积分、发通知、埋点都不改发布方
可以异步 ✓加 @Async 就丢到线程池
可以事务后触发 ★★@TransactionalEventListener(phase = AFTER_COMMIT) ——保证"事务成功才发事件" ✓✓

★ 最后一条很实用 ✓:

"下单成功后发短信"如果写在事务里,事务回滚了短信却发出去了 ✗✗

用 AFTER_COMMIT 就能避免 ✓✓

★ 但它不是消息队列 ✗:

局限说明
只在同一个 JVM 内 ✗跨进程要上 MQ(《消息队列基础与选型》)
默认同步执行 ⚠️要显式加 @Async
没有持久化 ✗进程崩了事件就丢了

6. 递进示例 ​

6.1 最小示例:看一个 Bean 的定义 ​

java
@Configuration
public class AppConfig {
    @Bean
    public OrderRepository orderRepository(DataSource ds) {       // ★ 工厂方法
        return new JdbcOrderRepository(ds);
    }
}

★ @Bean 就是工厂方法模式的声明式写法 ✓

6.2 业务示例:事务事件 ​

java
@Transactional
public String createOrder(...) {
    String orderNo = repository.save(...);
    publisher.publishEvent(new OrderCreatedEvent(orderNo));    // ★ 事务提交后才真正发送
    return orderNo;
}

6.3 反例:在业务里直接调用所有下游 ​

见 《设计模式:行为型》 的 5.2 ✗

7. 完整可运行代码与实测 ​

本节无新增实验代码 ——它的"实测"都来自本项目已有的部分 ✓

★ 三个已有的实测证据 ✓:

证据出处
手写 @Configuration 时容器里 9 个 Bean;引入自动配置后 160 个《Spring Boot 自动配置与项目结构》 ✓
类内部 this.method() 调用导致 @Transactional 失效《Spring:AOP 与事务》 ✓
上下文复用让集成测试从 4.038 s 降到 0.057 s(单例 Bean 的收益)T1-017 ✓

★ 第三条值得展开 ✓✓:

Spring 默认单例 Bean → 一次启动、多次复用 → 测试类之间可直接复用同一个容器,耗时降了 70 倍 ✓✓

这正是"单例模式"在框架层面的最大价值 ✓

8. Debug 观察点 ​

  1. 用 applicationContext.getBeanDefinitionNames():看容器里有多少 Bean ★;
  2. 对一个 Service 打印 getClass().getName():观察它是 XxxService 还是 XxxService$$SpringCGLIB$$0 ★★★;
  3. 在 AOP 切面里打断点:观察它只在"从外部调用"时进入 ✓;
  4. 把 @Transactional 的方法改成 private:观察事务不生效 ✗。

9. 常见错误 ​

#错误说明
1this.xxx() 调用期待 AOP 生效 ✗✗代理被绕过(5.3)✗
2在 final 类上用 CGLIB 代理 ✗无法继承 ✓
3使用字段注入(@Autowired 直接写在字段上) ✗依赖不显式、无法 final、不便测试 ✓
4把事务里的通知直接写在业务方法里 ✗✗事务回滚了通知已发出 ✓
5把 Spring 事件当消息队列用 ✗✗只在同 JVM 内、进程崩溃会丢 ✓
6自己 new 一个 Service 却期望 Spring 的注解生效 ✗它不在容器里 ✓

10. 动手实验 ​

目标:把"模式"与"框架"对应起来,并亲手触发一次"代理失效"。

步骤:

  1. 打印所有 Bean 名,数一数有多少 ✓;
  2. 打印一个 @Transactional Service 的实际类名:观察 CGLIB 代理 ★★;
  3. 写一个 this.method() 的内部调用,验证 AOP 失效 ★★★;
  4. 用 AFTER_COMMIT 事件替换"事务内直接发通知" ✓;
  5. 把字段注入改成构造器注入,并为它写一个不启动容器的单元测试 ✓。

验收方式:能说出"为什么 this.method() 不走代理"。

扩展任务:用 AOP 实现一个"接口耗时统计 + 慢调用告警":

  • 自定义注解 @Monitor ✓;
  • 切面里统计耗时,超过阈值打 WARN 日志 ✓;
  • 说明:为什么这个功能不该写进每个业务方法(提示:横切关注点)★★

11. 练习题 ​

  1. 判断:Spring 的单例 Bean 就是"单例模式"的直接应用。(对 / 错)
  2. 填空:BeanFactory 是 ______ 模式;AOP 的底层是 ______ 模式;JdbcTemplate 是 ______ 模式。
  3. 简答:为什么 this.method() 调用不会触发 AOP?
  4. (代码阅读)
java
@Service
public class OrderService {
    @Autowired private OrderService self;      // 注入自己

    @Transactional
    public void outer() { self.inner(); }

    @Transactional(propagation = REQUIRES_NEW)
    public void inner() { ... }
}

为什么要注入自己? 5. (设计)用 Spring 事件实现"下单后异步通知":要求事务提交后才发,且不影响主流程耗时。

参考答案(要点)
  1. 对。Spring 的单例 Bean 就是容器管理的单例 ✓ —— 但比手写单例更好:生命周期由容器管、可配置作用域、可被替换 ✓★
  2. 工厂(+ 注册表);代理;模板方法 ✓
  3. 因为 this 指向原始对象,而不是代理对象 ✗ ——AOP 的通知织在代理上,绕过代理等于绕过 AOP ✓★
  4. 为了让 inner() 走代理(从而实现 REQUIRES_NEW 开新事务)✓ —— 直接 this.inner() 会共用外层事务,REQUIRES_NEW 失效 ✗
  5. 要点:@TransactionalEventListener(phase = AFTER_COMMIT) + @Async ✓ 再加一个专用线程池,避免与其他异步任务互相影响 ✓

12. 掌握标准 ​

  • [ ] 能把 Spring 核心组件对应到具体模式 ★★★;
  • [ ] 知道 IoC/DI = 工厂 + 依赖倒置,并能说出构造器注入的三个好处 ★★★;
  • [ ] 知道 AOP 是代理,以及"内部调用失效"的原因 ★★★;
  • [ ] 知道 JdbcTemplate 是模板方法,并知道"XXXTemplate"的命名暗示 ★★;
  • [ ] 知道 Spring 事件的收益与局限 ★★;
  • [ ] 知道 AFTER_COMMIT 能避免"事务回滚但通知已发" ★★。

13. 扩展资源 ​

14. 下一步学习 ​

设计模式部分到此完成。接下来进入 S4 的后半段:Redis、消息队列、Nginx、微服务与容器。

《Redis 数据结构与常用命令》。

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引