一文看懂 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的注册表,TypeHandlerRegistryTypeHandler的注册表,TypeAliasRegistryTypeAlias的注册表,另外还以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对象,因此ExcecutorSqlSession是一对一绑定的。

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类型
  getResult(ResultSet rs, String columnName) throws SQLException;
  getResult(ResultSet rs, int columnIndex) throws SQLException;
  
  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组成。
BoundSqlSqlSource应用了上下文环境(指用户输入参数)后得到的对象,对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的构造函数依赖三个参数,分别是ConfigurationExecutorautocommitConfiguration是全局的,而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环境中的线程安全。
mybatis与Spring的整合

浏览 58
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报