mybatis中使用了较多的设计模式。下面是我在阅读过程中的一些梳理,可能存在偏差与错误,每种模式也只是列举其中一两种场景。
概况
| 类型 | 具体模式 | 模式说明 | 应用点 | 应用好处 |
|---|---|---|---|---|
| 创造型 | 单例模式 | ErrorContext | 错误统一处理 | |
| 创造型 | 工厂模式 | 工厂模式 | SqlSessionFactory与DataSourceFactory | 复杂对象创建 |
| 创造型 | 建造者模式 | 建造者模式 | SqlSessionFactoryBuilder | 1.复杂的对象构建 2.一个对象参数多,部分必填,简化构造函数 |
| 行为型 | 模板方法模式 | Executor | 将三种子类相同的流程放在了BaseExecutor中 | |
| 行为型 | 责任链模式 | 责任链模式 | interceptor | 灵活的给用户提供了扩展的口子 |
| 行为型 | 迭代器模式 | 迭代器模式 | PropertyTokenizer | 让PropertyTokenizer具有迭代器的功能,指向自己的children |
| 结构型 | 组合模式 | 组合模式 | SqlNode | 如果后续增加另外一种标签,只需要新增xxSqlNode即可 |
| 结构型 | 代理模式 | 代理模式 | MapperProxyFactory | 让我们普通的mapper拥有了sqlSession的功能 |
| 结构型 | 装饰者模式 | 装饰者模式 | cachingExecutor | 包装executor组件的二级缓存功能 |
| 结构型 | 门面模式 | 门面模式 | sqlSession | 屏蔽用户细节,统一对外接口。使用sqlSession就可以完成crud功能 |
创造型
单例模式-ErrorContext
ErrorContext这个类的作用就是统一收集异常信息,包括错误地址,错误sql,错误堆栈等信息。 便于出错时快速定位到是哪个类,哪个流程,哪个sql出了问题。我们日常写代码要么日志冗余,要么日志缺失,要么打不到关键点。Mybatis的这个设计我们是可以借鉴的。
ErrorContext相关变量
//存储的错误,它存在在一个threadLocal里面
private ErrorContext stored;
//出错文件的全路径。比如/user/luo/studentMapper.xml
private String resource;
//出措时,在做什么操作。比如select db
private String activity;
//更细粒度的出错定位。比如是studentMapper里面的selectUser方法
private String object;
//错误信息
private String message;
//错误sql
private String sql;
//错误堆栈
private Throwable cause;
使用方法: 在使用的时候会使用他的单例对象构建相关参数
在外层输出
会调用里面的toString方法
toString方法又会将相关参数拼装
最终变成
### Error querying database. Cause: java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed
### The error may exist in mapper/studentMapper.xml
### The error may involve mapper.StudentMapper.getStudentById
### The error occurred while executing a query
### Cause: java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed
工厂模式
- 普通工厂:SqlSessionFactory与DataSourceFactory
这两者属于工厂模式中的普通工厂。但SqlSessionFactory只有一个实现类DefaultSqlSessionFactory,并没有给用户留口子。
- 简单工厂:RoutingStatementHandler
- 这块是个人觉得感觉有点过度设计的地方。可以看到类图,BaseStatementHandler的三个实现类为执行StatementHandler的三种策略。而RoutingStatementHandler只是做为一个简单工厂来选择具体的handler
建造者模式-SqlSessionFactoryBuilder
建造者模式最重要的两个场景 1.复杂的对象构建 2.一个对象参数多,部分必填,简化构造函数
SqlSessionFactoryBuilder 从名字就看得出来是构建SqlSessionFactory,代码虽然只有短短两行。但整个configuration类的解析都在这个里面。这里符合上述的场景一。屏蔽了对象的复杂过程。
下图的discriminator,Environment,MapperStament等类里面都有一个内部类XXXBuilder,这里符合上述场景2.简化构造函数
建造者模式其实很好识别,只要是以xxxBudilder类结尾的基本就是属于建造者模式。建筑者模式除开上上述场景还有一个场景就是参数顺序不同,构造出来的结果不一样,我们现在常用的lombok的@Builder注解在我看来就是一种简化版的建造者模式
行为型
模板方法模式-Executor
模板方式个人觉得是大家必须要掌握的一种模式,第一它简单,第二是它不像有点模式,在我们日常开发中使用得非常非常少(桥接模式,访问者模式,迭代器模式,克隆模式等)。它是一个频繁很高的模式,无论是源码中还是我们日常开发中,许多场景都可以去使用。
Executor 作为sqlSession背后的支持者,负责整个数据的查询以及最后结果的封装,其中还包括了一级缓存与二级缓存的使用。mybatis给我们提供了3种具体的实现。simpleExecutor(默认实现),BatchExecutor(批处理),ReuseExecutor(prepareStament的复用)。但这三种实现除了具体的查询,更新逻辑有所不同,前面的准备工作,以及流程都是一样的。所以这非常契合我们模板方法的使用场景
比如下图的queryFromDatabase方法,三种类型只有在具体的查询不一致。
责任链模式-interceptor
责任链模式作为我们日常使用较少,但几乎所有框架,比如servlet,tomcat,spring都在使用。我们掌握也是比较有必要的。 他的定义:
- 为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;
- 当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。 其实大家记住生活中的“击鼓传花”这个例子就行了。 mybatis的这块代码个人感觉写的很漂亮,易懂并且使用简单。 Mybatis在扩展插件模式使用了责任链模式,我们先来看他的用法。
- 用法
Mybatis能拦截在参数准备阶段(ParameterHandler)、StatementHandler(Statement准备阶段,crud阶段)、Executor(crud阶段,Executor与StatementHandler的区别是StatementHandler在executor之后,executor中会执行一级缓存与二级缓存的查询)ResultSetHandler(结果集处理阶段)。共4个接口对象内的方法。
也就是在我们执行sql的每个阶段都为我们提供了扩展的口子。具体的使用也很简单。
1.我们只需要继承Interceptor类
2.在类上加上在哪个阶段,方法需要进行拦截的注解
3.重写intercept方法即可,里面加上我们需要做的操作(比如分页,比如统计sql时间,比如加乐观锁(sql后面添加version=xx))
4.在配置文件的标签中配上我们自己写的Interceptor类
这里给大家看一下pageHelper的实现(Mybatis著名的分页插件)
- 实现
Mybatis的拦截器写的比spring简单多了...只有这么几个类
- 类说明
- 流程说明
当我们执行db操作时,会沿着Executor->ParameterHandler->StatementHandler->ResultSetHandler 四个组件的顺序执行。而这4个组件的初始化也是在执行过程中通过configuration.new生成的,而初始化流程就会调用Interceptain.pluginAll方法
可以看到plginAll方法被我们四个拦截的口子所调用。
这4个方法的调用正是在
1.
public Object pluginAll(Object target) {
//遍历所有的interceptor。而这些interceptor正是我们写在配置文件中<plugin>标签中的类
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
2.调用Plugin.wrap(target, this);
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
3.该方法对于使用@Signature以及@Intercepts注解的类进行加强。加强的本质也是Jdk的动态代理
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
4. 当被代理的类进行调用时,就会调用invoke方法,从而调用interceptor.intercept方法。
这个方法也就是我们自己重写的类。从而完成拦截功能
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
迭代器模式-PropertyTokenizer
。核心思想就是将一个集合类的增加和删除与它的遍历解耦。实现也非常简单,继承JDK中自带的Iterator接口,重写hasNext,next接口就行。迭代器模式在我们日常开发中几乎不可能使用,因为我们几乎不会去写一个这种类似于集合的东西。 PropertyTokenizer类称为“属性分词器”。它属于mybatis反射包下面,负责对属于的参数进行 “分词”操作。这里简单介绍一下mybatis中功能强大的反射工具类。主要使用类为metaObject与metaClass。MetaObject在mybatis的源码中中使用很广泛,它的做法是将某个类封装成MetaObject对象,便捷的存,取数据。个人理解有2个好处。一是metaObject提供了比普通javaBean更为快捷的方式。对于bean中的list属性。它可以通过下图这种“属性[下表].属性名”直接存取值。二是保证代码的整洁,避免代码里随处可见的get/set。MetaObjct底层仍然使用的是jdk中的反射。整个的原理大概流程就是当我们使用MetaObject get/set的时候,PropertyTokenizer对我们的输入进行分词,然后递归的去get/set。例如下图中getValue("classList[0].name")分词结果。
PropertyTokenizer使用迭代器模式重写了这2个方法。将children作为自己的Next。
结构型
组合模式-SqlNode
组合模式的场景是
当你的程序结构有类似树一样的层级关系时,例如文件系统,视图树,公司组织架构等等 当你要以统一的方式操作单个对象和由这些对象组成的组合对象的时候。
组合模式其实就是 composite类和leaf类。composite类最终将工作交给leaf类来实现,但对使用者来说,他们并不知道 composite类和leaf类的区别。
mybatis使用组合模式的场景是在动态加载配置文件xml的sql语句成为sqlSource最终成为BoundSql的过程。BoundSql就是我们最后可以执行的sql语句,里面包括参数与sql。
我们日常开发中,写的sql里面包含许多标签,包括if标签(<if test xxx!=null>),where标签,foreach标签。这些在解析的时候都会具体的一个sqlNode。
我们上述说了组合模式是一个composite类和一堆leaf类。在mybatis中MixedSqlNode就是composite类,他持有List。当它被调用时,把工作交给真正的node去执行
- 流程说明
我们以上图中的DynamicSqlSource来说明 1.初始化
2. 当调用apply的时候就会走到MixedSqlNode中,交给各个SqlNode去拼装结果
代理模式-MapperProxyFactory
代理模式的核心思想就是“代理”二字。将原始对象交给代理者。代理者可以对原始对象前置增强,后置增强,通常自行管理其服务对象的生命周期。底层为jdk或者Cglib。例如Spring 使用动态代理实现 AOP 时有两个非常重要的类,即 JdkDynamicAopProxy 类和 CglibAopProxy 类。在Mybatis中,当我们去getMapper的时,MapperProxyFactory会将我们普通的Mapper类代理增强为拥有sqlSession功能的类(也就是能跟jdbc打交道的类)。它一共分为两个部分。一是代理构建部分。二是查询部分
- 构建部分
当我们使用sqlSession.getMapper的时候,会沿着下面的流程,初始化一个拥有sqlSession功能的mapper类
---sqlSession.getMapper -- configuration.getMapper(type, this); -- mapperRegistry.getMapper(type, sqlSession); ##sqlSession包装为MapperProxy。而MapperProxy继承了jdk中动态代理InvocationHandler类 --mapperProxyFactory.newInstance(sqlSession) ##使用Jdk的Proxy类 -- Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy) -- Jdk 中proxy.newProxyInstance - 查询部分 在上面流程中我们的mapperProxy类继承了InvocationHandler类,所以当我们使用被代理类进行方法调用时,会进入到mapperProxy类的Invoke方法(java反射基础,不清楚的同学可以先去补一下)。
走到mapperMethod.execute(sqlSession, args)
可以看到最后结果就是交给sqlSession来操作。
整个流程简单且清晰
装饰者模式-cachingExecutor
装饰者模式也是非常重要的一种模式。定义如下:
- 它指的是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。
- 它是通过创建一个包装对象,也就是装饰者来包裹真实的对象。
- 装饰者对象和具体构件有相同的接口。这样客户端对象就能以和真实对象相同的方式和装饰对象交互。
像我们Jdk中的InputStream就是标准的装饰者模式。装饰者模式有两个核心思想。
- 一是无论怎么包装,最终的核心还是”我“。(这个就是跟代理模式最大的区别,代理模式有可能最后”我不是我了“,因为我已经把我的生命周期全部交给了代理者)
- 二是包装者要跟被包装者有相同的行为。
给大家举一个例子--”手抓饼“,我们可以在基础版本的手抓饼上加入”香肠装饰者“,”培根装饰者“,”肉松装饰者“等等。但大家需要记住装饰者模式的核心思想是“包装我”,也就是你作为装饰者,你跟我要有相同的行为(方法)。一任何装饰者你都要有跟基础手抓饼相同的行为--加料。二:无论你怎么加料,最终还是一个手抓饼。
myBatis中cachingExecutor的作用就是跟executor类包装二级缓存。 类结构如下。cachingExecutor继承Executor且持有Executor对象。
对于查询操作,包装了了二级缓存的功能
对于mybatis的缓存详解,这里有详细说明mybatis缓存详解
门面模式-sqlSession
门面模式是设计模式中比较奇特的一种,它没有特定的um类图,它是一种思想。使用一个facade类/接口 去组合复杂的子系统。减少耦合。生活中的例子就是小米智能家居-米家,如果没有它,我们晚上休息时,需要关电视机,关电灯,关空调,而有了它,我们只需要跟门面类米家打交道。而我们sqlSesesion就是这么一个类。它作为用户使用Mybatis进行crud一个重要口子。用户不用去关系后续的执行器,事务问题。只需要使用sqlSesesion就可以了。