zoukankan      html  css  js  c++  java
  • 深入理解 MyBatis 启动流程

    环境简介与入口

    记录一下尝试阅读Mybatis源码的过程,这篇笔记是我一边读,一遍记录下来的,虽然内容也不多,对Mybatis整体的架构体系也没有摸的很清楚,起码也能把这个过程整理下来,这也是我比较喜欢的一种学习方式吧

    单独Mybatis框架搭建的环境,没有和其他框架整合

    入口点的源码如下:

    @Test
    public void test01() {
    
     try {
        this.resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 2. 创建SqlSessionFactory工厂
        this.factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        // 3. 创建sqlSession
        // todo 怎么理解这个sqlSession? 首先它是线程级别的,线程不安全, 其次它里面封装了大量的CRUD的方法
        this.sqlSession = factory.openSession();
    
        IUserDao mapper = this.sqlSession.getMapper(IUserDao.class);
        List<User> all = mapper.findAll();
        for (User user : all) {
            System.out.println(user);
        }
        // 事务性的操作,自动提交
        this.sqlSession.commit();
        // 6, 释放资源
        this.sqlSession.close();
        this.resourceAsStream.close();
      } catch (IOException e) {
       e.printStackTrace();
      }
    }
    
    

    构建SqlSessionFactory

    首先跟进这个,看看如何构建SqlSessionFactory对象

     this.factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    

    这个SqlSessionFactoryBuilder类的存在很简单,取名也叫他构建器,Mybatis的官网是这样解释它的,这个类可以被实例化(因为它有且仅有一个默认的无参构造),使用它的目的就是用来创建多个SqlSessionFactory实例,最好不要让他一直存在,进而保证所有用来解析xml的资源可以被释放

    所以跳过对这个构建器的关注,转而看的build()方法

    首先会来到这个方法,直接可以看到存在一个XML配置解析器,这其实并不意外,毕竟现在是MyBatis是孤军一人,就算我们使用的是注解开发模式,不也得存在主配置文件不是?

      public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        try {
          XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
          return build(parser.parse());
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error building SqlSession.", e);
        } finally {
          ErrorContext.instance().reset();
          try {
            inputStream.close();
          } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
          }
        }
      }
    

    接着看看parser.parse(),它里面会解析主配置文件中的信息,解析哪些信息呢? 源码如下: 很清楚的看到,涵盖mybatis配置文件中的所有的标签

     private void parseConfiguration(XNode root) {
        try {
          //issue #117 read properties first
          propertiesElement(root.evalNode("properties"));
          Properties settings = settingsAsProperties(root.evalNode("settings"));
          ...
          typeHandlerElement(root.evalNode("typeHandlers"));
          mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
      }
    

    第一个问题: 解析的配置信息存在哪里呢? 其实存放在一个叫Configuration的封装类中, 这个上面的解析器是XMLConfigBuilder 这个封装类的保存者是它的父类BaseBuilder

    继续跟进build()方法,源码如下:

     public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
      }
    

    小结: 至此也就完成了DefaultSqlSessionFactory()的构建,回想下,这个构建器真的太无私了,牺牲了自己,不仅仅创建了默认的SqlSessionFactory,还将配置文件的信息给了他

    打开SqlSession

    创建完成SqlSession工厂的创建, 我们继续跟进this.sqlSession = factory.openSession(); , 从工厂中获取一个SqlSession

    跟进几个空壳方法,我们很快就能来到下面的方法:

      private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
          final Environment environment = configuration.getEnvironment();
          final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
          tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
          final Executor executor = configuration.newExecutor(tx, execType);
          return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
          closeTransaction(tx); // may have fetched a connection so lets call close()
          throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    

    什么是SqlSession呢? 注释是这么说的,他是MyBatis在java中主要干活的接口,通过这个接口,你可以执行命令(它里面定义了大量的 诸如selectList类似的方法),获取mapper,合并事务

    The primary Java interface for working with MyBatis.
    Through this interface you can execute commands, get mappers and manage transactions.
    

    通过上面的我贴出来的函数,大家可以看到,通过事务工厂实例化了一个事物Transaction,那么问题来了,这个Transaction又是什么呢? 注释是这么解释的: Transaction 包装了一个数据库的连接,处理这个连接的生命周期,包含: 它的创建,准备 提交/回滚 和 关闭

    紧接着创建执行器:configuration.newExecutor(tx, execType),默认创建的是CachingExecutor它维护了一个SimpleExecutor, 这个执行器的特点是 在每次执行完成后都会关闭 statement 对象

    关于mybatis的执行器,其实挺多事的,打算专门写一篇笔记

    继续看 new DefaultSqlSession()我们得知,这个sqlSession的默认实现类是DefaultSqlSession

    第三个参数autocommit为false, 这也是为什么我们如果不手动提交事务时,虽然测试会通过,但是事务不会被持久化的原因

    小结: 当前函数是 openSession(), 如果说它是打开一个session,那跟没说是一样的,通过源码我们也看到了,这一步其实是Mybatis将 数据库连接,事务,执行器进行了一下封装然后返回给程序员

    获取Mapper -- maperProxy

    我们交给mybatis的mapper是一个接口,看看Mybatis是如何实例化我们的结果,返回给我们一个代理对象的

    跟进源码: IUserDao mapper = this.sqlSession.getMapper(IUserDao.class); ,经过一个空方法,我们进入Configuration类中的函数如下:

     public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
      }
    

    mapperRegistry中获取mapper,他是Configuration属性如下: 可以看到这个mapperRegistry甚至包含了Configuration,甚至还多了个 knownMappers

    那么问题来了,这个knownMappers是干啥呢? 我直接说,这个map就是在上面解析xml配置文件时,存放程序员在<mappers>标签下配置的<maper>

    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    
    // 详情:
    public class MapperRegistry {
        private final Configuration config;
        private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
    

    继续跟进这个getMapper()如下: 并且我们在配置文件中是这样配置的

    <mappers>
        <mapper class="com.changwu.dao.IUserDao"/>
    </mappers>
    
      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
          throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
          return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
          throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
      }
    

    所以不难想象,我们肯定能获取到结果,通过上面的代码我们能看到,获取到的对象被强转成了MapperProxyFactory类型,它的主要成员如下: 说白了,这个 map代理工厂是个辅助对象,它是对程序员提供的mapper结果的描述,同时内置使用jdk动态代理的逻辑为mapper创建代理对象

    public class MapperProxyFactory<T> {
    
      private final Class<T> mapperInterface;
      private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
      ...
        protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
        }
    

    说到了为mapper创建动态代理,就不得不去看看是哪个类充当了动态代理的需要的InvoketionHandler -- 这个类是mybatis中的MapperProxy, 没错它实现了InvocationHandler接口,重写了invoke()逻辑,源码如下:

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
    }
    

    小结: 至此当前模块的获取mapper对象已经完结了,我们明明白白的看到了MyBatis为我们的mapper使用jdk的动态代理创建出来代理对象, 这也是为什么我们免去了自己写实现类的粗活

    执行Map -- maperProxy

    上一个模块我们知道了Mybatis为我们创建出来了mapper接口的代理对象,那当我们获取到这个代理对象之后执行它的mapper.findAll();实际上触发的是代理对象的invoke()方法

    所以说,接着看上面的MapperProxyinvoke()方法:

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
    }
    

    上面的方法我们关注两个地方,第一个地方就是final MapperMethod mapperMethod = cachedMapperMethod(method);,见名知意: 缓存MapperMethod

    第一个问题: 这个MapperMethod是什么? 它其实是Mybatis为sql命令+方法全限定名设计的封装类

     */
    public class MapperMethod {
    
      private final SqlCommand command;
      private final MethodSignature method;
    

    说白了,就是想为MapperProxy保存一份map格式的信息,key=方法全名 value=MapperMethod(command,method),存放在MapperProxy的属性methodCache中

    comand -> "name=com.changwu.dao.IUserDao,fingAll"

    method -> "result= public abstract java.util.List.com.changwu.dao.IUserDao.findAll()"

    为什么要给这个MapperProxy保存这里呢? 很简单,它是mapper的代理对象啊,程序员使用的就是他,在这里留一份method的副本,再用的话多方便?

    完成缓存后,继续看它如何执行方法:,跟进 mapperMethod.execute(sqlSession, args)

    因为我使用的是@Select("select * from user"),所以一定进入下面的 result = executeForMany(sqlSession, args);

    
     public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        switch (command.getType()) {
          case INSERT: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.insert(command.getName(), param));
            break;
          }
          
          ..
            case SELECT:
            if (method.returnsVoid() && method.hasResultHandler()) {
              executeWithResultHandler(sqlSession, args);
              result = null;
            } else if (method.returnsMany()) {
              result = executeForMany(sqlSession, args);
            ...
    }
    

    所以我们继续关注这个 executeForMany(sqlSession, args);方法,看他的第一个参数是sqlSession,也就是我们的DefaultSqlSession,他里面存在两大重要对象: 1是configuration 配置对象, 2是Executor 执行器对象

    继续跟进:

      private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
        List<E> result;  
        Object param = method.convertArgsToSqlCommandParam(args);
        if (method.hasRowBounds()) {
          RowBounds rowBounds = method.extractRowBounds(args);
          result = sqlSession.selectList(command.getName(), param, rowBounds);
        } else {
          result = sqlSession.selectList(command.getName(), param);
        }
    

    我们在继续跟进这个selectList方法之前,先看看这个command.getName()是啥? 其实我们上面的代码追踪中有记录: 就是name=com.changwu.dao.IUserDao,fingAll

    继续跟进去到下面的方法:

    这个方法就比较有趣了,首先来说,下面的入参都是什么

    statement = "com.changwu.dao.IUserDao.findAll" parameter=null

    第二个问题:MappedStatement是什么? 它是一个对象,维护了很多很多的配置信息,但是我们关心它里面的两条信息,这其实可以理解成一种方法与sql之间的映射,如下图

    mappedStatement

      @Override
      public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
          MappedStatement ms = configuration.getMappedStatement(statement);
          return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    

    前面阅读时,我们知道Mybatis为我们创建的默认的执行器 Executor是CachingExecutor,如下图

    CachingExecutor

    继续跟进,主要做了下面三件事, 获取到绑定的sql,然后调用SimpleExecutor缓存key,然后继续执行query()方法

     @Override
      public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
      }
    

    接着调用SimpleExecutorquery()方法,然后我们关注SimpleExecutordoQuery()方法,源码如下

     @Override
      public <E> List<E> 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);
        }
      }
    

    我们注意到.在SimpleExcutor中创建的了一个 XXXStatementHandler这样一个处理器, 所以我们的只管感觉就是,sql真正执行者其实并不是Executor,而是Executor会为每一条sql的执行重新new 一个 StatementHandler ,由这个handler去具体的执行sql

    关于这个StatementHandler到底是是何方神圣? 暂时了解它是Mybatis定义的一个规范接口,定义了如下功能即可

    public interface StatementHandler {
      // sql预编译, 构建statement对象 
      Statement prepare(Connection connection, Integer transactionTimeout)
          throws SQLException;
      // 对prepare方法构建的预编译的sql进行参数的设置
      void parameterize(Statement statement)
          throws SQLException;
      // 批量处理器
      void batch(Statement statement)
          throws SQLException;
      // create update delete
      int update(Statement statement)
          throws SQLException;
      // select
      <E> List<E> query(Statement statement, ResultHandler resultHandler)
          throws SQLException;
      
      <E> Cursor<E> queryCursor(Statement statement)
          throws SQLException;
      // 获取sql的封装对象 
      BoundSql getBoundSql();
      // 获取参数处理对象
      ParameterHandler getParameterHandler();
    
    }
    
    

    了解了这个StatementHandler是什么,下一个问题就是当前我们创建的默认的statement是谁呢? routingStatementHandler如下图

    https://img2018.cnblogs.com/blog/1496926/201910/1496926-20191025220026439-543525632.png

    创建preparedStatement

    马上马就发生了一件悄无声息的大事!!!根据现有的sql等信息,构建 PreparedStatement,我们关注这个 prepareStatement(handler, ms.getStatementLog());方法,通过调试我们得知,prepareStatement()RoutingStatementHandler的抽象方法,被PreparedStatementHandler重写了,所以我们去看它如何重写的,如下:

    @Override
    public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      statement = instantiateStatement(connection);
    

    我们关注这个instantiateStatement()方法, 并且进入它的connection.prepareStatement(sql);方法,如下图:

    preparedStatement

    纯洁的微笑... 见到了原生JDK, jdbc的亲人...

    创建完事这个 preparedStatement,下一步总该执行了吧...绕这么多圈...

    我们回到上面代码中的return handler.query(stmt, resultHandler);准备执行,此时第一个参数就是我们的刚创建出来的PreparedStatement, 回想一下,上面创建的这个默认的statement中的代表是PreparedStatementHandler,所以,我们进入到这个StatementHandler的实现类RountingStatementHandler中,看他的query()方法

      @Override
      public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        return delegate.query(statement, resultHandler);
      }
    

    调用RountingStatementHandler中维护的代表的StatementHandler也就是PreparedStatementHandlerquery()方法,顺势跟进去,最终会通过反射执行jdbc操作,如图, 我圈出来的对象就是我们上面创建出来的preparedStatement

    执行preparedStatement

    提交事务

    跟进conmit()方法,分成两步

    • 清空缓存
    • 提交事务

    清空缓存是在CachingExecutor中调用了SimpleExecutor简单执行器的方法commit(required)

      @Override
      public void commit(boolean required) throws SQLException {
        delegate.commit(required);
        tcm.commit();
      }
    

    接在SimpleExecutor的父类BaseExecutor中完成

      @Override
      public void commit(boolean required) throws SQLException {
        if (closed) {
          throw new ExecutorException("Cannot commit, transaction is already closed");
        }
        clearLocalCache();
        flushStatements();
        if (required) {
          transaction.commit();
        }
      }
    

    提交事务的操作在 tcm.commit();中完成

    本文到这里也就行将结束了,如果您觉得挺好玩的,欢迎点赞支持,有错误的话,也欢迎批评指出

  • 相关阅读:
    djano框架根据小牛深入研究
    python raise 是啥东西
    python调request报错
    python当前时间,时间偏移
    写好了,定时任务,怎么让定时任务,去在服务器上跑?
    python实现定时任务-目的解决自动化造数据
    django-celery
    Fruits【水果】
    The Extinction of Some Languages【一些语言的消失】
    Dawson City【道森市】
  • 原文地址:https://www.cnblogs.com/ZhuChangwu/p/11741125.html
Copyright © 2011-2022 走看看