mybatis源码阅读与设计模式分析
1、目标
orm框架…一直在用,不解释
- 了解mybatis实现原理
- 梳理用到的设计模式,思考中这个场景使用该设计模式到好处以及是否有弊端。
- 对于框架里面遇到的一些有意思的方案(如果有的话),思考是否有其它替代到方案,并横向对比优劣。
2、阅读官方文档
习惯性到官网瞅瞅…突然发下一些以前不知道的特性,故而记录下~
2.1、文档中读到到
- MyBatis 3.4.2 开始,可以为占位符指定一个默认值(默认关闭,需要开启)。例如
<property name="username" value="${username:ut_user}"/>
,默认username为ut_user - 当返回行结果所有列是空到时,mybatis默认返回null; 如果配置了
returnInstanceForEmptyRow=true
, mybatis会返回一个空实例[或空列表]。 shrinkWhitespacesInSql=true
从SQL中删除多余的空格字符。请注意,这也会影响SQL中的文字字符串。 (新增于 3.5.5) 【这条特性有什么用?把字符串到空格都删了??】- xml允许使用别名。例如一条sql的返回类型是:Author,中xml中可以直接写成
<... resultType="author">
java bean的别名默认为首字母小写的非限定类名。所以xml里面,java.util.List
可以直接写成list
- mybatis从参数或者结果集取值的时候,用不同类型处理器将取到的值转换成java类型(BooleanTypeHandler,ShortTypeHandler等)。可以重写已有的类型处理器或者新增自己的类型处理器以支持更多类型(实现
org.apache.ibatis.type.TypeHandler
或者继承org.apache.ibatis.type.BaseTypeHandler
)。 - mybatis允许使用插件对sql语句执行过程的某一点做拦截调用。实现
Interceptor
接口,指定要拦截等方法。
允许拦截的方法调用包括(这个记录下,写代码的时候可能要用到):
Executor (update, query, flushStatements, commit, rollback,
getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
- mybatis xml中
<select>
支持配置timeout
属性
3. 源码
3.1. 总览
3.1.1. 包结构
org.apache.ibatis.annotations -- 注解
org.apache.ibatis.binding -- 生成mapper接口的动态代理
org.apache.ibatis.builder -- 包含Configuration对象所有构建器,xml和注解两种方式配置的解析
org.apache.ibatis.cache -- 缓存
org.apache.ibatis.cursor -- 游标
org.apache.ibatis.datasource -- 数据源
org.apache.ibatis.exceptions -- 异常
org.apache.ibatis.executor -- sql执行器 核心
org.apache.ibatis.io -- io
org.apache.ibatis.jdbc -- jdbc
org.apache.ibatis.lang --
org.apache.ibatis.logging -- 日志
org.apache.ibatis.mapping -- 配置文件与实体文件映射
org.apache.ibatis.parsing -- 解析
org.apache.ibatis.plugin -- 拦截器
org.apache.ibatis.reflection -- 反射
org.apache.ibatis.scripting -- 动态sql语言 <if> <where>等
org.apache.ibatis.session -- sql session 核心
org.apache.ibatis.transaction -- 事务
org.apache.ibatis.type -- 类型处理器
3.1.2. 流程
public static void main(String[] args) throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession sqlSession = sqlSessionFactory.openSession();) {
DemoMapper demoMapper = sqlSession.getMapper(DemoMapper.class);
List<Demo> list = demoMapper.selectProductList();
}
}
<configuration>
<properties resource="db.properties" />
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="me/hhy/mapper/DemoMapper.xml"/>
</mappers>
</configuration>
从上面的使用实例可以看出mybatis运行的流程:
- 根据配置文件,创建SqlSessionFactory。-- 这里解析配置文件并根据配置文件创建一个SqlSessionFactory
- 打开一个mysql会话 – sqlSessionFactory.openSession
- 获取mapper接口
- 调用接口方法 – 执行sql
3.2. 细读
3.2.1. 创建SqlSessionFactory(配置[xml]解析部分)
3.2.1.1 加载配置文件mybatis.xml
按前面的使用示例显示,加载配置文件使用的是Resource.getResourceAsStream(“filename”),返回一个InputStream。功能比较简单,但有值得学习的点:
public class Resources {
private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();
public static InputStream getResourceAsStream(String resource) throws IOException {
return getResourceAsStream(null, resource);
}
public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
if (in == null) {
throw new IOException("Could not find resource " + resource);
}
return in;
}
}
public class ClassLoaderWrapper {
public InputStream getResourceAsStream(String resource) {
return getResourceAsStream(resource, getClassLoaders(null));
}
public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
return getResourceAsStream(resource, getClassLoaders(classLoader));
}
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
for (ClassLoader cl : classLoader) {
// 从classloader加载指定资源 cl.getResourceAsStream(resource);
}
}
ClassLoaderWrapper() {
try {
systemClassLoader = ClassLoader.getSystemClassLoader();
} catch (SecurityException ignored) {
// AccessControlException on Google App Engine
}
}
...
1、对classloader进行了包装(ClassLoaderWrapper)
Java中资源加载绕不过classloader,而Java中默认提供了多个classloader,且允许用户自定义classloader。如果不做一层包装,找资源不方便。
这里一般都能想到要封装,区别就是如何做。可能第一时间想到的是直接在Resource类里面封装一个类似于ClassLoaderWrapper#getResourceAsStream的方法。但明显是把多classloader的逻辑抽离出一个新的类更合适。因为Resources只需要关心资源加载,多个classloader这个细节应该对Resource屏蔽【单一职责、最少知识原则】
2、 还有一个细节是定标准(接口)要充分考虑扩展。
比如ClassLoaderWrapper中有两个方法签名getResourceAsStream(String resource, ClassLoader classLoader)
与 getResourceAsStream(String resource)
考虑了有无自定义classloader的场景。自己做设计的时候,也一定要尽量考虑全面。当然,也不要因为“扩展”,写很多可能一直不会用到的方法,需要有权衡。
3.2.1.2 SqlSessionFactory创建
通过SqlSessionFactory构建,build分为三步。1)根据配置文件,构建XMLConfigBuilder;2)XMLConfigBuilder解析出Configuration;3)根据Configuration返回SqlSessionFactory
1、构建XMLConfigBuilder
回忆下上面的mybatis.xml内容,配置文件里最重要的两部分:environments和mapper,一个定义了环境,一个定义了mapper。
XMLConfigBuilder 有几个关键属性
public class XMLConfigBuilder extends BaseBuilder {
private final XPathParser parser; // 解析xml的,对jdk中的实现做了层封装
private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory(); // 反射工厂
protected final Configuration configuration; // BaseBuilder 中的
}
- localReflectorFactory有个细节,在mybatis中,
ReflectorFactory
是个接口,只有一个默认实现DefaultReflectorFactory
。设计要考虑扩展,面向接口编程 - XPathParser 解析xml(调用的是jdk里面的实现
parser.evalNode("/configuration")
,读取节点,解析到Configuration) - configuration 从xml解析出来的属性会设置到这个实例中
从configuration标签解析出后,主要读取了以下标签
// root为configuration节点
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
挑选个比较关心的mapper节点看看, 例如以下配置
<mappers>
<mapper resource="me/hhy/mapper/xxMapper.xml"/>
</mappers>
mapperElement(root.evalNode("mappers"));
的解析步骤:
(1)使用Resources类读取文件,返回流(这块和前面读取mybatis.xml一样)
(2)根据文件流以及configure等配置生成XMLMapperBuilder
(mybatis.mxl解析用的是XMLConfigBuilder
)
(3)调用XMLMapperBuilder#parse
(parser.evalNode("/mapper")
)
(4)解析mapper节点下的子节点(例如:parameterMapElement(context.evalNodes("/mapper/parameterMap"));
等)
2、构建SqlSessionFactory
得到Configuration后,直接new DefaultSqlSessionFactory(Configuration config))
3.2.1.2. mybatis初始化(SqlSessionFactory构建部分)总结
- 读取配置文件(由Resource类负责)
- 解析配置文件(XMLxxBuilder系列类负责)
- 解析完配置文件,将文件内容保存到
Configuration
,SqlSessionFactory
持有Configuration
读取配置文件部分个人感觉写的比较好,主要是前面提到的Resource
中使用ClassLoaderWrapper
、还有一些对于扩展的考虑。
解析配置文件部分结构也是比较清晰的(仅仅是比较清晰),XMLxxBuilder系列的类各自有不同的职责。总体感觉代码没上周看的soul框架的清爽,mybatis的代码风格一般,不过其中的设计思想还是值得学习的。
3.2.2. 打开一个mysql会话 – sqlSessionFactory.openSession
sqlSessionFactory.openSession操作涉及的关键类(接口):
关键的类(接口):
- Transaction,定义了事务的操作。关键的方法:
Connection getConnection()
、void commit()
、void rollback()
等 - Executor,sql执行器。关键方法:
update(MappedStatement ms, Object parameter)
、query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler
等 - SqlSession,openSession的返回值。关键方法:
selectOne
、selectList
等
流程:
- 从环境配置中获取TransactionFactory
- 使用TransactionFactory创建事务(这里包含了dataSource信息,从Configuration)
- 生成执行器Excutor
BatchExecutor
,ReuseExecutor
,SimpleExecutor
,CachingExecutor
newExecutor的时候根据不同executorType new不同的实现。(interceptorChain.pluginAll(executor)
看起来openSession并没有建立数据库连接…
3.2.3. sqlSession.getMapper(DemoMapper.class);
小标题的代码会返回了一个DemoMapper对象,我们调用接口方法,即执行了sql语句。DemoMapper是一个接口,并没有实现,不难想到,通过动态代理实现功能。
这边返回的其实是一个代理类:MapperProxy
。
代理类的选择:在sql解析阶段,会将mapper add进来,维护在MapperRegistry
的Map<Class<?>, MapperProxyFactory<?>> knownMappers
属性中。getMapper的时候,会从knownMappers中获取。
重点看代理类的实现(类和方法中的代码均经过省略),调用mapper的方法,会调用代理类的invoke方法。在invoke里面,封装参数、执行sql调用、构建返回结果。
3.2.3.1 动态代理对象invoke关键步骤总览
- 解析出真实的sql
SqlSource#getBoundSql
,这里包含动态参数解析
mybatis中使用DynamicSqlSource处理动态sql, 实现了SqlSource接口。getBoundSql就是调用的SqlSource#getBoundSql。DynamicSqlSource里有个SqlNode类型动属性rootSqlNode,是用户传入动动态sql参数。SqlNode这边有个组合模式下小节会讲到。 - 构建StatementHandler对象,如果有插件的话,会生成插件的代理对象。例如sql拦截器就是一个插件
根据类型构建StatementHandler,遍历插件(如果有的话),调用插件的plugin方法,传入hanlder,返回插件生成的动态代理对象(InvokeHandler被指定为Plugin类)StatementHandler这有一个装饰器模式下小节会讲到
这里生成StatementHandler还有个细节特别妙,生成完StatementHandler后,会作为插件处理的入参,根据插件再生成StatementHandler的代理对象。下小节讲(StatementHandler) interceptorChain.pluginAll(statementHandler);
- 打开数据库连接最终调用的是
dataSource.getConnection();
,例如mybatis自带的PooledDataSource
的getConnection
就是通过java.sql包里的DriverManager.getConnection()
方法实现的。
DataSource
是一个接口,解析阶段从xml中来的。mybatis实现了UnpooledDataSource
和PooledDataSource
, 也可以使用第三方的链接池或者自定义。【面向接口编程真香】 - 调用StatementHandler的query方法,传入 Statement 与 resultHandler。这里有一点要注意,statementHandler其实是一个动态代理对象, 并且InvokeHandler为Plugin类。Plugin的invoke会调用拦截器方法
interceptor.intercept
。之后就是调用statement执行真正的sql语句,使用ResultHandler解析结果。
3.2.3.2 动态代理对象invoke关键步骤设计亮点
3.2.3.2.1 SqlNode-组合模式【组合模式】,
组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
public class DynamicSqlSource implements SqlSource {
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) { ... }
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
...
}
}
节点抽象
public interface SqlNode {
boolean apply(DynamicContext context);
}
组合对象:DynamicSqlSource里的rootSqlNode对象类型一般为MixedSqlNode。包含了多个Node
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
}
组合对象:ChooseSqlNode包含多个SqlNode 对应mybatis xml里标签
public class ChooseSqlNode implements SqlNode {
private final SqlNode defaultSqlNode;
private final List<SqlNode> ifSqlNodes;
public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {...}
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : ifSqlNodes) {
if (sqlNode.apply(context)) {
return true;
}
} ...
return false;
}
}
单个对象:TextSqlNode就是一个简单的SqlNode
public class TextSqlNode implements SqlNode {
private final String text;
public TextSqlNode(String text) {...}
@Override
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}
}
从上面代码可以看出DynamicSqlSource只持有一个rootSqlNode节点。多节点也是封装成一个MixedSqlNode节点。对于SqlSource来说,Node结构是无感知的。它只需要调用rootSqlNode.apply(context);将动态动入参应用到上下文。而MixedSqlNode#apply会遍历持有的node list,分别调用各自的SqlNode#apply。TextSqlNode就是简单的对string做处理。ChooseSqlNode就比较复杂,因为里面还包括子标签等。所以ChooseSqlNode里也有一个node list, 也需要对自己所有的子标签做处理。
3.2.3.2.2 StatementHandler的装饰器模式
上小节讲到构建StatementHandler对象有个装饰器模式
构建StatementHandler一般是如下调用,StatementHandler是抽象接口,RoutingStatementHandler是包装类。
这里的RoutingStatementHandler有点拿来当工厂用的感觉… 虽然他是一个包装类,对RoutingStatementHandler做实际调用的时候,其实调用的是实际对象SimpleStatementHandler
、PreparedStatementHandler
、CallableStatementHandler
的方法。
我理解的装饰器模式,应该是更倾向与包装类装饰实际对象的方法实现。例如实际对象的A方法从db查数据,缓存包装对象的A方法先从缓存查,找不到再从db查。但并不是说他这里用的不合适,代码整洁明了即可。
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
public class RoutingStatementHandler implements StatementHandler {
private final StatementHandler delegate;
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
return delegate.prepare(connection, transactionTimeout);
}
@Override
public void parameterize(Statement statement) throws SQLException {
delegate.parameterize(statement);
}
...
}
3.2.3.2.3 构建StatementHandler,处理插件的部分
首先,构建StatementHandler的代码如下。interceptorChain.pluginAll(statementHandler);
是根据插件返回动态代理对象部分。
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
细看pluginAll方法:调用的第一次,我们new 出来的statementHandler(也就是target)被作为plugin的入参,返回的是一个代理对象,也同样给target赋值。
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
例如现在有a,b,c三个插件。
StatementHandler mHandler = …;
mHandler = interceptorChain.pluginAll(mHandler)的过程就是:
a代理对象 = a插件.plugin(mHandler);
b代理对象 = b插件.plugin(a代理对象);
c代理对象 = c插件.plugin(b代理对象);
在插件里面,可以选择要不要继续传递调用
总结
这个框架里面好多源码没细看,只看了大体流程
关于设计的收获(其实道理都懂…不过确实看别人的代码对这块能有更深的理解):
- 面向接口编程- 定义标准与高扩展性
- 单一指责、最少知识
- 对组合模式应用的理解(树形结构的处理)
- 动态代理【代理模式】应用的理解(感觉动态代理在各种框架源码里用的比较多…这里有体会,但不太好说…)
- 装饰器模式 包装对象当工厂用…感觉是第一次见,但并没有感觉有啥不合适…
- 总感觉插件的处理(3.2.3.2.3小节)那块代码写得很好…眼前一亮的感觉。
框架里用到的其他设计模式(这些比较简单,就没拎出来写了):
Builder模式、模版方法模式、单例模式、工厂方法模式
其他的收获:
- 了解mybatis的大体实现流程(以后面试问到应该不惧)
https://mybatis.org/mybatis-3/zh/index.html
https://www.jianshu.com/p/7bc6d3b7fb45
https://www.jianshu.com/p/46c6e56d9774