主题
框架源码里的设计模式:Spring、MyBatis 与 JDK
本文是设计模式系统学习系列的 L3 实战篇。前置:工厂家族、代理与装饰器。 学完可以配合面试题食用:Spring 中的设计模式、Spring Bean 生命周期
为什么模式要放在源码里学
背 23 种 GoF 模式的定义,面试时一条条列出来,面试官听完大概率面无表情。为什么呢?因为"你知道工厂模式"和"你在 BeanFactory 源码里见过工厂模式"是两回事。
更好的学习路径是:先把框架当作模式的使用案例集,看到源码里的某段结构,认出"哦,这里是个模板方法",然后回过头来理解模式的适用场景。模式是地图,框架源码是实景导航,没有地图容易迷路,没有实景地图就是一张废纸。
Spring 里的模式地图
Spring 是 Java 生态里设计模式密度最高的框架之一,几乎每种 GoF 高频模式都能在 Spring 源码里找到对应实现。
工厂模式:BeanFactory
从最核心的说起。BeanFactory 接口定义了 IoC 容器的基本行为,而 ApplicationContext 扩展了它。ClassPathXmlApplicationContext、AnnotationConfigApplicationContext 都是不同的工厂方法实现。 Spring 还用 FactoryBean 模式让用户自定义 Bean 的创建逻辑。FactoryBean 和 BeanFactory 名字像但职责不同,前者是用户在容器里注册的一个"工厂对象",后者是容器本身。区分这两个的关键在于职责边界。
代理模式:AOP
Spring AOP 的底层就是代理。接口有代理时用 JDK 动态代理,没有接口时用 CGLIB。Spring Boot 2.x 默认 CGLIB,因为现代项目里接口不一定总是存在(比如 @Service 直接写在类上)。
从源码角度看,DefaultAopProxyFactory 根据条件选择 JdkDynamicAopProxy 还是 ObjenesisCglibAopProxy,创建代理后把 MethodInterceptor 链挂上去。调用方法时,invoke 方法依次执行拦截器链,最后反射调用目标方法。
模板方法:JdbcTemplate
JdbcTemplate 的核心就在 execute() 方法里:打开连接 -> 创建 Statement -> 执行 -> 解析结果集 -> 关闭资源。这个骨架是固定的,唯一变化的部分——结果集如何映射到对象——通过 RowMapper 回调暴露给调用方。
这就是模板方法模式的精髓:骨架固定,钩子开放。RestTemplate、JmsTemplate、TransactionTemplate 都是同一思路。
观察者模式:ApplicationEvent
Spring 的事件机制是观察者模式的标准化实现。ApplicationEventPublisher 是 Subject,@EventListener 注解标记的方法就是 Observer。@TransactionalEventListener 更进一步,允许在事务提交后才发送事件,避免"事务还没提交,消费者已经去查数据库了,查到的是旧数据"。
责任链:拦截器与过滤器
Spring MVC 的 HandlerInterceptor 链和 Filter 链都是责任链。Filter 链是 Servlet 容器级别的,Interceptor 链是 Spring MVC 级别的,两者的装配顺序都可以通过 @Order 控制。
链的每个节点可以决定"继续"还是"中断"。比如权限校验拦截器通过返回 false 或抛出异常来中断请求链。
适配器:HandlerAdapter
Spring MVC 里有多种 Handler 类型:@Controller 方法、HttpRequestHandler、SimpleControllerHandlerAdapter。HandlerAdapter 接口就是为了让 DispatcherServlet 统一调用这些不同类型的 Handler。每种 Handler 对应一个 Adapter 实现,这是典型的适配器模式。
MyBatis 里的模式
MyBatis 的代码量比 Spring 小很多,但模式密度不低。
建造者:SqlSessionFactoryBuilder
SqlSessionFactoryBuilder 读取配置文件(XML 或配置类),构建 SqlSessionFactory。这个构建过程涉及多个步骤:解析数据源、解析映射文件、初始化配置。XMLConfigBuilder 和 XMLMapperBuilder 都是建造者的一部分。
代理:Mapper 接口的无实现类调用
MyBatis 最让人困惑的地方:UserMapper 明明只是一个接口,没有实现类,为什么调用 userMapper.findById(1) 能正常工作?
答案在 MapperProxyFactory 里。MyBatis 为每个 Mapper 接口创建 JDK 动态代理,MapperProxy 的 invoke 方法根据方法名从 MappedStatement 缓存里找到对应的 SQL 配置,然后执行。下面是一个最小复刻:
java
// MyBatis Mapper 动态代理的最小复刻
public class MiniMapperProxy implements InvocationHandler {
private final Map<String, String> sqlMapping = new HashMap<>();
public MiniMapperProxy(Map<String, String> sqlMapping) {
this.sqlMapping = sqlMapping;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 跳过 Object 的方法
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
String sql = sqlMapping.get(method.getName());
if (sql == null) {
throw new RuntimeException("No SQL found for " + method.getName());
}
System.out.println("执行 SQL: " + sql + ",参数: " + Arrays.toString(args));
// 实际会走 SqlSession 执行并映射结果
return null;
}
@SuppressWarnings("unchecked")
public static <T> T createMapper(Class<T> mapperInterface, Map<String, String> sqlMapping) {
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class<?>[]{mapperInterface},
new MiniMapperProxy(sqlMapping)
);
}
}
// 使用
public interface UserMapper {
User findById(Long id);
List<User> findAll();
}
Map<String, String> mapping = new HashMap<>();
mapping.put("findById", "SELECT * FROM user WHERE id = ?");
mapping.put("findAll", "SELECT * FROM user");
UserMapper proxy = MiniMapperProxy.createMapper(UserMapper.class, mapping);
proxy.findById(1L); // 输出: 执行 SQL: SELECT * FROM user WHERE id = ?,参数: [1]核心逻辑就 50 行:接口没有实现类,但代理对象在运行时告诉了 JVM"这个接口的任何方法调用都由我来处理"。
模板方法:BaseExecutor
MyBatis 的 BaseExecutor 定义了执行 SQL 的骨架:doUpdate、doQuery、doFlushStatements 等抽象方法留给子类实现。SimpleExecutor 每次新建 Statement,ReuseExecutor 复用 PreparedStatement,BatchExecutor 批量执行。子类只需实现 doQuery,其他逻辑(缓存、事务管理、延迟加载)由父类封装。
装饰器:Cache 装饰链
MyBatis 的二级缓存通过装饰器模式实现。PerpetualCache 是基础实现,LruCache 装饰它加上 LRU 淘汰策略,FifoCache 装饰它加上 FIFO 策略,SerializedCache 装饰它加上序列化支持。装饰器可以任意嵌套,组合出不同缓存的特性。
JDK 标准库里的模式
JDK 本身也大量使用模式,而且是最权威的"官方示例"。
装饰器:IO 流
BufferedInputStream 持有一个 InputStream 引用,在调用 read() 时先读缓冲区,缓冲区空了才调底层 InputStream.read()。这就是最典型的装饰器:不改变接口,叠加功能。DataInputStream、PushbackInputStream、GZIPInputStream 都是装饰器。
适配器:InputStreamReader
InputStreamReader 把字节输入流适配成字符输入流。InputStream 和 Reader 是两个不同的类层次,用适配器连接起来。这是对象适配器的典型例子。
享元:Integer 缓存
Integer.valueOf(100) 返回的是缓存对象,Integer.valueOf(200) 也是。默认 -128 到 127 范围的 Integer 对象被缓存复用。String.intern() 也是享元,连接池、线程池都是享元思想在工程中的应用。
迭代器:集合框架
Collection 接口继承 Iterable,iterator() 方法返回 Iterator。ArrayList、HashSet、LinkedList 各有自己的迭代器实现,但对外暴露的是同一个接口。for-each 语法糖底层就是迭代器。
观察者(已废弃)
java.util.Observable 和 Observer 在 JDK 9 被标记为废弃,原因是它无法泛型化、线程安全也不够完善。替代方案是 PropertyChangeListener 或各种事件总线。
在项目里怎么讲清楚你用的模式
回答这个问题时,不是列"我知道工厂模式"就够了,而是:
- 遇到了什么问题:比如"支付渠道从 3 个加到 10 个,每次都要改 switch-case"
- 为什么选了这个模式:比如"策略模式把渠道算法抽成接口,新增渠道只需加一个实现类,符合 OCP"
- 代价是什么:比如"类从 3 个变成 10 个,但每个类的职责很清晰,维护成本没有上升"
模式不是答案,是分析工具。说清楚"不用模式会怎样"比"用了什么模式"更能体现理解深度。
常见误区与小结
- 误区 1:拿到问题就套模式。先写能跑的代码,遇到痛点再考虑模式,不要提前设计
- 误区 2:觉得模式是"框架的事",自己写业务代码用不上。业务代码里的 if-else 膨胀、职责混杂都是模式的使用场景
- 误区 3:JDK 动态代理一定要有接口,CGLIB 不需要接口但 final 方法不行,很多人在 AOP 里踩过这个坑
- 误区 4:模式不是一一对应的,一个框架实现可能同时包含多种模式(比如 Spring 的 AOP 同时是代理+模板方法+责任链)
- 误区 5:读源码从模式出发,不考虑版本差异。不同版本结构可能不同,以当前版本代码为准
小结:模式的工程价值不在"用了多少种",而在"能不能把框架源码看懂、能不能把业务代码写干净"。从框架源码里认模式,是性价比最高的学习方式,因为每一个模式的出现都有真实的工程背景。
下一篇预告:用模式重构:从 if-else 地狱到可扩展设计,用一个完整的订单创建方法,展现重构的四个步骤。
参考
参考:Spring Framework 源码
org.springframework.aop包、MyBatis 源码org.apache.ibatis.binding包、GoF《设计模式:可复用面向对象软件的基础》