一文看懂 MyBatis 原理与核心组件!
共 8158字,需浏览 17分钟
·
2021-11-23 20:22
你知道的越多,不知道的就越多,业余的像一棵小草!
你来,我们一起精进!你不来,我和你的竞争对手一起精进!
编辑:业余草
cnblogs.com/insaneXs/p/12997368.html
推荐:https://www.xttblog.com/?p=5292
一文看懂 MyBatis 原理与核心组件
Configuration
Configuration
是mybatis的全局配置类,保存了环境对象Enviroment
(Environment
表示数据源相关环境),各种配置信息,以及作为各种资源解析后的注册表。
例如,MapperRegister
表示Mapper
的注册表,TypeHandlerRegistry
是TypeHandler
的注册表,TypeAliasRegistry
是TypeAlias
的注册表,另外还以Map的形式保存了MappedStatement
, ResultMap
,ParameterMaps
等的映射关系,其中key均是namespace + id
的形式。
SqlSessionFactory
SqlSessionFactory
是负责创建SqlSession
的工厂。
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
主要是通过openSession()
创建SqlSession。此外,还有一个返回全局配置对象的方法getConfiguration()
。可以猜测其实现类应该会直接或间接的保持对Congfiguration
的引用。
观察openSession()
的参数,猜测创建SqlSession的方式有两种,一种直接基于传入的数据库连接Connection
。另一种通过全局配置对象Configuration
在获取数据源环境Enviroment
,在获取环境。SqlSessionFactory
的默认实现是DefualtSqlSessionFactory
。
SqlSession
SqlSession
表示某次数据库操作会话,因此SqlSession
接口定义的主要是CRUD和事务操作的相关接口。
另外还有个重要的方法getMapper
,可以返回对应Mapper的对象。
❝注意:SqlSession CRUD相关方法的参数第一个参数均为statement的字符串,这个字符串并非SQL语句,而是MappedStatement的ID。
❞
SqlSession
的默认实现是DefualtSqlSession
。DefaultSqlSession
不是线程安全的,使用者需要自己确保线程安全问题,或者是使用SqlSessionManager
,它提供了SqlSession
的线程安全管理。
Executor
执行器,负责真正执行数据库操作,并且提供了缓存的能力。
每个DefualtSqlSession
内部都有一个Executor
,在创建DefaultSqlSession
实例时,同时创建了Executor
对象,因此Excecutor
和SqlSession
是一对一绑定的。
Executor
可以分为两类:
第一类是 BaseExecutor
以及子类,这类的Executor
有操作数据库的能力,并且提供了mybatis的一级缓存。第二类是 CachingExecutor
,它对第一个的执行器进行了包装,提供了二级缓存,并在二级缓存未命中时,委托给内部的第一类执行器处理。
MappedStatement
MappedStatement
表示的是mapper.xml中定义的一个SQL节点。当创建Configuration对象在创建xml时,就会将一个个节点解析成对应的MappedStatement
对象。
MappedStatement
中大部分属性都可以在xml的定义中找到相关的配置。
四种处理器 TypeHandler,ParameterHandler,StatementHandler,ResultSetHandler
TypeHandler 类型处理器,提供了Java对象和JDBC TYPE的转换。
public interface TypeHandler<T> {
//将某个Parameter Java类型 转成 JDBC 类型 用于执行
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
//将结果集中中的某列 转成 Java类型
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
通常我们可以拓展这个接口实现自定义枚举类型与JDBC的转换类型。
ParameterHandler 参数处理器,负责将PreparedStatement中的占位符替换成对应的参数。
public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement ps)
throws SQLException;
}
StatementHandler 核心组件,与数据库交互,从数据库连接中获取Statement对象,执行SQL,并映射结果集等功能。
public interface StatementHandler {
Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;
void parameterize(Statement statement)
throws SQLException;
void batch(Statement statement)
throws SQLException;
int update(Statement statement)
throws SQLException;
List query(Statement statement, ResultHandler resultHandler)
throws SQLException ;
Cursor queryCursor(Statement statement)
throws SQLException ;
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}
ResultSetHandler 结果集处理器,StatementHandler获取到结果集后 ResultSet
,会提交给ResultSetHandler
处理,以转换成Java对象集合。
public interface ResultSetHandler {
List handleResultSets(Statement stmt) throws SQLException ;
Cursor handleCursorResultSets(Statement stmt) throws SQLException ;
void handleOutputParameters(CallableStatement cs) throws SQLException;
}
SqlSource和BoundSql
一个SqlSource
表示MappedStatement
定义的Sql片段,一个SqlSource
可能由多个SqlNode
组成。
而BoundSql
是SqlSource
应用了上下文环境(指用户输入参数)后得到的对象,对SqlSource
中条件和参数做了筛选,形成的实际SQL(仍可能有'?'占位符)。
执行过程
以一个简单的程序作为入口来看看mybatis一次查询执行的主要流程。
class MybatisTest{
public static void main(){
//STEP 1
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//STEP 2
try (SqlSession session = sqlSessionFactory.openSession()) {
//STEP 3
BlogMapper mapper = session.getMapper(BlogMapper.class);
//STEP 4
Blog blog = mapper.selectBlog(101);
}
}
}
形成全局配置对象,构建SqlSessionFactory
第一步是读取全局的配置文件,解析文件形成我们的全局配置Configuration。解析的过程主要是针对xml文件各节点的解析,本文目的为把握主体流程,这里不深入分析。
得到配置对象后,SqlSessionFacotryBuilder
会将Configuration
对象传入创建SqlSessionFactory
对象。
打开SqlSession
得到SqlSessionFactory
工厂对象后,可以通过openSession()
方式获得SqlSession
对象(默认是DefaultSqlSession
)。
DefaultSqlSession
的构造函数依赖三个参数,分别是Configuration
,Executor
和autocommit
。Configuration
是全局的,而Executor
却是跟DefaultSqlSession
一一绑定的(也就是说在创建DefaultSqlSession
的时候, 会创建一个新的Executor
,并且这个Executor
不会暴露给其他SqlSession
使用),理解这一点对搞清一级缓存很有用。
获得代理对象
当调用SqlSession.getMapper()
时,首先会从Congifuration
的注册表中查找对应类型是否已经注册,没有则抛出异常。
如果存在,则通过MapperProxyFactory
创建代理对象。MapperProxyFactory
主要是通过JDK动态代理创建代理对象的,这一过程分为两步:
先创建JDK动态代理中的重要组件 InvocationHandler
,该接口在这里对应的实现是MapperProxy
对象,而且MapperProxy
保存了对SqlSession
的引用。再通过Proxy.newProxyInstance() 获得动态代理的对象。
❝注意:就算是相同的SqlSession,每次getMapper得到的代理对象也并非同一个,只不过对于相同SqlSession创建的Mapper而言,MapperProxy引用的SqlSession相同。
❞
代理对象通过反射调用执行方法
既然代理对象是JDK动态代理创建的,那么其方法的执行最终会落到InvocationHandler,也就这里的MapperProxy的invoke中。
而MapperProxy.invoke()
又调用了MapperMethod.execute()
。MapperMethod.execute()
在SQL的执行前后做了两件事,处理参数,以及对执行结果进行计数,而核心的SQL执行还是交回给了SqlSession对象。
Executor执行器执行
SqlSession
在执行CRUD时,会从Configuration
查找对应的MappedStatement
对象,然后将MappedStatement
传递给Executor
对象执行。
此时,如果开启了二级缓存,CachingExecutor
会先从MappedStatement
的Cache中查找,如果缓存未命中,CachingExecutor
则会将查找任务委托给内部的BaseExecutor
。而BaseExecutor
则会先从内部的LocalCache
中查找,如果缓存未命中,则将SQL的执行交给StatementHandler
。
StatementHandler执行SQL
StatementHandler的执行过程分为两个阶段:
准备阶段:这一阶段的主要目的是得到Statement对象 执行阶段:通过Statement执行SQL
当得到Sql的执行结果后,还会应用ResultSetHandler,将结果集转换成Java容器类。
用一副粗糙的图概括上述业务流程图:
一级缓存与二级缓存
什么是一级缓存
一级缓存是Executor内部的缓存机制。主要原理是BaseExecutor有一个叫localCache的字段用来存放这个会话的执行结果。因此,一级缓存是SqlSession内部的缓存(因为Executor和SqlSession是一一绑定的)。
一级缓存的有效期是某一次会话过程,一旦会话关闭,一级缓存也就失效。另外,如果会话中发生了增删改的写操作,一级缓存的会话同样会失效。
什么是二级缓存
二级缓存是MappedStatement的缓存,MappedStatement有一个Cache字段用来存放二级缓存。因此,我们常说二级缓存是跨SqlSession的。二级缓存默认是关闭的,如果希望开启二级缓存需要同时确保mybatis设置中的Cache打开,以及对应的MappedStatement开启了缓存。
那么二级缓存的实现原理是怎么样的?
我们知道二级缓存的使用者是CachingExecutor,在CachingExecutor执行查询前会先查看MappedStatement中是否存放对应的缓存。
如果缓存未命中,CachingExecutor会由内部的BaseExecutor执行数据库查询操作,得到查询结果后,CachingExecutor交给内部的TransactionCacheManager保存。只有当事务提交完成后,TransactionCacheManager保存的缓存才会写入MappedStatement的Cache中。
读者可以自己思考下这么做的用意。
二级缓存的脏读
因为二级缓存是与MappedStatement绑定的,换句话说就是和命名空间绑定的,假设存在这个一个情况,MappedStatement A 缓存了User的数据,但是MappedStatment B 可能也对User表进行了修改,但是 A中的缓存无法感知这一变化,缓存一直生效。这就产生了二级缓存的脏读问题。
为了避免上述问题,首先我们在开发的时候需要确保相应的规范,让相同表的操作尽量在相同的命名空间下。如果实在需要在不同的命名空间下操作相同的表,就需要CacheRef设置让二者使用相同的缓存。
自定义拦截器
mybatis通过Interceptor接口向用户提供了拓展的机制。
其底层实现原理依旧是利用了JDK的动态代理。当我们通过Configuraion.newExecutor()时会将创建得到Executor在经过动态代理包装一层,以达到实现拦截方法执行的目的。
此处InvocationHandler的实现是Proxy对象,可以看其invoke()方法的实现。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//是否匹配拦截器的Signature
Set methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
//将连接点的信息(方法,参数,目标对象)封装成 Invocation对象,传入由Interceptor执行
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
mybatis与Spring的整合
在mybatis与Spring集成的过程中,以下几个组件承担了重要角色:
ClassPathMapperScanner:负责扫描相关Mapper对象,并作为BeanDefinition注册到容器中。 MapperFactoryBean:注册的BeanDefinition都是FactoryBean,当实例化Mapper时,会调用其getObject()方法,主要流程依旧是通过JDK动态代理创建Mapper实例,只不过这里关联的SqlSession是SqlSessionTemplate。 SqlSessionTemplate(核心): SqlSessionTemplate虽然实现了 SqlSession
接口,但其方法实现均是委托给一个SqlSession的动态代理,其InvocationHandler的实现是SqlSessionInterceptor
。它会在执行前先去获取真正的SqlSession,从而保证SqlSession在Spring环境中的线程安全。