之前就自己关注的问题研究了mybatis的源代码,今天我们来看一下mybatis中都使用了哪些设计模式。
关于设计模式,我平时也有不少使用,但是总感觉理解的不是很深刻,学习源码并观察设计模式在其中的应用,也可以更加深入的了解设计模式。
建造者(Builder)设计模式
将一个复杂的对象的构造与他的表示分离,是同样的构造过程可以创建不同的表示,他是将一个复杂的对象分解为多个简单的对象,然后一步一步的构建而成。
一般来说,如果一个对象的构建比较复杂,超出了构造函数所能包含的范围,就可以使用工厂模式和Builder模式。
相对于工厂模式会产出一个完整的产品,Builder应用于更加复杂的对象的构建,甚至只会构建产品的一个部分。
使用过Mybatis的肯定很熟悉这段代码吧
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession sqlSession = sqlSessionFactory.openSession();
我们使用简单的两行代码之后,便可以获取我们的mapper,然后直接查询数据库了。但是在这两行简单的代码背后,却是非常复杂的配置解析过程。
在Mybatis环境的初始化过程中,SqlSessionFactoryBuilder会调用XMLConfigBuilder读取所有的mybatis-config.xml和所有的*Mapper.xml文件,构建Mybatis运行的核心对象Configuration对象,然后将该Configuration对象作为参数构建一个SqlSessionFactory对象。
其中XMLConfigBuilder在构建Configuration对象时,也会调用XMLMapperBuilder用于读取*Mapper文件,而XMLMapperBuilder会使用XMLStatementBuilder来读取和build所有的SQL语句。
在这个过程中,有一个相似的特点,就是这些Builder会读取文件或者配置,然后做大量的XpathParser解析、配置或语法的解析、反射生成对象、存入结果缓存等步骤,这么多的工作都不是一个构造函数所能包括的,因此大量采用了Builder模式来解决。
常用的Builder:
- SqlSessionFactoryBuilder:构建SqlSession
- XMLConfigBuilder:构建Mybatis-config.xml成对象
- XMLMapperBuilder:解析*Mapper.xml成对象
- XMLStatementBuilder:解析单个的select、insert等statement标签
- ParameterMapping.Builder:构建每一个#{}参数成为一个ParameterMapping
- MappedStatement.Builder:构建每一个select、insert等statement标签
- ResultMap.Builder:构建ResultMap标签
- ResultMapping.Builder:构建ResultMap标签中的每个参数
工厂设计模式
工厂模式相对来说很简单
在工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
- LogFactory
//根据传入的类名来构建Log
public static Log getLog(String logger) {
try {
//构造函数,参数必须是一个,为String型,指明logger的名称
return logConstructor.newInstance(new Object[] { logger });
} catch (Throwable t) {
throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
}
}
- SqlSessionFactory
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);
//然后产生一个DefaultSqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
//如果打开事务出错,则关闭它
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
//最后清空错误上下文
ErrorContext.instance().reset();
}
}
单例设计模式
这个很简单,不再说明
public final class LogFactory {
//...
//单例模式,不得自己new实例
private LogFactory() {
// disable construction
}
}
代理设计模式
代理模式大概是Mybatis的核心了吧,我在《Mybatis核心源码-通过sqlSession获取映射器代理工厂》一文中写了通过sqlSession.getMapper获取对应的代理对象,然后通过代理对象获取对应的mapper.xml,这里便用到了代理设计模式,所以我不再赘述了,有兴趣的小伙伴可以看一下对应的文章。
组合设计模式
组合模式组合多个对象形成树形结构以表示“整体-部分”的结构层次。
组合模式对单个对象(叶子对象)和组合对象(组合对象)具有一致性,它将对象组织到树结构中,可以用来描述整体与部分的关系。
同时它也模糊了简单元素(叶子对象)和复杂元素(容器对象)的概念,使得客户能够像处理简单元素一样来处理复杂元素,从而使客户程序能够与复杂元素的内部结构解耦。
在使用组合模式中需要注意一点也是组合模式最关键的地方:叶子对象和组合对象实现相同的接口。这就是组合模式能够将叶子节点和对象节点进行一致处理的原因。
这里面的if标签,trim标签在mybatis中都对应一个SqlNode,例如IfSqlNode、TrimSqlNode、StaticTextSqlNode等
在mybatis中,在创建SqlSource时,会解析当前select标签中所有的动态标签,构建成SqlNode并放到集合中,最终构建成MixedSqlNode混合节点。
在这张图中,MixedSqlNode就是根结点,而IfSqlNode、TrimSqlNode等动态标签就是树枝节点,StaticTextSqlNode(#{})和TextSqlNode(${})则是叶子节点。
组合模式树的各个节点
其中的StaticTextSqlNode(#{})和TextSqlNode(${})是叶子结点,所有的非叶子结点最终都会通过递归调用获取到对应的叶子结点,也就是动态sql标签中的sql,然后进行拼接。
MixedSqlNode根结点下的所有树枝节点:
IfSqlNode树枝节点下会有一个StaticTextSqlNode叶子节点。
在组合模式中,根节点和树枝节点本质上属于同一种数据类型,他们具备一致的行为(apply)
了解完动态标签的数据结构后,我们来看一个复杂的含有动态sql的select语句标签是怎么被拼接成一个完整的sql的。
首先作为根结点的MixedSqlNode执行他的apply方法:
public class MixedSqlNode implements SqlNode {
//组合模式,拥有一个SqlNode的List
private List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
//依次调用list里每个元素的apply
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
}
实际上MixedSqlNode循环调用了树枝节点的apply方法
作为树枝节点的IfSqlNode中有一个叶子节点StaticTextSqlNode,树枝节点的apply在解析完Ognl表达式后,调用了叶子节点StaticTextSqlNode的apply方法。而叶子节点的apply方法就是简单的使用append方法进行了字符串拼接。
public class IfSqlNode implements SqlNode {
private ExpressionEvaluator evaluator;
private String test;
private SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
//mixed里面是一个静态标签,if标签包裹的地方
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
@Override
public boolean apply(DynamicContext context) {
//如果满足条件,则apply,并返回true
//Ognl表达式
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
}
通过使用组合模式,mybatis的动态标签可以通过递归的方式将每一个树枝节点(动态标签)进行拼接,最终形成一个完整的sql。
模板方法设计模式
模板方法设计模式是非常常用的设计模式,我平时在开发中也经常用到,通过抽象类定义统一的模板,然后让实现类去实现具体的细节
在Mybatis中,通过StatementHandler接口,定义了统一的sql执行过程:
然后通过它的实现类PreparedStatementHandler等去实现具体的细节:
模板模式其实非常简单,通过抽象类去定义统一的模板,然后通过实现类去补充具体的实现细节。
适配器设计模式
适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。
mybatis中最值得称赞的是mybatis对于各个日志的集成工作,我在《Mybatis整合日志框架-工厂设计模式+适配器设计模式》一文中详细解释了mybatis通过定义Log接口,Mybatis提供了多种日志框架的实现,这些实现都匹配这个Log接口所定义的接口方法,最终实现了所有外部日志框架到Mybatis日志包的适配
装饰者设计模式
装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。
其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。
我在《Mybatis一级缓存原理解析》一问中解析了一级缓存的原理,Mybatis在创建executor时其实是直接创建了CachingExecutor,然后将SimpleExecutor通过构造器的方式传给了CachingExecutor,使用CachingExecutor在执行时除了执行自己特有的二级缓存后,其他的都是通过包装类SimpleExecutor去执行的。