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

即根据不同的输入参数来构建 SqlSessionFactory 这个工厂对象。
3 工厂模式
在 Mybatis 中比如 SqlSessionFactory 使用的是工厂模式,该工厂没有那么复杂的逻辑,是一个简单工厂模式。
SqlSession 可以认为是一个 Mybatis 工作的核心的接口,通过这个接口可以执行执行 SQL 语句、获取 Mappers、管理事务。类似于连接 MySQL 的 Connection 对象。

可以看到,该 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 接口的所有节点,就是整个组合模式树的各个节点:

组合模式的简单之处在于,所有的子节点都是同一类节点,可以递归的向下执行,比如对于 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 包含以下结构:

其中的 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 日志包的适配:

比如对于 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 永久缓存进行缓存策略等方便的控制。如下图:

用于装饰 PerpetualCache 的标准装饰器共有 8 个(全部在 org.apache.ibatis.cache.decorators 包中):
- FifoCache:先进先出算法,缓存回收策略
- LoggingCache:输出缓存命中的日志信息
- LruCache:最近最少使用算法,缓存回收策略
- ScheduledCache:调度缓存,负责定时清空缓存
- SerializedCache:缓存序列化和反序列化存储
- SoftCache:基于软引用实现的缓存管理策略
- SynchronizedCache:同步的缓存装饰器,用于防止多线程并发访问
- 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.");
}
}