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();中完成

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

  • 相关阅读:
    Html禁止粘贴 复制 剪切
    表单标签
    自构BeanHandler(用BeansUtils)
    spring配置中引入properties
    How Subcontracting Cockpit ME2ON creates SD delivery?
    cascadia code一款很好看的微软字体
    How condition value calculated in sap
    Code in SAP query
    SO Pricing not updated for partial billing items
    Javascript learning
  • 原文地址:https://www.cnblogs.com/ZhuChangwu/p/11741125.html
Copyright © 2011-2022 走看看