设计模式在 MyBatis 框架中的应用
MyBatis 这个框架,我用了快十年了。说实话,刚接触那会儿只觉得它比 JDBC 省事,后来读源码才发现——这玩意儿简直就是设计模式的教科书。今天咱们就挑四个最典型的模式聊聊:Builder、工厂、代理、模板。每个模式我都踩过坑,也从中捞到过不少好处。
一、Builder 模式:SqlSessionFactoryBuilder
先问个问题:你平时怎么创建 SqlSessionFactory?
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(inputStream);
看到没?SqlSessionFactoryBuilder 就是个典型的 Builder。它把配置文件的解析、环境变量的组装、插件的注册……这些复杂步骤全封装起来了。
为什么不用构造器直接 new?
你想想看,一个 SqlSessionFactory 要依赖数据源、事务管理器、映射器、缓存……参数少说十几个。要是全塞进构造器里,调用方得疯。Builder 模式的好处就是——你可以分步设置,最后一步 build() 才真正创建对象。
核心要点:Builder 模式解决的是「复杂对象构建」问题。它把构建过程和表示分离,同样的构建过程可以创建不同的表示。
我在项目中遇到过一种情况:需要根据不同的环境(开发、测试、生产)创建不同的 SqlSessionFactory。用 Builder 就特别爽——只需要换配置文件路径,build() 方法内部自动解析,完全不用改代码。
个人习惯:我一般会把 SqlSessionFactoryBuilder 的创建过程封装成一个工具类,比如叫 MyBatisUtil。这样项目中所有地方都通过这个工具类获取 SqlSessionFactory,统一管理,避免重复解析配置文件。
二、工厂模式:SqlSessionFactory
Builder 造出来的是啥?是 SqlSessionFactory。而这个 SqlSessionFactory 本身,又是一个工厂。
SqlSession session = sqlSessionFactory.openSession();
这行代码背后,SqlSessionFactory 负责创建 SqlSession。SqlSession 是什么?说白了就是数据库会话,你所有的增删改查都得通过它。
工厂模式在这里的价值:
- 客户端不需要知道 SqlSession 的具体实现类
- 工厂可以控制 SqlSession 的生命周期(比如是否自动提交、是否使用二级缓存)
- 切换实现类时,客户端代码零改动
我记得有一次,项目要从单数据源切换到读写分离。原来的 SqlSessionFactory 是 DefaultSqlSessionFactory,现在要换成支持多数据源的。我只需要在工厂里做文章——重写 openSession() 方法,根据当前线程上下文选择数据源。调用方完全无感知。
避坑指南:我曾经犯过一个错——每次请求都 new 一个 SqlSessionFactory。这玩意儿创建成本很高(要解析配置文件、初始化环境),而且线程安全。正确的做法是:一个应用只创建一个 SqlSessionFactory 实例,用单例模式管理。
三、代理模式:MapperProxy
这个是我觉得 MyBatis 最骚的操作。你写个接口,连实现类都没有,就能直接调用方法?
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1);
这背后就是 MapperProxy 在干活。它用 JDK 动态代理,在运行时生成接口的代理对象。当你调用 selectById 时,代理对象拦截方法调用,解析出对应的 SQL,然后执行。
代理模式的核心逻辑:
- getMapper() 返回一个代理对象(MapperProxy)
- 调用接口方法时,代理对象拦截
- 根据方法名和返回值类型,从配置中查找对应的 MappedStatement
- 执行 SQL,返回结果
说白了,你写的接口方法名就是 SQL 的 ID,参数就是 SQL 的参数,返回值就是结果集的映射。这一切都是代理帮你搞定的。
深度理解:MapperProxy 用的是 JDK 动态代理,所以你的 Mapper 必须是接口。如果你用类,就得用 CGLIB。MyBatis 默认优先用 JDK 动态代理,因为性能更好,而且不需要额外依赖。
我在项目中遇到过一个问题:自定义了一个拦截器,想给所有 Mapper 方法加日志。一开始我直接修改 Mapper 接口,后来发现不对——接口没有实现类,你改接口没用。正确的做法是实现 MyBatis 的 Interceptor 接口,在代理层做手脚。嗯,这里要注意:拦截器要小心处理参数和返回值,别把代理对象搞坏了。
四、模板方法模式:BaseExecutor
最后说说 BaseExecutor。这个类我读源码时印象特别深——它把 SQL 执行的骨架搭好了,具体步骤留给子类去实现。
public abstract class BaseExecutor implements Executor {
@Override
public <E> List<E> query(...) {
// 1. 检查二级缓存
// 2. 检查一级缓存
// 3. 如果缓存没有,调用 doQuery()
// 4. 存入缓存
// 5. 返回结果
}
protected abstract <E> List<E> doQuery(...);
}
看到没?query() 是模板方法,它定义了算法骨架:先查缓存,缓存没有才查数据库,查完再放回缓存。而 doQuery() 是抽象方法,留给子类实现具体的数据库操作。
模板方法模式的好处:
- 复用公共逻辑(缓存处理、事务管理)
- 子类只需关注差异化实现(比如 SimpleExecutor 直接查,ReuseExecutor 复用 Statement)
- 骨架变更时,所有子类自动生效
我记得有一次要优化批量插入性能。默认的 SimpleExecutor 每次执行都创建新的 Statement,太慢了。我参考 ReuseExecutor 的思路,自己写了个 BatchExecutor,重写 doQuery() 和 doUpdate(),用批量提交的方式执行 SQL。性能提升了 10 倍不止。
个人经验:模板方法模式特别适合「流程固定、步骤可变」的场景。比如你写一个数据同步工具,同步流程是固定的(读取-转换-写入),但每个步骤的实现可能不同(读 MySQL、读 Oracle、写 ES)。用模板方法,骨架写一次,具体实现随便换。
知识体系总览
为了让你看得更清楚,我画了张图,把这四种模式在 MyBatis 中的位置和关系串起来:
四种模式对比总结
| 设计模式 | MyBatis 中的角色 | 核心作用 | 我踩过的坑 |
|---|---|---|---|
| Builder | SqlSessionFactoryBuilder | 分步构建复杂对象 | 重复创建 Builder,浪费资源 |
| 工厂 | SqlSessionFactory | 统一创建 SqlSession | 没考虑多数据源场景 |
| 代理 | MapperProxy | 接口方法映射到 SQL 执行 | 拦截器误改了代理对象状态 |
| 模板 | BaseExecutor | 定义执行骨架,子类实现细节 | 重写 doQuery 时忘了处理缓存 |
说实话,学设计模式最好的方式就是读框架源码。MyBatis 的代码量不大,但设计模式用得恰到好处。你把它吃透了,以后自己写框架或者重构代码,脑子里自然就有这些模式的身影。
一句话总结:Builder 造工厂,工厂产会话,代理拦调用,模板定流程。四个模式串起来,就是 MyBatis 的核心骨架。