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; <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException; <E> Cursor<E> queryCursor(Statement statement) throws SQLException; BoundSql getBoundSql(); ParameterHandler getParameterHandler(); }
-
ResultSetHandler 结果集处理器,StatementHandler获取到结果集后
ResultSet
,会提交给ResultSetHandler
处理,以转换成Java对象集合。public interface ResultSetHandler { <E> List<E> handleResultSets(Statement stmt) throws SQLException; <E> Cursor<E> 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<Method> 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环境中的线程安全。