Skip to content

框架源码里的设计模式:Spring、MyBatis 与 JDK

本文是设计模式系统学习系列的 L3 实战篇。前置:工厂家族代理与装饰器。 学完可以配合面试题食用:Spring 中的设计模式Spring Bean 生命周期

为什么模式要放在源码里学

背 23 种 GoF 模式的定义,面试时一条条列出来,面试官听完大概率面无表情。为什么呢?因为"你知道工厂模式"和"你在 BeanFactory 源码里见过工厂模式"是两回事。

更好的学习路径是:先把框架当作模式的使用案例集,看到源码里的某段结构,认出"哦,这里是个模板方法",然后回过头来理解模式的适用场景。模式是地图,框架源码是实景导航,没有地图容易迷路,没有实景地图就是一张废纸。

Spring 里的模式地图

Spring 是 Java 生态里设计模式密度最高的框架之一,几乎每种 GoF 高频模式都能在 Spring 源码里找到对应实现。

工厂模式:BeanFactory

从最核心的说起。BeanFactory 接口定义了 IoC 容器的基本行为,而 ApplicationContext 扩展了它。ClassPathXmlApplicationContextAnnotationConfigApplicationContext 都是不同的工厂方法实现。 Spring 还用 FactoryBean 模式让用户自定义 Bean 的创建逻辑。FactoryBeanBeanFactory 名字像但职责不同,前者是用户在容器里注册的一个"工厂对象",后者是容器本身。区分这两个的关键在于职责边界。

代理模式:AOP

Spring AOP 的底层就是代理。接口有代理时用 JDK 动态代理,没有接口时用 CGLIB。Spring Boot 2.x 默认 CGLIB,因为现代项目里接口不一定总是存在(比如 @Service 直接写在类上)。

从源码角度看,DefaultAopProxyFactory 根据条件选择 JdkDynamicAopProxy 还是 ObjenesisCglibAopProxy,创建代理后把 MethodInterceptor 链挂上去。调用方法时,invoke 方法依次执行拦截器链,最后反射调用目标方法。

模板方法:JdbcTemplate

JdbcTemplate 的核心就在 execute() 方法里:打开连接 -> 创建 Statement -> 执行 -> 解析结果集 -> 关闭资源。这个骨架是固定的,唯一变化的部分——结果集如何映射到对象——通过 RowMapper 回调暴露给调用方。

这就是模板方法模式的精髓:骨架固定,钩子开放。RestTemplateJmsTemplateTransactionTemplate 都是同一思路。

观察者模式:ApplicationEvent

Spring 的事件机制是观察者模式的标准化实现。ApplicationEventPublisher 是 Subject,@EventListener 注解标记的方法就是 Observer。@TransactionalEventListener 更进一步,允许在事务提交后才发送事件,避免"事务还没提交,消费者已经去查数据库了,查到的是旧数据"。

责任链:拦截器与过滤器

Spring MVC 的 HandlerInterceptor 链和 Filter 链都是责任链。Filter 链是 Servlet 容器级别的,Interceptor 链是 Spring MVC 级别的,两者的装配顺序都可以通过 @Order 控制。

链的每个节点可以决定"继续"还是"中断"。比如权限校验拦截器通过返回 false 或抛出异常来中断请求链。

适配器:HandlerAdapter

Spring MVC 里有多种 Handler 类型:@Controller 方法、HttpRequestHandlerSimpleControllerHandlerAdapterHandlerAdapter 接口就是为了让 DispatcherServlet 统一调用这些不同类型的 Handler。每种 Handler 对应一个 Adapter 实现,这是典型的适配器模式。

MyBatis 里的模式

MyBatis 的代码量比 Spring 小很多,但模式密度不低。

建造者:SqlSessionFactoryBuilder

SqlSessionFactoryBuilder 读取配置文件(XML 或配置类),构建 SqlSessionFactory。这个构建过程涉及多个步骤:解析数据源、解析映射文件、初始化配置。XMLConfigBuilderXMLMapperBuilder 都是建造者的一部分。

代理:Mapper 接口的无实现类调用

MyBatis 最让人困惑的地方:UserMapper 明明只是一个接口,没有实现类,为什么调用 userMapper.findById(1) 能正常工作?

答案在 MapperProxyFactory 里。MyBatis 为每个 Mapper 接口创建 JDK 动态代理,MapperProxyinvoke 方法根据方法名从 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 的骨架:doUpdatedoQuerydoFlushStatements 等抽象方法留给子类实现。SimpleExecutor 每次新建 Statement,ReuseExecutor 复用 PreparedStatement,BatchExecutor 批量执行。子类只需实现 doQuery,其他逻辑(缓存、事务管理、延迟加载)由父类封装。

装饰器:Cache 装饰链

MyBatis 的二级缓存通过装饰器模式实现。PerpetualCache 是基础实现,LruCache 装饰它加上 LRU 淘汰策略,FifoCache 装饰它加上 FIFO 策略,SerializedCache 装饰它加上序列化支持。装饰器可以任意嵌套,组合出不同缓存的特性。

JDK 标准库里的模式

JDK 本身也大量使用模式,而且是最权威的"官方示例"。

装饰器:IO 流

BufferedInputStream 持有一个 InputStream 引用,在调用 read() 时先读缓冲区,缓冲区空了才调底层 InputStream.read()。这就是最典型的装饰器:不改变接口,叠加功能。DataInputStreamPushbackInputStreamGZIPInputStream 都是装饰器。

适配器:InputStreamReader

InputStreamReader 把字节输入流适配成字符输入流。InputStreamReader 是两个不同的类层次,用适配器连接起来。这是对象适配器的典型例子。

享元:Integer 缓存

Integer.valueOf(100) 返回的是缓存对象,Integer.valueOf(200) 也是。默认 -128 到 127 范围的 Integer 对象被缓存复用。String.intern() 也是享元,连接池、线程池都是享元思想在工程中的应用。

迭代器:集合框架

Collection 接口继承 Iterableiterator() 方法返回 IteratorArrayListHashSetLinkedList 各有自己的迭代器实现,但对外暴露的是同一个接口。for-each 语法糖底层就是迭代器。

观察者(已废弃)

java.util.ObservableObserver 在 JDK 9 被标记为废弃,原因是它无法泛型化、线程安全也不够完善。替代方案是 PropertyChangeListener 或各种事件总线。

在项目里怎么讲清楚你用的模式

回答这个问题时,不是列"我知道工厂模式"就够了,而是:

  1. 遇到了什么问题:比如"支付渠道从 3 个加到 10 个,每次都要改 switch-case"
  2. 为什么选了这个模式:比如"策略模式把渠道算法抽成接口,新增渠道只需加一个实现类,符合 OCP"
  3. 代价是什么:比如"类从 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《设计模式:可复用面向对象软件的基础》

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1