前言
Mybatis
应该是当前已知的主流框架源码阅读成本最低,设计最为简洁友好的框架。
设计模式差不多是框架设计者和阅读者的潜在遵守的规约,如果双方都按照这个套路来,读写双方都很愉快。
如果把编码比作文章的话,设计模式差不多也是整个行文的中心思想和脉络,按照既定模式来撸,没毛病!
-
Mybatis 整体架构图
- Mybatis核心流程之
SqlSessionFactory
与SqlSession
.
涉及到的设计模式 :
工厂模式
建造者模式
- Mybatis之MapperProxy - 生成mapper对象
涉及的设计模式:
单例模式
代理模式
- Mybatis之执行器Excutor:直接与JDBC交互
涉及的设计模式:模板方法
策略模式
职责链模式
一、工厂模式
Mybatis获取数据源的方式就用了工厂模式,设计简洁,可拓展性好。
工厂模式是Java对象实例化的一种解决方案。
-
简单工厂模式
工厂方法模式的应用- Mybatis数据源配置
综上对比
工厂方法比简单工厂模式更符合开闭原则
,但是对应的复杂度也更高了!产品接口
JDBC原生数据源接口
ps: 实际开发中关于Connection的管理基本不用mybatis原生的,已经被druid连接池取代
public interface DataSource {
Connection getConnection() throws SQLException;
Connection getConnection(String username, String password);
}
- 工厂接口- Mybatis数据源工厂接口
public interface DataSourceFactory {
void setProperties(Properties props);
DataSource getDataSource();
}
- 工厂接口的三个实现类
//JNDI获取数据源
public class JndiDataSourceFactory implements DataSourceFactory
.....
//不使用连接池的简单实现
public class UnpooledDataSourceFactory implements DataSourceFactory
.....
//使用连接池的简单实现
public class PooledDataSourceFactory extends UnpooledDataSourceFactory
....
- 产品实现类(使用连接池...)
public class PooledDataSource implements DataSource {
public PooledDataSource() {
dataSource = new UnpooledDataSource();
}
public PooledDataSource(UnpooledDataSource dataSource) {
this.dataSource = dataSource;
}
public PooledDataSource(String driver, String url, String username, String password) {
dataSource = new UnpooledDataSource(driver, url, username, password);
expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
}
- mybatis 数据源定义配置(客户端)
- 小结
产品接口
&工厂接口
就是设计中常见的顶层
模块
实现类就是可变
模块
- tips
工厂模式是我们最常用的
实例化
对象模式了,是代替new
操作的一种模式。
工厂方法确实为系统结构提供了非常灵活强大的动态扩展机制,增加拓展功能,只需添加对应的工厂接口和产品实现接口的一对实现,系统其他地方无需变更。
工厂方法类比于建造者Build模式,前者偏向于类的实例化(new出来),后者侧重于类的初始化构建,属性填充
,不再是简单new.
二、装饰器模式
通过组合的方式动态地给一个对象添加一些额外的职责或者行为
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责,很契合设计模式原则之一开闭原则
当对象族实现复杂度已经无法用继承来实现(eg:可能有大量独立的扩展)
装饰类增加了系统的扩展性,对单元测试友好
为支持每一种组合将产生大量的子类,使得子类数目呈现组合叠加爆炸性增长)
- 源码套路
X x=new X1(new X2(new X3()))......
UML类图
一层一层嵌套,"装饰器类" 持有目标对象的引用,具体方法执行委托给具体的目标对象子类形成了一连串"装饰器链",不断地增强功能
应用场景1
mybatis 二级缓存
-
抽象组件具体实现类 (cache.impl包下)
装饰器族(cache.decorators包下)
- build模式构建二级缓存
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
- 装饰器入口(重点看装饰器链的组成)
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
cache = new SerializedCache(cache);
}
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
四不四很符合X x=new X1(new X2(new X3()))......[呲牙]
- 以FifoCache分析一下装饰器的使用
public class FifoCache implements Cache {
private final Cache delegate;
public FifoCache(Cache delegate) {
this.delegate = delegate;
this.keyList = new LinkedList
Cache的设计上目标类和装饰器类实现了Cache接口,装饰器一个一个装饰器类串起来,层层包装目标类形成一个链
增强的功能一览
- ScheduledCache:根据时间间隔清理缓存数据
- LoggingCache:缓存命中率打印(debug)
- LruCache:最近最少使用原则
- SoftCache:jdk软引用策略,jvm内存不足时回收缓存对象
- 等等.....
应用场景2
JDK IO流
FileInputStream in=new FileInputStream(new File ("hello.txt"));
BufferedInputStream inBuffered=new BufferedInputStream (in);
BufferedInputStream是一层装饰,增强了“缓冲区”的功能....
等等.....
三、模板设计模式 & 策略模式
定义一个操作中算法的骨架或流程,充分利用"多态"使得子类可以不改变算法的结构即可重新定义实现
适用场景
完成一件事情,有固定的流程步骤比如说 1->2->3->4,但是每个步骤根据子类对象的不同,而实现细节不同,就可以在父类中定义不变的方法,把可变的方法通过子类回调来实现
关键字: 回调
- UML类图
- spring/mybatis/dubbo出现频率较高,一般表现为抽象类+protected+abstract方法组合
- 代码套路
public abstract class Abstractxxxxxx{
public void method{
//校验逻辑
//参数装配
//业务逻辑1、2、3
doMethod();
//异常处理
//资源清理
}
//抽象方法由子类实现,父类回调...
protected abstract void doMethod() ;
}
mybatis Executor执行器
BaseExecutor 模板抽象父类,定义不变方法和抽象方法
一下三个是模板方法子类,也就是具体的实现策略.
SimpleExecutor 简单执行器类,常规的CURD
ReuseExecutor 复用Statement执行器
BatchExecutor 批量update执行器类
策略模式
动态的改变对象的行为,实现某一个功能有多种算法或者策略,多种不同解决方案动态切换,起到改变对象行为的效果,一般会结合模板方法模式配合使用
public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 一级二级缓存处理逻辑....
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List list;
queryStack++;
list = resultHandler == null ? (List) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//真正的查询db入口
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
//todo 一些清理方法,缓存,事务,连接关闭等等
查询db方法入口
private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {
//省略....
//local缓存
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//doQuery是抽象方法,可变方法交给具体的子类去实现
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//后置处理.....
return list;
//protected abstract 暗示着需要子类去实现
protected abstract List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
update/delete/insert套路同上
- 模板方法的一个策略实现类SimpleExecutor,真正的执行器
public class SimpleExecutor extends BaseExecutor {
//真正查询db的入口实现
public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
方法调用,根据ExecutorType类型不同选择不同的实现类
sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
Executor选择器入口
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
//根据参数选择执行器实现 -> 策略模式
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
//上文装饰器的包装入口 -> 装饰器模式
executor = new CachingExecutor(executor);
}
//插件拦截器链的入口-> 责任链模式
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
- 客户端根据实际情况选择消费策略算法
客户端依赖抽象类,实现类依赖的抽象,一定程度上契合了依赖倒转原则客户端和实现类通过抽象类关联在一起。
四、适配器模式
将一个类的接口转换成客户希望的另外一个接口
-
UMl类图(类适配器)
-
Adpter
也可以是抽象为接口,适配由子类实现 -
对象适配器
类图类似,Adpter
和Adptee
关系由继承->关联,也就是委托
业务代码中,面向适配器开发可以拓展新功能,这里适配器模式也有一点装饰器的意思,只不过"动机"不同!一个是单纯的增强,一个是适配
应用场景
mybatis 日志框架适配
- Target目标日志接口:
等待适配
package org.apache.ibatis.logging;
public interface Log {
boolean isDebugEnabled();
void error(String s, Throwable e);
debug
tarce
warn
.....
}
- log4j适配器类,采用了
对象适配
-委托给"org.apache.log4j
.Logger"
package org.apache.ibatis.logging.log4j;
import org.apache.ibatis.logging.Log;
import org.apache.log4j.Logger;
/**
* @author Eduardo Macarron
*/
public class Log4jImpl implements Log {
private Logger log;
public void error(String s, Throwable e) {
log.log(FQCN, Level.ERROR, s, e);
}
其他日志适配器类也一样,继承"org.apache.ibatis.logging.Log"类,里面持有对第三方日志框架的日志记录类的引用
- 客户端即加载 LogFactory类的时候,首先会去根据配置文件动态确定使用哪个第三方日志框架
- 解析xml配置文件的构造器代码截取
Class extends Log> logImpl = (Class extends Log>)resolveClass(
props.getProperty("logImpl"));
configuration.setLogImpl(logImpl);
Mybatis没有自己的日志系统,依赖于第三方实现,通过配置文件参数根据Logfactory来适配对应的第三方日志系统(log4j,jdk-log,commonslogging)过程略,可参照slf4j
日志适配模式
五、快速讲解门面模式之SLF4J
- SLF4J相当于一层门面
Facade
,日志框架的抽象,提供了对日志子系统(log4j,logback,jdk-logging
)的入口 - 工厂模式获取当前类的日志对象,对于具体的实现,客户端是不知道的,实际使用是绑定具体实现类
-
门面模式-UML
子系统:
职责单一,易于维护
门面:充当了客户类与子系统类之间的“第三者”,对客户端隐藏了很多细节,也就是"最少知道",比较契合迪米特法则
迪米特法则:如果两个类不必彼此直接通向,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个累哦的某一个方法的话,可以通过第三者转发这个调用
门面模式的Facade充当这个第三者
同理->自行脑补:门面模式也比较符合依赖倒转
原则 Facade充当中间层-抽象接口
五 代理模式
为其他对象提供一种代理以控制对这个对象的访问,可能是主流开源框架使用频率最高的设计模式!
主要有3个角色:访问者
、代理人
、被代理人
-
静态代理类图
静态代理中每个代理类只能为一个接口服务,这样程序开发中必然会产生许多的代理类,冗杂的代理类阅读起来也是一种灾难!
所以我们就会想办法可以通过一个代理类完成全部的代理功能,那么我们就需要用动态代理
动态代理本质上也是
静态代理
,只不过是以字节码增强,类加载器根据目标target类二进制文件,动态生成其代理代码,并载入jvm方法区,类名多以xxx$xxxx.classMybais核心原理
1、MyBatis框架是如何去执行SQL语句?
2、Xml文件的SQL是如何和Mapper文件绑定在一起的?第一步从获取Mapper对象入手
//全局配置获取mapper对象
public T getMapper(Class type) {
return configuration.getMapper(type, this);
}
// mapper注册工厂MapperRegistry获取,Mybatis启动会扫描所有的Mapper接口
public T getMapper(Class type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
- 具体实现 创建代理类
public T getMapper(Class type, SqlSession sqlSession) {
// knownMappers是mapper代理的缓存map
final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//key code 最终调用 `newInstance` 方法
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
protected T newInstance(MapperProxy mapperProxy) {
//JDK动态代理生成对应的mapper接口
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
- 动态代理类需要实现InvocationHandler接口,对应的invoke业务方法
// Mapper接口调用会执行如下逻辑
public Object invoke(Object proxy, Method method, Object[] args) {
//省略.....
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 交给MapperMethod处理
return mapperMethod.execute(sqlSession, args);
}
小结:cachedMapperMethod方法维护了一个接口名+方法名
的map集合,和Xml文件中的 namespace
+ id
标签一一对应
伪代码
- Execute执行器业务代码
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
if (SqlCommandType.INSERT == command.getType()) {
//如果对应的类型是insert ,也就是对应Xml文件中的标签
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
//如果对应的类型是update ,也就是对应Xml文件中的标签
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
//如果对应的类型是delete ,也就是对应Xml文件中的标签
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
//如果对应的类型是select ,也就是对应Xml文件中的
- 一层一层最终会调用Executor直接和JDBC交互
public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
//StatementHandler封装了原生的Statement
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
- 客户端使用,单独使用Mybatis
//1、创建SessionFactory
sqlSessionFactory=new SqlSessionFactoryBuilder().build(reader);
//2、打开Session
session=sqlSessionFactory.openSession();
//3、获取用户接口mapper对象
UserMapper userMapper=session.getMapper(UserMapper.class);
//4、执行crud方法
User user=userMapper.selectUserById(1000L);
// 最终实际执行的CRUD方法实际上是代理类执行Executor进行JDBC的常规操作.
六 建造者模式
建造者模式一般用来构建复杂对象,区别于工程模式实例化简单对象.
建造者模式屏蔽了对象创建的复杂细节,对象的构造和表示相分离
BUILD模式类图
Product-
产品角色
: 一个具体的产品对象
Builder-抽象建造者
:创建一个Product对象的各个部件指定的抽象接口
ConcreteBuilder-具体建造者
:实现抽象接口,构建和装配填充属性
Director-指挥者
: 主要是用于创建一个复杂的对象。它主要有两个作用,一是:隔离了客户与对象的生产过程,二是:负责控制产品对象的生产过程。
Mybatis
中关于建造者模式的应用:我们先看看Mybatis
是如何集成到Spirng
容器中的,通过 Spring
来管理,完美兼容?
其实很多开源框架都有集成spring的需求:借助spring的可拓展机制开发一个第三发的插件来完成对自身的集成和适配
- Spring可拓展利器-
FactoryBean
了解一下. - Mybatis和Spring的集成通过一个插件mybatis-spring来完成
先从配置文件入手....
//配置sqlSessionFactory,SqlSessionFactoryBean是FactoryBean的一种典型实现
//.....
FactoryBean
和BeanFactory
的区别是一个老生常谈的问题
BeanFactory
提供了IOC容器最基本的形式,给具体的IOC容器的实现提供了规范,暂时不展开...
FactoryBean
为IOC容器中Bean的实现提供了更加灵活的方式,给Bean的实现加上了一个简单工厂模式和装饰模式 : 可以在getObject()
方法中灵活配置,完成复杂Bean的装配
FactoryBean
通常用来构造复杂的bean和属性填充(简单的配置无法实现)
关于spring的可拓展机制,有机会我会单独讲解.
- Mybatis-spring插件中的FactoryBean
重点看getObject()方法
public class SqlSessionFactoryBean implements FactoryBean {
//getObject方法返回一个构建完成的sqlSessionFactory
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
//实际调用buildSqlSessionFactory来创建..... InitializingBean接口实现
public void afterPropertiesSet() throws Exception {
this.sqlSessionFactory = buildSqlSessionFactory();
}
}
- SqlSessionFactory充当了一个
Director角色
MyBatis核心配置类 Configuration 是产品类Product角色
,庞大且复杂,初始化比较麻烦,使用了专门的建造者SqlSessionFactoryBuilder
进行构建
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
//待创建的全局配置Configuration,也就是带装配的 `Product` 角色
Configuration configuration;
//Xml方式build全局配置
XMLConfigBuilder xmlConfigBuilder = null;
if (this.configLocation != null) {
xmlConfigBuilder = new XMLConfigBuilder(configLocation.getInputStream(),
null, this.configurationProperties);
configuration = xmlConfigBuilder.getConfiguration();
}
//省略.....
return this.sqlSessionFactoryBuilder.build(configuration);
}
//最终创建一个包装了Configuration的SqlSessionFactory
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
- 具体的建造者:直接构建,并没有接口定义
public class SqlSessionFactoryBuilder {
//省略.......
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
Mapper接口Bean并没有实现类,如何能实现自动装配和依赖注入?
- 答案就是Spring可拓展机制利器之一
BeanFactoryPostProcessor
和FactoryBean
- 关键配置入口
//mapper扫描和装配配置
BeanFactoryPostProcessor
可以插手Spring-bean
的实例化过程...(另一个利器是BeanPostProcessor,大家可以自行了解
)
该可拓展接口的在Mybatis
和 Spring
集成的作用简单总结起来就是Spring若干个手动操作
:
- 扫描basePackage包下所有的mapper接口类,并
手动
将mapper接口类封装成为BeanDefinition
对象, -
手动
注册到Spring的BeanFactory
容器中:以便可以使用@Autowired
或@Resource
来自动注入了 -
MapperFactoryBean
手动装配一个Spring容器的关于mapper的实例bean(实际上是代理) - 具体代码细节过于繁杂,不再展开了....