主题
界面
主题
前五节讲了设计规则与模式;本节把它们映射到你每天都在用的 Spring 里:
你以为在"学设计模式",其实你一直在"用设计模式" ✓
Spring 本身就是"设计模式的教科书级实现"★ 本节的价值 ✓✓:
既能"看懂框架为什么这么设计",也能"在业务里正确地用同一套模式" ✓
JdbcTemplate 是模板方法的教科书实现 ★★;@Component / @Autowired 吗?(《Spring:IOC 与依赖注入》)✓先看一个"知道模式之后才看得懂"的 Spring 行为 ✗:
现象:在 ShopService 里调用 this.refund(),加在 refund() 上的 @Transactional 不生效 ✗
原因:this 指向"原始对象",不是"代理对象" —— 事务通知根本没被触发 ✗✗★ 如果你知道"AOP = 代理模式",这个现象是一眼能看穿的 ✓✓ 不知道的人则会以为是"Spring 的 bug" ✗(本模块在 《Spring:AOP 与事务》 实测过这个场景)
| Spring 组件 | 用到的模式 | 说明 |
|---|---|---|
BeanFactory / ApplicationContext ★★★ | 工厂 + 注册表 | getBean(name) 就是注册式工厂 |
| 单例 Bean(默认) ★★★ | 单例 | 由容器管理生命周期,而不是手写 getInstance() |
@Autowired 注入 ★★★ | 依赖倒置 | 业务类只依赖接口,实现由容器装配 |
AOP(@Transactional) ★★★ | 代理 | JDK 动态代理或 CGLIB |
JdbcTemplate ★★★ | 模板方法 | 固定"连接/异常/释放",开放"SQL 与映射" |
ApplicationEvent ★★ | 观察者 | 业务与副作用解耦 |
ResourceLoader / ViewResolver ★★ | 策略 | 不同实现按配置替换 |
HandlerInterceptor / Filter ★★ | 责任链 | 请求在链上依次处理 |
BeanPostProcessor ★ | 模板方法 + 扩展点 | 在 Bean 生命周期里插钩子 |
★ 一句话 ✓✓:
Spring 的核心思想 = 工厂 + 依赖倒置 + 代理 ✓
其余都是在为这三样服务 ✓
没有 Spring 时 ✗:
class OrderService {
private final OrderRepository repo = new MySQLOrderRepository(); // ✗ 写死
}★ 三个后果 ✓:
| # | 后果 |
|---|---|
| ① 无法单元测试 ✗✗ | 必须连真实数据库 |
| ② 无法替换实现 ✗ | 换库要改业务类 |
| ③ 依赖关系藏在代码里 ✗ | 看类图也看不出全貌 |
有 Spring 时 ✓:
@Service
public class OrderService {
private final OrderRepository repo; // ← 接口
public OrderService(OrderRepository repo) { this.repo = repo; } // ← 构造注入
}★ Spring 替你做了两件事 ✓✓:
| 做的事 | 对应模式 |
|---|---|
| 创建与装配对象 | 工厂 + 单例 ✓ |
| 让业务依赖接口 | 依赖倒置 ✓ |
★ 为什么推荐"构造器注入" ✓✓:
| 理由 | 说明 |
|---|---|
| 依赖不可变 ★★ | final 字段 + 构造器注入 ✓ |
| 依赖显式 ★★ | 看构造器就知道它依赖什么(字段注入看不出来)✗ |
| 便于测试 ★★★ | 直接 new OrderService(fakeRepo),不需要 Spring 容器 ✓✓ |
★ 最后一条最实在 ✓ ——本项目的单元测试全是这么做的 ✓
AOP 要解决的问题 ✓:横切关注点(日志、事务、鉴权、重试、限流)✗ —— 它们散布在所有业务方法上,写进业务里就是重复代码 ✗✗
✓ 用 AOP 抽出来:
@Transactional
public void createOrder(...) { ... } // ★ 业务只加一个注解,事务由代理织入★ 底层就是代理(本节前面讲过) ✓✓:
| 情况 | 用的代理 |
|---|---|
| 目标实现了接口 ✓ | JDK 动态代理 |
| 没有接口 | CGLIB(生成子类) |
★★ 由此推出的三个"必须知道" ✓✓:
| # | 结论 | 原因 |
|---|---|---|
| ① | this.method() 内部调用不走 AOP ✗✗ | this 是原始对象,不是代理 ✓ |
| ② | final 类/方法无法被 CGLIB 代理 ✗ | CGLIB 靠继承 ✓ |
| ③ | 私有方法上的 AOP 注解无效 ✗ | 代理无法覆盖私有方法 ✓ |
★ 第 ① 条的三种解法 ✓✓:
| 解法 | 说明 |
|---|---|
| 把方法拆到另一个 Bean ★★★ | 最干净 ✓ |
注入自己(@Lazy 自注入) | 能工作但不优雅 ⚠️ |
用 AopContext.currentProxy() | 要开 exposeProxy ⚠️ |
JdbcTemplate 为什么好用 ★★★ 对比 JDBC 原生写法 ✗:
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:
return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> mapRow(rs), id);★ 它把模板方法的三个特点全体现了 ✓✓:
| 特点 | 在 JdbcTemplate 里 |
|---|---|
| 流程固定 ✓ | 获取连接 → 执行 → 转换异常 → 释放 |
| 变化点开放 ✓ | SQL 与 RowMapper 是你的 |
| 异常统一 ✓ | SQLException → DataAccessException |
★ 同类还有:RedisTemplate、RestTemplate、TransactionTemplate、KafkaTemplate ✓ ——"XXXTemplate"这个名字本身就是提示 ✓✓
用法 ✓:
// 发布
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 |
| 没有持久化 ✗ | 进程崩了事件就丢了 |
@Configuration
public class AppConfig {
@Bean
public OrderRepository orderRepository(DataSource ds) { // ★ 工厂方法
return new JdbcOrderRepository(ds);
}
}★ @Bean 就是工厂方法模式的声明式写法 ✓
@Transactional
public String createOrder(...) {
String orderNo = repository.save(...);
publisher.publishEvent(new OrderCreatedEvent(orderNo)); // ★ 事务提交后才真正发送
return orderNo;
}见 《设计模式:行为型》 的 5.2 ✗
本节无新增实验代码 ——它的"实测"都来自本项目已有的部分 ✓
★ 三个已有的实测证据 ✓:
| 证据 | 出处 |
|---|---|
手写 @Configuration 时容器里 9 个 Bean;引入自动配置后 160 个 | 《Spring Boot 自动配置与项目结构》 ✓ |
类内部 this.method() 调用导致 @Transactional 失效 | 《Spring:AOP 与事务》 ✓ |
| 上下文复用让集成测试从 4.038 s 降到 0.057 s(单例 Bean 的收益) | T1-017 ✓ |
★ 第三条值得展开 ✓✓:
Spring 默认单例 Bean → 一次启动、多次复用 → 测试类之间可直接复用同一个容器,耗时降了 70 倍 ✓✓
这正是"单例模式"在框架层面的最大价值 ✓
applicationContext.getBeanDefinitionNames():看容器里有多少 Bean ★;getClass().getName():观察它是 XxxService 还是 XxxService$$SpringCGLIB$$0 ★★★;@Transactional 的方法改成 private:观察事务不生效 ✗。| # | 错误 | 说明 |
|---|---|---|
| 1 | this.xxx() 调用期待 AOP 生效 ✗✗ | 代理被绕过(5.3)✗ |
| 2 | 在 final 类上用 CGLIB 代理 ✗ | 无法继承 ✓ |
| 3 | 使用字段注入(@Autowired 直接写在字段上) ✗ | 依赖不显式、无法 final、不便测试 ✓ |
| 4 | 把事务里的通知直接写在业务方法里 ✗✗ | 事务回滚了通知已发出 ✓ |
| 5 | 把 Spring 事件当消息队列用 ✗✗ | 只在同 JVM 内、进程崩溃会丢 ✓ |
| 6 | 自己 new 一个 Service 却期望 Spring 的注解生效 ✗ | 它不在容器里 ✓ |
目标:把"模式"与"框架"对应起来,并亲手触发一次"代理失效"。
步骤:
@Transactional Service 的实际类名:观察 CGLIB 代理 ★★;this.method() 的内部调用,验证 AOP 失效 ★★★;AFTER_COMMIT 事件替换"事务内直接发通知" ✓;验收方式:能说出"为什么 this.method() 不走代理"。
扩展任务:用 AOP 实现一个"接口耗时统计 + 慢调用告警":
@Monitor ✓;BeanFactory 是 ______ 模式;AOP 的底层是 ______ 模式;JdbcTemplate 是 ______ 模式。this.method() 调用不会触发 AOP?@Service
public class OrderService {
@Autowired private OrderService self; // 注入自己
@Transactional
public void outer() { self.inner(); }
@Transactional(propagation = REQUIRES_NEW)
public void inner() { ... }
}为什么要注入自己? 5. (设计)用 Spring 事件实现"下单后异步通知":要求事务提交后才发,且不影响主流程耗时。
this 指向原始对象,而不是代理对象 ✗ ——AOP 的通知织在代理上,绕过代理等于绕过 AOP ✓★inner() 走代理(从而实现 REQUIRES_NEW 开新事务)✓ —— 直接 this.inner() 会共用外层事务,REQUIRES_NEW 失效 ✗@TransactionalEventListener(phase = AFTER_COMMIT) + @Async ✓ 再加一个专用线程池,避免与其他异步任务互相影响 ✓JdbcTemplate 是模板方法,并知道"XXXTemplate"的命名暗示 ★★;AFTER_COMMIT 能避免"事务回滚但通知已发" ★★。设计模式部分到此完成。接下来进入 S4 的后半段:Redis、消息队列、Nginx、微服务与容器。
暂无