在学习设计模式的过程中,我们大多数还是只停留在概念层面,很少有机会能在实际开发中用到,任何一个知识点,如果我们不能达到至少从2个方面去窥探它的话,很难真正去理解它。
而阅读优秀框架的源码是我们窥探设计模式很好的途径,在Mybatis中用到了大概有9种设计模式:
- Builder模式
- 工厂模式
- 单例模式
- 代理模式
- 组合模式
- 模板方法模式
- 适配器模式
- 装饰着模式
- 迭代器模式
下面我将从Mybatis源码层面去讲解这9种设计模式。
Builder模式
Builder模式的定义是"将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。",它属于创建类模式,建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。
并且在《effective-java》中第2条也提到:遇到多个构造器参数时,考虑用构建者(Builder)模式。
首先我们先来回顾一下使用Mybatis的流程:
java
// 1. 获取配置文件
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
// 2.加载解析配置文件并获取SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
// 3.根据SqlSessionFactory对象获取SqlSession对象
SqlSession sqlSession = factory.openSession();
// 4.通过SqlSession中提供的 API方法来操作数据库
List<User> list = sqlSession.selectList("com.chen.mapper.UserMapper.selectUserList");
System.out.println("list.size() = " + list.size());
// 5.关闭会话
sqlSession.close(); // 关闭session 清空一级缓存
在上面 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in); 就使用到了Builder模式,查看对应源码可以发现 SqlSessionFactoryBuilder
会调用XMLConfigBuilder
读取所有的MybatisMapConfig.xml
和所有的*Mapper.xml
文件,构建Mybatis运行的核心对象Configuration
对象,然后将该Configuration
对象作为参数构建一个SqlSessionFactory
对象。
无论是在JDK的源码中还是在其它的优秀框架源码里,设计者都喜欢设计很多个参数的重载方法,而参数个数少的方法内部也是直接调用参数多的重载方法,这样做是方便使用者去使用。
其中XMLConfigBuilder
在构建Configuration
对象时,也会调用XMLMapperBuilder
用于读取*.Mapper
文件,而XMLMapperBuilder
会使用XMLStatementBuilder
来读取和build所有的SQL语句。
XMLConfigBuilder是抽象类BaseBuilder的一个子类,专门用来解析全局配置文件,针对不同的构建目标还有其他的一些子类,比如:
- XMLMapperBuilder:解析Mapper映射器
- XMLStatementBuilder:解析增删改查标签
- XMLScriptBuilder:解析动态SQL
在解析< mapper >标签时又创建XMLMapperBuilder
紧接着在XMLMapperBuilder类里解析增删改查标签时又创建了XMLStatementBuilder
.....
在这个过程中,有一个相似的特点,就是这些Builder会读取文件或者配置,然后做大量的XpathParser解析、配置或语法的解析、反射生成对象、存入结果缓存等步骤,这么多的工作都不是一个构造函数所能包括的,因此大量采用了Builder模式来解决。
从建造者模式的设计初衷上来看,SqlSessionFactoryBuilder 虽然带有 Builder 后缀,但 不要被它的名字所迷惑,它并不是标准的建造者模式。一方面,原始类 SqlSessionFactory 的构建只需要一个参数,并不复杂。
另一方面,Builder 类SqlSessionFactoryBuilder 仍然定义了多包含不同参数列表的构造函数。 实际上,SqlSessionFactoryBuilder 设计的初衷只不过是为了简化开发。因为构建 SqlSessionFactory 需要先构建 Configuration,而构建 Configuration 是非常复杂的,需 要做很多工作,比如配置的读取、解析、创建 n 多对象等。为了将构建 SqlSessionFactory 的过程隐藏起来,对程序员透明,MyBatis 就设计了 SqlSessionFactoryBuilder 类封装这些构建细节。
工厂模式
在Mybatis中比如SqlSessionFactory
使用的是工厂模式,该工厂没有那么复杂的逻辑,是一个简单工厂模式。
简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
下面是SqlSessionFactory接口源码截图,可以看到提供了很多重载方法用来创建SqlSession
SqlSession
可以认为是一个Mybatis工作的核心的接口,通过这个接口可以执行执行SQL语句、获取Mappers、管理事务。类似于连接MySQL的Connection
对象。
SqlSessionFactory的默认实现是DefaultSqlSessionFactory,在DefaultSqlSessionFactroy内部对openSession()方法的实现都是直接调用 openSessionFromDataSource()方法,它是真正用来创建SqlSession对象的。
openSessionFromDataSource方法,
可以看到该方法先从configuration读取对应的环境配置,然后初始化TransactionFactory
获得一个Transaction
对象,然后通过Transaction
获取一个Executor
对象,最后通过configuration、Executor、是否autoCommit三个参数构建了SqlSession
。
单例模式
单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
单例模式的要点有三个:
- 一是某个类只能有一个实例;
- 二是它必须自行创建这个实例;
- 三是它必须自行向整个系统提供这个实例。
在Mybatis中有两个地方用到单例模式,ErrorContext
和LogFactory
,其中ErrorContext
是用在线程范围内的单例,记录该线程的执行环境错误信息,而LogFactory
则是提供给整个Mybatis使用的日志工厂,用于获得针对项目配置好的日志对象。
1、下面是ErrorContext的源码
构造函数是private修饰,具有一个static的局部instance变量和一个获取instance变量的方法,在获取实例的方法中,先判断是否为空如果是的话就先创建,然后返回构造好的对象。
值得注意的是,LOCAL的静态实例变量使用了
ThreadLocal
修饰,也就是说它属于每个线程各自的数据,而在instance()
方法中,先获取本线程的该实例,如果没有才创建该线程独有的ErrorContext
。
2、下面是LogFactory的源码
代理模式
代理模式可以认为是Mybatis的核心使用的模式,正是由于这个模式,我们只需要编写Mapper.java
接口,不需要实现,由Mybatis后台帮我们完成具体SQL的执行。
代理模式(Proxy Pattern) :给某一个对象提供一个代 理,并由代理对象控制对原对象的引用。代理模式的英 文叫做Proxy,它是一种对象结构型模式。
在使用代理模式时需要注意2个地方:
- 提前创建一个Proxy,
- 使用的时候会自动请求Proxy,然后由Proxy来执行具体事务;
根据上面提到的2点,现在我们来一步步探究Mapper接口的代理是如何提前创建出来的以及是如何使用的。
Mapper接口代理什么时候创建的?
在解析全局配置文件中的mapper时就会开始创建所有mapper接口对应的代理对象。
1、在解析mapper标签时,如果是使用如下方式来指定所有mapper接口,则会调用 configuration.addMappers(mapperPackage);该方法会解析指定包下面的所有mapper接口以及创建每个mapper接口对应的代理对象
xml
<mappers>
<package name="com.ping.mapper"/>
</mappers>
2、进入到configuration.addMappers方法
其中的mapperRegistry是Configuration类中的一个成员变量,用于存储和创建每个mapper接口的代理对象并根据mapper接口类型来获取对应的代理对象。
3、进入到MapperRegistry中的addMappers方法
4、继续addMapper()方法
5、MapperRegistry中的成员变量knownMappers就是一个Map,存储了每个mapper接口和对应代理工厂的映射
总结:
Configuration对象存储了MapperRegistry对象,MapperRegistry对象帮我们解析每个mapper接口并创建每个mapper接口的代理工厂,把它存储在Map<Class, MapperProxyFactory> knownMappers中。
后续获取一个mapper接口代理对象的时候直接调用MapperRegistry中的getMapper方法接口,它会根据mapper接口类型从knownMappers获取对应的代理工厂,然后使用代理工厂来完成代理的创建。
Mapper接口代理的使用
在上一小节中我们已经知道了Mybatis会在解析全局配置文件中的mapper标签时提前帮我们创建好所有mapper接口的代理对象,现在我们就来分析mapper接口的代理是如何被使用的。
当我们使用Configuration
的getMapper
方法时,会调用mapperRegistry.getMapper
方法,而该方法又会调用mapperProxyFactory.newInstance(sqlSession)
来生成一个具体的代理:
继续跟进MapperProxyFactory的newInstance方法
先通过T newInstance(SqlSession sqlSession)
方法会得到一个MapperProxy
对象,然后调用T newInstance(MapperProxy<T> mapperProxy)
生成代理对象然后返回。
进入到MapperProxy直接查看最重要的invoke方法
非常典型的,该MapperProxy
类实现了InvocationHandler
接口,并且实现了该接口的invoke
方法。 当真正执行一个Mapper
接口的时候,就会转发给MapperProxy.invoke
方法,而该方法则会调用后续的sqlSession.cud>executor.execute>prepareStatement
等一系列方法,完成SQL的执行和返回。
组合模式
组合模式(Composite Pattern) 的定义是:将对象组合成树形结构以表示整个部分的层次结构.组合模式可以让用户统一对待单个对象和对象的组合.
组合模式其实就是将一组对象(文件夹和文件)组织成树形结构,以表示一种'部分-整体' 的层次结构,(目录与子目录的嵌套结构). 组合模式让客户端可以统一单个对象(文件)和组合对象(文件夹)的处理逻辑(递归遍历).
Mybatis支持动态SQL的强大功能,比如下面的这个SQL:
java
<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
UPDATE users
<trim prefix="SET" prefixOverrides=",">
<if test="name != null and name != ''">
name = #{name}
</if>
<if test="age != null and age != ''">
, age = #{age}
</if>
<if test="birthday != null and birthday != ''">
, birthday = #{birthday}
</if>
</trim>
where id = ${id}
</update>
在这里面使用到了trim、if等动态元素,可以根据条件来生成不同情况下的SQL;
在DynamicSqlSource.getBoundSql
方法里,调用了rootSqlNode.apply(context)
方法,apply
方法是所有的动态节点都实现的接口:
对于实现该SqlSource
接口的所有节点,就是整个组合模式树的各个节点:
组合模式的简单之处在于,所有的子节点都是同一类节点,可以递归的向下执行,比如对于TextSqlNode,因为它是最底层的叶子节点,所以直接将对应的内容append到SQL语句中:
但是对于IfSqlNode,就需要先做判断,如果判断通过,仍然会调用子元素的SqlNode,即contents.apply
方法,实现递归的解析。
模板方法模式
模板方法模式(template method pattern)原始定义是:在操作中定义算法的框架,将一些步骤推迟到子类中。模板方法让子类在不改变算法结构的情况下重新定义算法的某些步骤。
模板方法中的算法可以理解为广义上的业务逻辑,并不是特指某一个实际的算法.定义中所说的算法的框架就是模板, 包含算法框架的方法就是模板方法.
模板类定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
在Mybatis中,sqlSession的SQL执行,都是委托给Executor实现的,Executor包含以下结构:
其中的BaseExecutor就采用了模板方法模式,它实现了大部分的SQL执行逻辑,然后把以下几个方法交给子类定制化完成:
该模板方法类有几个子类的具体实现,使用了不同的策略:
- 简单
SimpleExecutor
:每执行一次update
或select
,就开启一个Statement
对象,用完立刻关闭Statement
对象。(可以是Statement
或PrepareStatement
对象) - 重用
ReuseExecutor
:执行update
或select
,以sql作为key查找Statement
对象,存在就使用,不存在就创建,用完后,不关闭Statement
对象,而是放置于Map
内,供下一次使用。(可以是Statement
或PrepareStatement
对象) - 批量
BatchExecutor
:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()
),等待统一执行(executeBatch()
),它缓存了多个Statement对象,每个Statement对象都是addBatch()
完毕后,等待逐一执行executeBatch()
批处理的;BatchExecutor
相当于维护了多个桶,每个桶里都装了很多属于自己的SQL,就像苹果蓝里装了很多苹果,番茄蓝里装了很多番茄,最后,再统一倒进仓库。(可以是Statement或PrepareStatement对象)
比如在SimpleExecutor中这样实现doUpdate方法:
模板模式基于继承来实现代码复用。如果抽象类中包含模板方法,模板方法调用有待子类实 现的抽象方法,那这一般就是模板模式的代码实现。而且,在命名上,模板方法与抽象方法 一般是一一对应的,抽象方法在模板方法前面多一个"do",比如,在 BaseExecutor 类 中,其中一个模板方法叫 update(),那对应的抽象方法就叫 doUpdate()。
适配器模式
适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
由于Java中的日志框架也非常多,Log4j,Log4j2,Apache Commons Log,java.util.logging,slf4j等,这些工具对外的接口也都不尽相同,为了统一这些工具,MyBatis通过适配器模式定义了一套统一的日志接口供上层使用
在Mybatsi的logging包中,有一个Log接口:定义了四种日志级别,相比较其他的日志框架的多种日志级别显得非常的精简,但也能够满足大多数常见的使用了
该接口定义了Mybatis直接使用的日志方法,而Log接口具体由谁来实现呢?Mybatis提供了多种日志框架的实现,这些实现都匹配这个Log接口所定义的接口方法,最终实现了所有外部日志框架到Mybatis日志包的适配:
比如对于Log4jImpl
的实现来说,该实现持有了org.apache.log4j.Logger
的实例,然后所有的日志方法,均委托该实例来实现。
装饰者模式
装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为"油漆工模式",它是一种对象结构型模式。
在mybatis中,缓存的功能由根接口Cache(org.apache.ibatis.cache.Cache)
定义。整个体系采用装饰器设计模式,数据存储和缓存的基本功能由PerpetualCache(org.apache.ibatis.cache.impl.PerpetualCache)
永久缓存实现,然后通过一系列的装饰器来对PerpetualCache
永久缓存进行缓存策略等方面的控制。如下图:
缓存实现类 | 描述 | 作用 | 装饰条件 |
---|---|---|---|
基本缓存 | 缓存基本实现类 | 默认是PerpetualCache,也可以自定义比如RedisCache、EhCache等,具备基本功能的缓存类 | 无 |
LruCache | LRU策略的缓存 | 当缓存到达上限时候,删除最近最少使用的缓存(Least Recently Use) | eviction="LRU"(默认) |
FifoCache | FIFO策略的缓存 | 当缓存到达上限时候,删除最先入队的缓存 | eviction="FIFO" |
SoftCacheWeakCache | 带清理策略的缓存 | 通过JVM的软引用和弱引用来实现缓存,当JVM内存不足时,会自动清理掉这些缓存,基于SoftReference和WeakReference | eviction="SOFT"eviction="WEAK" |
LoggingCache | 带日志功能的缓存 | 比如:输出缓存命中率 | 基本 |
SynchronizedCache | 同步缓存 | 基于synchronized关键字实现,解决并发问题 | 基本 |
BlockingCache | 阻塞缓存 | 通过在get/put方式中加锁,保证只有一个线程操作缓存,基于Java重入锁实现 | blocking=true |
SerializedCache | 支持序列化的缓存 | 将对象序列化以后存到缓存中,取出时反序列化 | readOnly=false(默认) |
ScheduledCache | 定时调度的缓存 | 在进行get/put/remove/getSize等操作前,判断缓存时间是否超过了设置的最长缓存时间(默认是一小时),如果是则清空缓存--即每隔一段时间清空一次缓存 | flushInterval不为空 |
TransactionalCache | 事务缓存 | 在二级缓存中使用,可一次存入多个缓存,移除多个缓存 | 在TransactionalCacheManager中用Map维护对应关系 |
比如在BlockingCache中:
迭代器模式
迭代器(Iterator)模式,又叫做游标(Cursor)模式。GOF给出的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。
在软件系统中,容器对象拥有两个职责: 一是存储数据,而是遍历数据.从依赖性上看,前者是聚合对象的基本职责.而后者是可变化的,又是可分离的.因此可以将遍历数据的行为从容器中抽取出来,封装到迭代器对象中,由迭代器来提供遍历数据的行为,这将简化聚合对象的设计,更加符合单一职责原则.
Java的Iterator
就是迭代器模式的接口,只要实现了该接口,就相当于应用了迭代器模式:
比如Mybatis的PropertyTokenizer
是property包中的重量级类,该类会被reflection包中其他的类频繁的引用到。这个类实现了Iterator
接口,在使用时经常被用到的是Iterator
接口中的hasNext
这个函数。
可以看到,这个类传入一个字符串到构造函数,然后提供了iterator方法对解析后的子串进行遍历,是一个很常用的方法类。
实际上,PropertyTokenizer 类也并非标准的迭代器类。它将配置的解析、解析之后的元 素、迭代器,这三部分本该放到三个类中的代码,都耦合在一个类中,所以看起来稍微有点 难懂。不过,这样做的好处是能够做到惰性解析。我们不需要事先将整个配置,解析成多个 PropertyTokenizer 对象。只有当我们在调用 next() 函数的时候,才会解析其中部分配置。
总结
设计模式的本质是什么?:就是多态的应用
在多态应用的基础上遵循设计原则:单一职责 + 开闭原则 + 依赖倒置原则。
对于23种设计模式我们并不需要刻意的记忆,而且很多设计模式的实现原理很像,只不过是名字不一样,并且如果去阅读一些框架的源码的话,可以发现其实框架对很多设计模式的实 现,都并非标准的代码实现,都做了比较多的自我改进。 实际上,这就是所谓的灵活应用, 只借鉴不照搬, 根据具体问题针对性地去解决。
软件设计模式犹如剑法一样,当哪天我们真正做到:手中无剑,心中有剑,人剑合一的境界,我们才真正理解软件设计。