Java-18 深入浅出 MyBatis源码中 9 大设计模式全景解析:从 SqlSessionFactory 到 PropertyTokenizer(2026

0 阅读22分钟

TL;DR

  • 场景:MyBatis 作为半自动 ORM 框架,承担参数映射、结果集映射、事务管理、缓存、Mapper 代理、插件扩展等复杂职责,是学习设计模式落地的经典样本。本文按"概念—案例—源码"三段式拆解 9 大设计模式。
  • 结论:MyBatis 不是为了用设计模式而用模式,而是把"复杂对象分层构建 / 接口代理转发 / 节点树拼接 / 装饰叠加 / 日志适配"这些具体问题,自然映射到了建造者、工厂、代理、组合、模板方法、装饰者、适配器等结构。
  • 产出:9 大模式 × 概念定义 × 最小可运行 Java 案例 × MyBatis 源码级落地解析(SqlSessionFactoryBuilder、MapperProxy、SqlNode、BaseExecutor、Cache 装饰链、Log 适配器、PropertyTokenizer),可直接作为源码阅读路线图。

请添加图片描述

MyBatis 中用到的设计模式总结

基本介绍

MyBatis 是一个非常经典的半自动 ORM 框架。它不像 Hibernate 那样完全屏蔽 SQL,而是把 SQL 的编写权交给开发者,同时负责完成参数映射、结果集映射、事务管理、缓存管理、Mapper 代理、插件扩展等工作。

也正因为 MyBatis 的职责比较复杂,它内部大量使用了设计模式。学习 MyBatis 中的设计模式,不只是为了记住"某个类用了某个模式",更重要的是理解 MyBatis 是如何组织复杂代码、如何解耦模块、如何保持扩展能力的。

MyBatis 中用到了如下设计模式:

  • 建造者模式:SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、Environment
  • 工厂模式:SqlSessionFactory、TransactionFactory、LogFactory、ObjectFactory
  • 单例模式:ErrorContext、LogFactory
  • 代理模式:MapperProxy、ConnectionLogger、Plugin
  • 组合模式:SqlNode、MixedSqlNode、ChooseSqlNode、IfSqlNode
  • 模板方法模式:BaseExecutor、SimpleExecutor、ReuseExecutor、BatchExecutor
  • 适配器模式:Log、Slf4jImpl、Log4jImpl、Jdk14LoggingImpl
  • 装饰者模式:Cache、LruCache、LoggingCache、SynchronizedCache
  • 迭代器模式:PropertyTokenizer

具体如下图所示:

MyBatis 9 大设计模式总览表

下面按照几个主要设计模式分别说明。

建造者模式

概念介绍

建造者模式的核心思想是:将一个复杂对象的构建过程与它的表示分离,使得同样的构建过程可以创建不同的表示。

简单来说,就是通过一个 Builder 对象,一步一步构建出最终对象。

当一个对象的创建过程比较复杂,不能只靠一个构造函数完成时,就可以考虑使用建造者模式。例如:

  • 对象属性很多
  • 对象创建过程有明显步骤
  • 创建过程中需要解析、校验、填充默认值
  • 调用方不应该直接关心复杂的构建细节

建造者模式和工厂模式有些相似,但关注点不同。

工厂模式更关注"创建哪一种对象"。

建造者模式更关注"一个复杂对象如何一步一步创建出来"。

简单案例

比如要组装一台电脑,我们需要:

  • 显示器
  • 鼠标
  • 键盘
  • 音响

如果直接使用构造函数,代码可能是这样的:

new WzkComputer("三星", "罗技", "罗技", "破喇叭");

这种写法的问题是参数含义不够清晰,参数顺序也容易写错。如果以后电脑配置项继续增加,构造函数会越来越复杂。

使用建造者模式之后,可以这样写:

WzkComputer wzkComputer = new WzkComputerBuilder()
        .installDisplay("三星")
        .installMouse("罗技")
        .installKeyboard("罗技")
        .installSound("破喇叭")
        .build();

这种写法可读性更高,每一步都能看出是在安装什么部件。

编写代码

WzkComputer

@Data
@AllArgsConstructor
@NoArgsConstructor
public class WzkComputer {

    private String displayer;
    private String mouse;
    private String keyword;
    private String sound;

}

WzkComputerBuilder

package icu.wzk.design.builder;

public class WzkComputerBuilder {

    private WzkComputer wzkComputer = new WzkComputer();

    public WzkComputerBuilder installDisplay(String displayName) {
        wzkComputer.setDisplayer(displayName);
        return this;
    }

    public WzkComputerBuilder installMouse(String mouseName) {
        wzkComputer.setMouse(mouseName);
        return this;
    }

    public WzkComputerBuilder installKeyboard(String keyboardName) {
        wzkComputer.setKeyword(keyboardName);
        return this;
    }

    public WzkComputerBuilder installSound(String soundName) {
        wzkComputer.setSound(soundName);
        return this;
    }

    public WzkComputer build() {
        return wzkComputer;
    }
}

WzkBuilderTest

package icu.wzk.design.builder;

public class WzkBuilderTest {

    public static void main(String[] args) {
        WzkComputer wzkComputer = new WzkComputerBuilder()
                .installDisplay("三星")
                .installMouse("罗技")
                .installKeyboard("罗技")
                .installSound("破喇叭")
                .build();

        System.out.println(wzkComputer);
    }

}

测试运行

执行上述的 WzkBuilderTest,控制台输出结果如下:

WzkComputer(displayer=三星, mouse=罗技, keyword=罗技, sound=破喇叭)
Process finished with exit code 0

对应的截图如下所示:

WzkBuilderTest IDEA 运行结果

MyBatis 中的体现

MyBatis 中最典型的建造者模式体现在 SqlSessionFactoryBuilder 构建 SqlSessionFactory 的过程中。

MyBatis 初始化时,需要解析大量配置,例如:

  • mybatis-config.xml
  • mapper.xml
  • properties
  • settings
  • typeAliases
  • plugins
  • objectFactory
  • environments
  • databaseIdProvider
  • typeHandlers
  • mappers
  • statement
  • resultMap
  • parameterMap
  • sql 片段

这些配置最终都会被组装到一个核心对象中:Configuration

Configuration 可以理解为 MyBatis 运行期的全局配置中心。后续创建 SqlSession、执行 SQL、获取 Mapper、处理缓存、加载插件,都会依赖它。

因此,Configuration 的构建过程非常复杂,不适合直接通过一个构造函数完成。MyBatis 使用了多层 Builder 来分工处理:

  • SqlSessionFactoryBuilder:入口构建器,负责创建 SqlSessionFactory
  • XMLConfigBuilder:负责解析 MyBatis 全局配置文件
  • XMLMapperBuilder:负责解析 Mapper XML 文件
  • XMLStatementBuilder:负责解析具体 SQL 语句节点
  • CacheBuilder:负责构建缓存对象

SqlSessionFactoryBuilder 多重 build() 重载方法

在 MyBatis 环境初始化过程中,SqlSessionFactoryBuilder 会调用 XMLConfigBuilder 读取全局配置文件和 Mapper 文件。

简化后的解析流程如下:

private void parseConfiguration(XNode root) {
    try {
        // 解析 <properties /> 标签
        propertiesElement(root.evalNode("properties"));

        // 解析 <settings /> 标签
        Properties settings = settingsAsProperties(root.evalNode("settings"));

        // 加载自定义 VFS 实现
        loadCustomVfs(settings);

        // 解析 <typeAliases /> 标签
        typeAliasesElement(root.evalNode("typeAliases"));

        // 解析 <plugins /> 标签
        pluginElement(root.evalNode("plugins"));

        // 解析 <objectFactory /> 标签
        objectFactoryElement(root.evalNode("objectFactory"));

        // 解析 <objectWrapperFactory /> 标签
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));

        // 解析 <reflectorFactory /> 标签
        reflectorFactoryElement(root.evalNode("reflectorFactory"));

        // 将 settings 配置设置到 Configuration 中
        settingsElement(settings);

        // 解析 <environments /> 标签
        environmentsElement(root.evalNode("environments"));

        // 解析 <databaseIdProvider /> 标签
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

其次,XMLConfigBuilder 在构建 Configuration 对象时,也会调用 XMLMapperBuilder 读取 Mapper 文件。

XMLMapperBuilder 又会继续使用 XMLStatementBuilder 读取和构建所有 SQL 语句。

整体流程大致如下:

SqlSessionFactoryBuilder
        |
        v
XMLConfigBuilder
        |
        v
Configuration
        |
        v
XMLMapperBuilder
        |
        v
XMLStatementBuilder
        |
        v
MappedStatement

可以看到,SqlSessionFactoryBuilder 并不是简单地 new 一个对象,而是把配置文件解析、Mapper 注册、SQL 构建、缓存构建、插件加载等复杂逻辑逐步完成。

MyBatis 初始化配置解析流程

建造者模式在 MyBatis 中的价值主要有三点:

第一,屏蔽复杂构建细节。调用方只需要调用 build(),不需要知道内部解析了多少配置。

第二,分层构建。不同 Builder 负责不同层级的解析和构建,职责清晰。

第三,方便扩展。后续新增配置项时,可以在对应 Builder 中扩展解析逻辑,而不需要破坏整体结构。

工厂模式

概念介绍

工厂模式也是创建型设计模式。

它的核心思想是:把对象创建逻辑封装起来,调用方不直接使用 new 创建对象,而是通过工厂类获取对象。

工厂模式常见形式包括:

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

MyBatis 中既有简单工厂的影子,也有工厂方法模式的应用。

简单工厂模式通常通过一个工厂类,根据不同参数返回不同对象。

工厂方法模式则更强调抽象,通常定义一个工厂接口,由不同实现类负责创建不同产品对象。

简单案例

假设有一个电脑生产商,最开始只能生产联想电脑,后来业务发展了,又可以生产惠普电脑。

如果调用方直接写:

new LenovoComputer();
new HpComputer();

那么调用方就需要知道具体产品类。

使用工厂模式后,调用方只需要告诉工厂要什么类型的电脑,具体创建逻辑由工厂负责。

WzkComputer

package icu.wzk.design.factory;

public abstract class WzkComputer {

    public abstract void start();

}

LenovoComputer

package icu.wzk.design.factory;

public class LenovoComputer extends WzkComputer {

    @Override
    public void start() {
        System.out.println("生产联想电脑");
    }
}

HpComputer

package icu.wzk.design.factory;

public class HpComputer extends WzkComputer {

    @Override
    public void start() {
        System.out.println("生产惠普电脑");
    }
}

ComputerFactory

package icu.wzk.design.factory;

public class ComputerFactory {

    public static WzkComputer createComputer(String type) {
        switch (type) {
            case "lenovo":
                return new LenovoComputer();
            case "hp":
                return new HpComputer();
            default:
                throw new IllegalArgumentException("Invalid computer type: " + type);
        }
    }

}

WzkComputerTest

package icu.wzk.design.factory;

public class WzkComputerTest {

    public static void main(String[] args) {
        ComputerFactory.createComputer("lenovo").start();
        ComputerFactory.createComputer("hp").start();
    }

}

控制台输出:

生产联想电脑
生产惠普电脑

MyBatis 中的体现

MyBatis 中执行 SQL 语句、获取 Mapper、管理事务的核心接口是 SqlSession

但是开发者并不会直接 new DefaultSqlSession(),而是通过 SqlSessionFactory 获取 SqlSession

SqlSessionFactory 接口方法定义 IDE 截图

SqlSessionFactory 是一个典型的工厂接口,它负责创建 SqlSession

简化结构如下:

public interface SqlSessionFactory {

    SqlSession openSession();

    SqlSession openSession(boolean autoCommit);

    SqlSession openSession(Connection connection);

    SqlSession openSession(TransactionIsolationLevel level);

}

可以看到,SqlSessionFactory 提供了很多重载方法,允许调用方根据不同参数创建不同配置的 SqlSession

默认实现类是 DefaultSqlSessionFactory

DefaultSqlSessionFactory 源码截图

DefaultSqlSessionFactory 内部会根据配置创建事务、执行器,然后再创建 DefaultSqlSession

简化逻辑如下:

private SqlSession openSessionFromDataSource(
        ExecutorType execType,
        TransactionIsolationLevel level,
        boolean autoCommit) {

    Transaction tx = null;

    try {
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);

        tx = transactionFactory.newTransaction(
                environment.getDataSource(),
                level,
                autoCommit
        );

        final Executor executor = configuration.newExecutor(tx, execType);

        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
    }
}

从这段逻辑可以看到,创建 SqlSession 并不是简单地创建一个对象,而是要先准备:

  • Environment
  • TransactionFactory
  • Transaction
  • Executor
  • DefaultSqlSession

这就是工厂模式的价值:把复杂对象的创建细节隐藏起来。

除了 SqlSessionFactory,MyBatis 中还有很多工厂类:

  • TransactionFactory:负责创建事务对象
  • ObjectFactory:负责创建普通 Java 对象
  • LogFactory:负责创建日志对象
  • ProxyFactory:负责创建延迟加载代理对象

例如事务工厂:

public interface TransactionFactory {

    void setProperties(Properties props);

    Transaction newTransaction(Connection conn);

    Transaction newTransaction(DataSource dataSource,
                               TransactionIsolationLevel level,
                               boolean autoCommit);
}

它有两个典型实现:

  • JdbcTransactionFactory
  • ManagedTransactionFactory

这样 MyBatis 不需要直接依赖某一个具体事务实现,而是通过 TransactionFactory 创建事务对象,从而降低了模块之间的耦合。

代理模式

概念介绍

代理模式的核心思想是:为目标对象提供一个代理对象,由代理对象控制对目标对象的访问。

代理模式常用于下面几类场景:

  • 延迟加载
  • 权限控制
  • 日志增强
  • 事务增强
  • 远程调用
  • 接口方法转发
  • AOP 拦截

代理模式分为静态代理和动态代理。

静态代理需要手写代理类。

动态代理可以在运行时生成代理对象,Java 中常见的动态代理方式包括:

  • JDK 动态代理
  • CGLIB 代理
  • Javassist 代理

简单案例

假设有一个用户服务接口:

public interface UserService {

    void queryUser();

}

真实实现类如下:

public class UserServiceImpl implements UserService {

    @Override
    public void queryUser() {
        System.out.println("查询用户");
    }
}

使用 JDK 动态代理:

import java.lang.reflect.Proxy;

public class WzkProxyTest {

    public static void main(String[] args) {
        UserService target = new UserServiceImpl();

        UserService proxy = (UserService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[]{UserService.class},
                (proxyObj, method, args1) -> {
                    System.out.println("方法执行前");
                    Object result = method.invoke(target, args1);
                    System.out.println("方法执行后");
                    return result;
                }
        );

        proxy.queryUser();
    }

}

输出结果:

方法执行前
查询用户
方法执行后

MyBatis 中的体现

MyBatis 中最典型的代理模式就是 Mapper 接口代理。

平时我们写 Mapper 时,一般只写接口:

public interface UserMapper {

    User selectById(Long id);

}

这个接口没有实现类,但是在业务代码中却可以直接调用:

User user = userMapper.selectById(1L);

原因就是 MyBatis 在运行时为 Mapper 接口生成了代理对象。

核心类包括:

  • MapperProxy
  • MapperProxyFactory
  • MapperRegistry

MapperProxy 实现了 InvocationHandler,它会拦截 Mapper 接口的方法调用。

简化逻辑如下:

public class MapperProxy<T> implements InvocationHandler {

    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }

        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
}

当调用:

userMapper.selectById(1L);

底层大致流程如下:

userMapper.selectById(1L)
        |
        v
MapperProxy.invoke()
        |
        v
MapperMethod.execute()
        |
        v
SqlSession.selectOne()
        |
        v
Executor.query()
        |
        v
JDBC 执行 SQL

也就是说,Mapper 接口本身没有实现类,MyBatis 通过动态代理拦截接口方法,再根据接口方法的全限定名找到对应的 SQL 语句。

例如:

com.wzk.mapper.UserMapper.selectById

这个方法名会和 Mapper XML 中的 statement id 对应起来:

<select id="selectById" resultType="User">
    select * from user where id = #{id}
</select>

所以 Mapper 方法能够执行,本质上不是因为有手写实现类,而是因为 MyBatis 在运行时创建了代理类。

除了 Mapper 代理,MyBatis 中还有其他代理应用:

  • ConnectionLogger:代理 JDBC Connection,用于打印日志
  • PreparedStatementLogger:代理 PreparedStatement,用于打印 SQL 参数
  • ResultSetLogger:代理 ResultSet,用于打印结果集日志
  • Plugin:MyBatis 插件机制,本质也是动态代理

MyBatis 插件机制的核心接口是 Interceptor

public interface Interceptor {

    Object intercept(Invocation invocation) throws Throwable;

    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

}

插件可以拦截 MyBatis 的核心对象,例如:

  • Executor
  • StatementHandler
  • ParameterHandler
  • ResultSetHandler

这使得开发者可以在 SQL 执行前后插入自定义逻辑,例如分页、审计、监控、数据权限等。

代理模式在 MyBatis 中的价值非常大。它让开发者只需要定义 Mapper 接口,而不需要写实现类,同时也让插件机制具备了很强的扩展能力。

组合模式

概念介绍

组合模式的核心思想是:把对象组合成树形结构,使客户端可以用一致的方式处理单个对象和组合对象。

组合模式适合处理层级结构,例如:

  • 文件和文件夹
  • 菜单和子菜单
  • XML 节点
  • 组织架构
  • 动态 SQL 节点

组合模式的关键是统一抽象。

不管是叶子节点,还是组合节点,都实现同一个接口。调用方不需要区分当前处理的是单个节点还是一组节点。

MyBatis 中的体现

MyBatis 中的动态 SQL 使用了组合模式。

常见动态 SQL 标签包括:

  • <if>
  • <choose>
  • <when>
  • <otherwise>
  • <trim>
  • <where>
  • <set>
  • <foreach>

这些标签最终都会被解析成不同的 SqlNode

SqlNode 是统一接口:

public interface SqlNode {

    boolean apply(DynamicContext context);

}

不同动态 SQL 标签有不同实现:

  • StaticTextSqlNode
  • TextSqlNode
  • IfSqlNode
  • ChooseSqlNode
  • MixedSqlNode
  • TrimSqlNode
  • WhereSqlNode
  • SetSqlNode
  • ForEachSqlNode

例如 MixedSqlNode 内部保存了多个 SqlNode

public class MixedSqlNode implements SqlNode {

    private final List<SqlNode> contents;

    public MixedSqlNode(List<SqlNode> contents) {
        this.contents = contents;
    }

    @Override
    public boolean apply(DynamicContext context) {
        contents.forEach(node -> node.apply(context));
        return true;
    }
}

这就是典型的组合模式。

例如下面这个动态 SQL:

<select id="selectUser" resultType="User">
    select * from user
    <where>
        <if test="name != null">
            and name = #{name}
        </if>
        <if test="age != null">
            and age = #{age}
        </if>
    </where>
</select>

解析后大致会形成下面这样的节点树:

MixedSqlNode
    |
    |-- StaticTextSqlNode: select * from user
    |
    |-- WhereSqlNode
            |
            |-- MixedSqlNode
                    |
                    |-- IfSqlNode: name != null
                    |
                    |-- IfSqlNode: age != null

执行动态 SQL 时,MyBatis 会从根节点开始调用 apply() 方法。每个节点根据自身逻辑决定是否向 SQL 中追加内容。

比如 IfSqlNode 会判断 test 条件是否成立。

WhereSqlNode 会处理 where 关键字,并自动去掉多余的 and 或 or。

ForEachSqlNode 会处理集合遍历。

ChooseSqlNode 会处理类似 Java 中 switch 的逻辑。

组合模式在这里的价值是:

  • 所有动态 SQL 节点都有统一接口
  • 单个节点和组合节点可以被一致处理
  • 动态 SQL 可以自然形成树形结构
  • 复杂 SQL 拼接逻辑被拆成多个节点对象

如果没有组合模式,MyBatis 可能需要用大量 if else 去判断不同 XML 标签,代码会非常混乱,也很难扩展新的动态 SQL 标签。

模板方法模式

概念介绍

模板方法模式的核心思想是:在父类中定义算法骨架,把某些具体步骤延迟到子类中实现。

也就是说,父类控制整体流程,子类只负责实现某些变化点。

模板方法模式适合下面几类场景:

  • 多个类有相同执行流程
  • 流程中的部分步骤不同
  • 希望统一控制主流程
  • 希望避免重复代码

MyBatis 中的体现

MyBatis 中的 Executor 使用了模板方法模式。

Executor 是 SQL 执行器,负责执行查询、更新、提交、回滚、缓存处理等操作。

核心接口是 Executor,抽象父类是 BaseExecutor,具体实现类包括:

  • SimpleExecutor
  • ReuseExecutor
  • BatchExecutor
  • CachingExecutor

其中 BaseExecutor 定义了 SQL 执行的基本流程。

例如查询逻辑中,BaseExecutor 会负责:

  • 判断执行器是否已经关闭
  • 处理一级缓存
  • 创建缓存 key
  • 查询本地缓存
  • 查询数据库
  • 处理延迟加载
  • 清理本地缓存
  • 维护查询栈

简化后的查询流程如下:

@Override
public <E> List<E> query(
        MappedStatement ms,
        Object parameter,
        RowBounds rowBounds,
        ResultHandler resultHandler,
        CacheKey key,
        BoundSql boundSql) throws SQLException {

    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }

    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }

    List<E> list;

    try {
        queryStack++;

        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;

        if (list != null) {
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        queryStack--;
    }

    return list;
}

真正访问数据库的逻辑会交给子类实现:

protected abstract <E> List<E> doQuery(
        MappedStatement ms,
        Object parameter,
        RowBounds rowBounds,
        ResultHandler resultHandler,
        BoundSql boundSql) throws SQLException;

不同 Executor 的实现方式不同:

SimpleExecutor 每次执行都会创建新的 Statement。

ReuseExecutor 会复用 Statement。

BatchExecutor 会批量执行 SQL。

它们的整体流程都一样,但具体执行策略不同。

这就是模板方法模式。

父类 BaseExecutor 负责统一流程,子类负责具体差异。

模板方法模式在 MyBatis 中的价值是:

  • 统一 SQL 执行主流程
  • 避免多个 Executor 重复编写相同逻辑
  • 允许不同子类扩展具体执行策略
  • 保证缓存、事务、异常上下文等逻辑一致

如果没有模板方法模式,SimpleExecutorReuseExecutorBatchExecutor 可能都要重复处理缓存、异常、查询栈、事务等逻辑,代码重复度会非常高。

适配器模式

概念介绍

适配器模式的核心思想是:把一个类的接口转换成客户端期望的另一个接口,使原本接口不兼容的类可以一起工作。

适配器模式常用于下面几类场景:

  • 系统需要兼容多个第三方实现
  • 第三方 API 不统一
  • 业务代码希望依赖统一接口
  • 不想让外部差异污染内部代码

MyBatis 中的体现

MyBatis 中的日志模块使用了适配器模式。

MyBatis 自己定义了统一的日志接口 Log

public interface Log {

    boolean isDebugEnabled();

    boolean isTraceEnabled();

    void error(String s, Throwable e);

    void error(String s);

    void debug(String s);

    void trace(String s);

    void warn(String s);

}

但是不同日志框架的 API 并不完全一样。

MyBatis 需要兼容多种日志实现,例如:

  • SLF4J
  • Apache Commons Logging
  • Log4j
  • Log4j2
  • JDK Logging
  • StdOutImpl
  • NoLoggingImpl

所以 MyBatis 为不同日志框架提供了不同适配器:

  • Slf4jImpl
  • JakartaCommonsLoggingImpl
  • Log4jImpl
  • Log4j2Impl
  • Jdk14LoggingImpl
  • StdOutImpl
  • NoLoggingImpl

这些实现类都实现了 MyBatis 自己的 Log 接口。

MyBatis 内部只依赖自己的 Log 接口,不直接依赖某一个具体日志框架。

比如 MyBatis 内部只需要这样写:

private static final Log log = LogFactory.getLog(SomeClass.class);

至于底层最终使用 SLF4J、Log4j2 还是 JDK Logging,业务代码并不关心。

适配器模式在这里的价值是:

  • 屏蔽不同日志框架的 API 差异
  • MyBatis 内部只面向统一接口编程
  • 用户可以自由选择日志实现
  • 后续扩展新的日志框架成本较低

装饰者模式

概念介绍

装饰者模式的核心思想是:在不改变原对象结构的情况下,动态地给对象增加额外功能。

它和代理模式有些相似,但侧重点不同。

代理模式更关注"控制访问"。

装饰者模式更关注"增强功能"。

装饰者模式通常会让装饰类和被装饰类实现同一个接口。这样装饰后的对象仍然可以当作原对象使用。

MyBatis 中的体现

MyBatis 中的缓存模块大量使用了装饰者模式。

核心接口是 Cache

public interface Cache {

    String getId();

    void putObject(Object key, Object value);

    Object getObject(Object key);

    Object removeObject(Object key);

    void clear();

    int getSize();

}

基础缓存实现是 PerpetualCache

它的底层本质上就是一个 HashMap

public class PerpetualCache implements Cache {

    private final String id;

    private final Map<Object, Object> cache = new HashMap<>();

    @Override
    public void putObject(Object key, Object value) {
        cache.put(key, value);
    }

    @Override
    public Object getObject(Object key) {
        return cache.get(key);
    }
}

但是一个完整的缓存系统通常不只是简单的 HashMap。

还需要支持很多增强能力,例如:

  • LRU 淘汰
  • FIFO 淘汰
  • 日志记录
  • 同步控制
  • 序列化
  • 定时清理
  • 阻塞缓存
  • 事务缓存

如果把这些功能全部写进 PerpetualCache,这个类会非常臃肿,而且很难维护。

MyBatis 的做法是用多个装饰器一层层包装基础缓存。

常见缓存装饰器包括:

  • LruCache
  • FifoCache
  • SoftCache
  • WeakCache
  • LoggingCache
  • SynchronizedCache
  • SerializedCache
  • ScheduledCache
  • BlockingCache
  • TransactionalCache

例如,一个缓存对象可能被包装成下面这样:

SynchronizedCache
    -> LoggingCache
        -> LruCache
            -> PerpetualCache

每一层装饰器只负责增强一种能力。

比如 LoggingCache 用于增加缓存命中率统计:

public class LoggingCache implements Cache {

    private final Cache delegate;
    protected int requests = 0;
    protected int hits = 0;

    public LoggingCache(Cache delegate) {
        this.delegate = delegate;
    }

    @Override
    public Object getObject(Object key) {
        requests++;
        final Object value = delegate.getObject(key);

        if (value != null) {
            hits++;
        }

        return value;
    }
}

它并不关心底层缓存是 HashMap、LRU 还是其他实现,只负责在调用前后增加日志统计能力。

装饰者模式在 MyBatis 缓存中的价值是:

  • 每个缓存增强功能都可以独立拆分
  • 不需要修改基础缓存类
  • 多个增强功能可以自由组合
  • 避免缓存类变成一个巨大的全能类

这是一种非常典型的"组合优于继承"的设计方式。

单例模式

概念介绍

单例模式的核心思想是:一个类在系统中只提供一个实例,并提供一个全局访问点。

单例模式适合用于下面几类对象:

  • 全局配置对象
  • 工具类对象
  • 线程上下文对象
  • 缓存管理对象
  • 日志工厂对象

需要注意的是,单例模式并不等于随便写 static。真正好的单例设计要考虑线程安全、生命周期、资源释放和测试隔离等问题。

MyBatis 中的体现

MyBatis 中比较典型的单例应用是 ErrorContextLogFactory

ErrorContext 用于保存当前线程中的错误上下文信息。

它不是整个 JVM 全局共享一个普通对象,而是使用了 ThreadLocal,保证每个线程都有自己的错误上下文。

简化结构如下:

public class ErrorContext {

    private static final ThreadLocal<ErrorContext> LOCAL =
            ThreadLocal.withInitial(ErrorContext::new);

    private ErrorContext() {
    }

    public static ErrorContext instance() {
        return LOCAL.get();
    }

}

这种设计非常适合 MyBatis。

因为 MyBatis 在执行 SQL、解析 Mapper、处理参数时可能发生异常,异常信息需要附带当前线程的上下文,但又不能和其他线程串数据。

所以 ErrorContext 采用了 ThreadLocal + 单例访问入口 的方式。

LogFactory 则用于创建 MyBatis 内部的日志对象。它内部会根据当前环境选择合适的日志实现。调用方不需要关心底层到底使用哪种日志框架,只需要通过统一入口获取日志对象。

迭代器模式

概念介绍

迭代器模式的核心思想是:提供一种顺序访问集合对象元素的方法,而不暴露集合对象的内部结构。

通常我们使用 Java 的 Iterator 时,其实就是在使用迭代器模式。

例如:

Iterator<String> iterator = list.iterator();

while (iterator.hasNext()) {
    String item = iterator.next();
    System.out.println(item);
}

调用方不需要知道 ArrayList 内部是数组,LinkedList 内部是链表,只需要通过统一方式遍历元素。

MyBatis 中的体现

MyBatis 中的 PropertyTokenizer 使用了迭代器思想。

它主要用于解析嵌套属性表达式。

例如:

user.address.city

这个表达式需要被一层层解析:

user
address
city

PropertyTokenizer 的简化结构如下:

public class PropertyTokenizer implements Iterator<PropertyTokenizer> {

    private String name;
    private String indexedName;
    private String index;
    private String children;

    public PropertyTokenizer(String fullname) {
        int delim = fullname.indexOf('.');

        if (delim > -1) {
            name = fullname.substring(0, delim);
            children = fullname.substring(delim + 1);
        } else {
            name = fullname;
            children = null;
        }
    }

    @Override
    public boolean hasNext() {
        return children != null;
    }

    @Override
    public PropertyTokenizer next() {
        return new PropertyTokenizer(children);
    }
}

例如解析:

PropertyTokenizer tokenizer = new PropertyTokenizer("user.address.city");

System.out.println(tokenizer.getName()); // user

if (tokenizer.hasNext()) {
    tokenizer = tokenizer.next();
    System.out.println(tokenizer.getName()); // address
}

if (tokenizer.hasNext()) {
    tokenizer = tokenizer.next();
    System.out.println(tokenizer.getName()); // city
}

PropertyTokenizer 的作用并不是遍历一个传统集合,而是把一个属性路径当作可迭代结构来处理。

这在 MyBatis 的参数映射、结果映射、反射取值中非常重要。

例如 MyBatis 需要根据字符串表达式:

user.address.city

一层层取出对象属性:

user.getAddress().getCity()

PropertyTokenizer 就负责把属性路径拆成可逐段处理的结构。

各设计模式在 MyBatis 中的总结

设计模式MyBatis 中的典型类主要作用
建造者模式SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder分阶段构建复杂配置对象
工厂模式SqlSessionFactory、TransactionFactory、ObjectFactory、LogFactory屏蔽对象创建细节
单例模式ErrorContext、LogFactory管理全局或线程级唯一对象
代理模式MapperProxy、ConnectionLogger、Plugin接口代理、日志增强、插件拦截
组合模式SqlNode、MixedSqlNode、ChooseSqlNode、IfSqlNode组织动态 SQL 节点树
模板方法模式BaseExecutor、SimpleExecutor、ReuseExecutor、BatchExecutor固定 SQL 执行流程,开放具体执行细节
适配器模式Log、Slf4jImpl、Log4jImpl、Jdk14LoggingImpl兼容不同日志框架
装饰者模式Cache、LruCache、LoggingCache、SynchronizedCache动态增强缓存功能
迭代器模式PropertyTokenizer逐层解析属性路径

总结

MyBatis 之所以适合用来学习源码和设计模式,是因为它的设计非常清晰。

它没有 Spring 那么庞大,也没有 Netty 那么偏底层,但它完整覆盖了一个优秀框架该有的核心能力:

  • 配置解析
  • 对象构建
  • 工厂创建
  • 接口代理
  • SQL 执行
  • 缓存管理
  • 插件扩展
  • 日志适配
  • 动态 SQL 解析

从设计模式角度看,MyBatis 并不是为了使用设计模式而使用设计模式,而是在具体问题下自然选择了合适的设计方式。

例如:

SqlSessionFactoryBuilder 使用建造者模式,是因为 MyBatis 初始化流程本身就很复杂。

SqlSessionFactory 使用工厂模式,是因为 SqlSession 的创建需要隐藏事务、执行器、配置对象等细节。

MapperProxy 使用代理模式,是因为 Mapper 接口没有实现类,需要运行时动态代理。

SqlNode 使用组合模式,是因为动态 SQL 天然就是一棵节点树。

BaseExecutor 使用模板方法模式,是因为不同执行器的主流程一致,细节不同。

Cache 使用装饰者模式,是因为缓存能力需要灵活叠加。

Log 使用适配器模式,是因为 MyBatis 需要兼容多种日志框架。

PropertyTokenizer 使用迭代器模式,是因为属性路径需要被逐段解析。

因此,学习 MyBatis 中的设计模式,重点不是死记"哪个类用了哪个模式",而是理解每个模式解决了什么问题。

设计模式的真正价值不在于名字,而在于它背后的结构化思维。

作者:武子康的个人博客