1 9 种设计模式

Mybatis 至少遇到了以下的设计模式的使用:

  1. Builder 模式,例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;
  2. 工厂模式,例如 SqlSessionFactory、ObjectFactory、MapperProxyFactory;
  3. 单例模式,例如 ErrorContext 和 LogFactory;
  4. 代理模式,Mybatis 实现的核心,比如 MapperProxy、ConnectionLogger,用的 jdk 的动态代理;还有 executor.loader 包使用了 cglib 或者 javassist 达到延迟加载的效果;
  5. 组合模式,例如 SqlNode 和各个子类 ChooseSqlNode 等;
  6. 模板方法模式,例如 BaseExecutor 和 SimpleExecutor,还有 BaseTypeHandler 和所有的子类例如 IntegerTypeHandler;
  7. 适配器模式,例如 Log 的 Mybatis 接口和它对 jdbc、log4j 等各种日志框架的适配实现;
  8. 装饰者模式,例如 Cache 包中的 cache.decorators 子包中等各个装饰者的实现;
  9. 迭代器模式,例如迭代器模式 PropertyTokenizer;

2 建造者模式

在 Mybatis 环境的初始化过程中,SqlSessionFactoryBuilder 会调用 XMLConfigBuilder 读取所有的 MybatisMapConfig.xml 和所有的 *Mapper.xml 文件,构建 Mybatis 运行的核心对象 Configuration 对象,然后将该 Configuration 对象作为参数构建一个 SqlSessionFactory 对象。

其中 XMLConfigBuilder 在构建 Configuration 对象时,也会调用 XMLMapperBuilder 用于读取 *Mapper 文件,而 XMLMapperBuilder 会使用 XMLStatementBuilder 来读取和 build 所有的 SQL 语句。

在这个过程中,有一个相似的特点,就是这些 Builder 会读取文件或者配置,然后做大量的 XpathParser 解析、配置或语法的解析、反射生成对象、存入结果缓存等步骤,这么多的工作都不是一个构造函数所能包括的,因此大量采用了 Builder 模式来解决。

对于 builder 的具体类,方法都大都用 build 开头,比如 SqlSessionFactoryBuilder 为例,它包含以下方法:

image

即根据不同的输入参数来构建 SqlSessionFactory 这个工厂对象。

3 工厂模式

在 Mybatis 中比如 SqlSessionFactory 使用的是工厂模式,该工厂没有那么复杂的逻辑,是一个简单工厂模式。

SqlSession 可以认为是一个 Mybatis 工作的核心的接口,通过这个接口可以执行执行 SQL 语句、获取 Mappers、管理事务。类似于连接 MySQL 的 Connection 对象。

image

可以看到,该 Factory 的 openSession 方法重载了很多个,分别支持 autoCommit、Executor、Transaction 等参数的输入,来构建核心的 SqlSession 对象。

4 单例模式

在 Mybatis 中有两个地方用到单例模式,ErrorContext 和 LogFactory,其中 ErrorContext 是用在每个线程范围内的单例,用于记录该线程的执行环境错误信息,而 LogFactory 则是提供给整个 Mybatis 使用的日志工厂,用于获得针对项目配置好的日志对象。

ErrorContext 的单例实现代码:

public class ErrorContext {
    private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
    private ErrorContext() {    }
    public static ErrorContext instance() {
        ErrorContext context = LOCAL.get();
        if (context == null) {
            context = new ErrorContext();
            LOCAL.set(context);
        }
        return context;
    }

构造函数是 private 修饰,具有一个 static 的局部 instance 变量和一个获取 instance 变量的方法,在获取实例的方法中,先判断是否为空如果是的话就先创建,然后返回构造好的对象。

只是这里有个有趣的地方是,LOCAL 的静态实例变量使用了 ThreadLocal 修饰,也就是说它属于每个线程各自的数据,而在 instance () 方法中,先获取本线程的该实例,如果没有就创建该线程独有的 ErrorContext。

5 代理模式

当我们使用 Configuration 的 getMapper 方法时,会调用 mapperRegistry.getMapper 方法,而该方法又会调用 mapperProxyFactory.newInstance(sqlSession) 来生成一个具体的代理:

public class MapperProxyFactory<T> {
 
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
 
    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
 
    public Class<T> getMapperInterface() {
        return mapperInterface;
    }
 
    public Map<Method, MapperMethod> getMethodCache() {
        return methodCache;
    }
 
    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface },
                mapperProxy);
    }
 
    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
 
}

在这里,先通过 T newInstance(SqlSession sqlSession) 方法会得到一个 MapperProxy 对象,然后调用 T newInstance(MapperProxy<T> mapperProxy) 生成代理对象然后返回。

而查看 MapperProxy 的代码,可以看到如下内容 (新版的 mybatis 源码中已经改了):

public class MapperProxy<T> implements InvocationHandler, Serializable {
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            } else if (isDefaultMethod(method)) {
                return invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);
    }

该 MapperProxy 类实现了 InvocationHandler 接口,并且实现了该接口的 invoke 方法。

通过这种方式,我们只需要编写 Mapper.java 接口类,当真正执行一个 Mapper 接口的时候,就会转发给 MapperProxy.invoke 方法,而该方法则会调用后续的 sqlSession.cud>executor.execute>prepareStatement 等一系列方法,完成 SQL 的执行和返回。

6 组合模式

Mybatis 支持动态 SQL 的强大功能. 在 DynamicSqlSource.getBoundSql 方法里,调用了 rootSqlNode.apply(context) 方法,apply 方法是所有的动态节点都实现的接口:

public interface SqlNode {
    boolean apply(DynamicContext context);
}

对于实现该 SqlSource 接口的所有节点,就是整个组合模式树的各个节点:

image

组合模式的简单之处在于,所有的子节点都是同一类节点,可以递归的向下执行,比如对于 TextSqlNode,因为它是最底层的叶子节点,所以直接将对应的内容 append 到 SQL 语句中:

@Override
public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
}

但是对于 IfSqlNode,就需要先做判断,如果判断通过,仍然会调用子元素的 SqlNode,即 contents.apply 方法,实现递归的解析。

@Override
public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
        contents.apply(context);
        return true;
    }
    return false;
}

7 模板方法模式

模板方法模式是所有模式中最为常见的几个模式之一,是基于继承的代码复用的基本技术。

在 Mybatis 中,sqlSession 的 SQL 执行,都是委托给 Executor 实现的,Executor 包含以下结构:

image

其中的 BaseExecutor 就采用了模板方法模式,它实现了大部分的 SQL 执行逻辑,然后把以下几个方法交给子类定制化完成:

protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
 
protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;
 
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,
        ResultHandler resultHandler, BoundSql boundSql) throws SQLException;

该模板方法类有几个子类的具体实现,使用了不同的策略:

  • 简单 SimpleExecutor:每执行一次 update 或 select,就开启一个 Statement 对象,用完立刻关闭 Statement 对象。(可以是 Statement 或 PrepareStatement 对象)
  • 重用 ReuseExecutor:执行 update 或 select,以 sql 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后,不关闭 Statement 对象,而是放置于 Map<String, Statement> 内,供下一次使用。(可以是 Statement 或 PrepareStatement 对象)
  • 批量 BatchExecutor:执行 update(没有 select,JDBC 批处理不支持 select),将所有 sql 都添加到批处理中( addBatch() ),等待统一执行( executeBatch() ),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch() 完毕后,等待逐一执行 executeBatch() 批处理的;BatchExecutor 相当于维护了多个桶,每个桶里都装了很多属于自己的 SQL,就像苹果蓝里装了很多苹果,番茄蓝里装了很多番茄,最后,再统一倒进仓库。(可以是 Statement 或 PrepareStatement 对象)

比如在 SimpleExecutor 中这样实现 update 方法:

@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null,
                null);
        stmt = prepareStatement(handler, ms.getStatementLog());
        return handler.update(stmt);
    } finally {
        closeStatement(stmt);
    }
}

8 适配器模式

适配器模式 (Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器 (Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

在 Mybatsi 的 logging 包中,有一个 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);
}

该接口定义了 Mybatis 直接使用的日志方法,而 Log 接口具体由谁来实现呢?Mybatis 提供了多种日志框架的实现,这些实现都匹配这个 Log 接口所定义的接口方法,最终实现了所有外部日志框架到 Mybatis 日志包的适配:

image

比如对于 Log4jImpl 的实现来说,该实现持有了 org.apache.log4j.Logger 的实例,然后所有的日志方法,均委托该实例来实现。

public class Log4jImpl implements Log {
    private static final String FQCN = Log4jImpl.class.getName();
    private Logger log;
    public Log4jImpl(String clazz) {
        log = Logger.getLogger(clazz);
    }
    @Override
    public boolean isDebugEnabled() {
        return log.isDebugEnabled();
    }
    @Override
    public boolean isTraceEnabled() {
        return log.isTraceEnabled();
    }
    @Override
    public void error(String s, Throwable e) {
        log.log(FQCN, Level.ERROR, s, e);
    }
    @Override
    public void error(String s) {
        log.log(FQCN, Level.ERROR, s, null);
    }
    @Override
    public void debug(String s) {
        log.log(FQCN, Level.DEBUG, s, null);
    }
    @Override
    public void trace(String s) {
        log.log(FQCN, Level.TRACE, s, null);
    }
    @Override
    public void warn(String s) {
        log.log(FQCN, Level.WARN, s, null);
    }
}

9 装饰者模式

在 mybatis 中,缓存的功能由根接口 Cache( org.apache.ibatis.cache.Cache )定义。整个体系采用装饰器设计模式,数据存储和缓存的基本功能由 PerpetualCache( org.apache.ibatis.cache.impl.PerpetualCache )永久缓存实现,然后通过一系列的装饰器来对 PerpetualCache 永久缓存进行缓存策略等方便的控制。如下图:

image

用于装饰 PerpetualCache 的标准装饰器共有 8 个(全部在 org.apache.ibatis.cache.decorators 包中):

  1. FifoCache:先进先出算法,缓存回收策略
  2. LoggingCache:输出缓存命中的日志信息
  3. LruCache:最近最少使用算法,缓存回收策略
  4. ScheduledCache:调度缓存,负责定时清空缓存
  5. SerializedCache:缓存序列化和反序列化存储
  6. SoftCache:基于软引用实现的缓存管理策略
  7. SynchronizedCache:同步的缓存装饰器,用于防止多线程并发访问
  8. WeakCache:基于弱引用实现的缓存管理策略

另外,还有一个特殊的装饰器 TransactionalCache:事务性的缓存

正如大多数持久层框架一样,mybatis 缓存同样分为一级缓存和二级缓存

  • 一级缓存,又叫本地缓存,是 PerpetualCache 类型的永久缓存,保存在执行器中(BaseExecutor),而执行器又在 SqlSession(DefaultSqlSession)中,所以一级缓存的生命周期与 SqlSession 是相同的。
  • 二级缓存,又叫自定义缓存,实现了 Cache 接口的类都可以作为二级缓存,所以可配置如 encache 等的第三方缓存。二级缓存以 namespace 名称空间为其唯一标识,被保存在 Configuration 核心配置对象中。

二级缓存对象的默认类型为 PerpetualCache,如果配置的缓存是默认类型,则 mybatis 会根据配置自动追加一系列装饰器。

Cache 对象之间的引用顺序为:

SynchronizedCache–>LoggingCache–>SerializedCache–>ScheduledCache–>LruCache–>PerpetualCache

10 迭代器模式

比如 Mybatis 的 PropertyTokenizer 是 property 包中的重量级类,该类会被 reflection 包中其他的类频繁的引用到。这个类实现了 Iterator 接口,在使用时经常被用到的是 Iterator 接口中的 hasNext 这个函数。

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;
        }
        indexedName = name;
        delim = name.indexOf('[');
        if (delim > -1) {
            index = name.substring(delim + 1, name.length() - 1);
            name = name.substring(0, delim);
        }
    }
    public String getName() {        return name;    }
    public String getIndex() {        return index;    }
    public String getIndexedName() {        return indexedName;    }
    public String getChildren() {        return children;    }
    @Override    public boolean hasNext() {        return children != null;    }
    @Override    public PropertyTokenizer next() {        return new PropertyTokenizer(children);    }
    @Override    public void remove() {
        throw new UnsupportedOperationException(
                "Remove is not supported, as it has no meaning in the context of properties.");
    }
}