首页 > 其他分享 >MyBatis核心流程

MyBatis核心流程

时间:2023-10-24 17:03:01浏览次数:28  
标签:缓存 resource 核心 流程 ms MyBatis return null 加载

核心流程

核心流程搞懂:主线,涉及的模块不深究。再去基础支持层,再回来核心。

    /**
     * MyBatis API 的使用
     * MyBatis 在启动的时候会做哪些操作?
     *   1.加载全局配置文件
     *   2.加载映射文件
     *   3.加载的内容存储在了那个Java对象中? Configuration
     * @throws Exception
     */
    @Test
    public void test1() throws  Exception{
        // 1.获取配置文件
        InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
        // 2.加载解析配置文件并获取SqlSessionFactory对象
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
        // 3.根据SqlSessionFactory对象获取SqlSession对象
        SqlSession sqlSession = factory.openSession();
        // 4.通过SqlSession中提供的 API方法来操作数据库
         List<User> list = sqlSession.selectList("mapper.UserMapper.selectUserList");
        for (User item: list
             ) {
            System.out.println(item.toString());
        }
        // 5.关闭会话
        sqlSession.close(); // 关闭session  清空一级缓存
    }

mybatis在启动的时候做什么操作?

  1. 加载全局配置文件
  2. 加载映射文件
  3. 加载的内容存在什么java对象中

下面的题目分别对应着五步来进行

获取配置文件

// 1.使用的时候获取配置文件
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");

这一步调用了io包下的Resources类的静态方法getResourceAsStream,里面调用了一个重载的getResourceAsStream方法,两个参数是类加载器和路径,其中类加载器为空。

  /**
   * io.Resources
   * Returns a resource on the classpath as a Stream object
   *
   * @param loader   The classloader used to fetch the resource
   * @param resource The resource to find
   * @return The resource
   * @throws java.io.IOException If the resource cannot be found or read
   */
  public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
    InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
    if (in == null) {
      throw new IOException("Could not find resource " + resource);
    }
    return in;
  }

而后还是重载嵌套,其中的getClassLoaders加载了五种加载器,封装到一个类加载器数组中

  public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
    return getResourceAsStream(resource, getClassLoaders(classLoader));
  }
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
  return new ClassLoader[]{
      classLoader,
      defaultClassLoader,
      Thread.currentThread().getContextClassLoader(),
      getClass().getClassLoader(),
      systemClassLoader};
}

而后通过classLoaderWrapper中的getResourceAsStream生成字节输入流。

/**
 * io.ClassLoaderWrapper
 * Try to get a resource from a group of classloaders
 *
 * @param resource    - the resource to get
 * @param classLoader - the classloaders to examine
 * @return the resource or null
 */
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
  for (ClassLoader cl : classLoader) {
    if (null != cl) { // 不为空就加载

      // try to find the resource as passed
      InputStream returnValue = cl.getResourceAsStream(resource);

      // now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resource
      if (null == returnValue) {
        returnValue = cl.getResourceAsStream("/" + resource);
      }

      if (null != returnValue) {
        return returnValue;   // 猜测是变量放到里面节省内存空间?
      }
    }
  }
  return null;
}

这样就拿到了字节流。

加载解析配置文件

    // 2.加载解析配置文件并获取SqlSessionFactory对象
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);

首先是SqlSessionFactory,可以看到他是一个接口,里面有若干的方法,比如selectOne等。而他的实现有两个,分别是 DefaultSqlSessionFactory SqlSessionManager 。而我们没有直接通过DefaultSqlSessionFactory直接获取,而是通过Builder(建造者)模式进行获取。

为什么通过建造者获取呢?
1. SqlSessionFactory的目的是生产SqlSession的,当我们系统启动之后我们要获取工厂对象的时候,SqlSessionFactory工厂对象应该是单例。
2. 全局配置文件和映射文件也只需用在系统启动的时候完成加载操作。
由于1、2点,我们可以直接在工厂中直接完成配置文件的加载解析和工厂的创建。
但是这样会导致我们的职责不单一,所以用建造者模式来进行拆分。构建出这个复杂的对象。

SqlSessionFactoryBuilder的build方法重载了build方法,变成三个变量InputStream,String类型的环境,Properties三个变量,后两个为空。具体的解析给到了XMLConfigBuilder里。

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 用于解析 mybatis-config.xml,同时创建了 Configuration 对象 >>
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // 解析XML,最终返回一个 DefaultSqlSessionFactory >>
      // 全局配置文件中的信息都被封装到了Configuration对象中
      // 映射文件中的配置信息同样的也被封装到了Configuration对象中
      // 一个具体的CRUD标签的信息被封装到MappedStatment对象中
      return build(parser.parse());// 调用的下面的build
    } 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.
      }
    }
  }
  // 这里暴露了一个事实,信息是放到Configuration中的。
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

然后通过XMLConfigBuilder把字节流转为了XMLConfigBuilder对象(暂时不看),只需要知道他继承自BaseBuilder,在里面新建了一个Configuration实例,Configuration构造函数对配置进行了初始化,而后调用其中的parse方法,进行加载解析。保证只加载一遍,而后解析。

  public Configuration parse() {
    // 保证加载解析只能做一次
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // XPathParser,dom 和 SAX 都有用到 >>
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

再往后就是parseConfiguration,里面的参数XNode就是对应着我们的mapper-config.xml的内容,在里面的dtd文件中可以看到他们规定规定内容。

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      // 对于全局配置文件各种标签的解析
      propertiesElement(root.evalNode("properties"));
      // 解析 settings 标签
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      // 读取文件
      loadCustomVfs(settings);
      // 日志设置
      loadCustomLogImpl(settings);
      // 类型别名
      typeAliasesElement(root.evalNode("typeAliases"));
      // 插件
      pluginElement(root.evalNode("plugins"));
      // 用于创建对象
      objectFactoryElement(root.evalNode("objectFactory"));
      // 用于对对象进行加工
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      // 反射工具箱
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      // settings 子标签赋值,默认值就是在这里提供的 >>
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      // 创建了数据源 >>
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析引用的Mapper映射器
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

以propertiesElement举例,可以看到他的作用是把提取到的信息放到Configuration中的variables中去。

  private void propertiesElement(XNode context) throws Exception {
    if (context != null) {  // 意味着这个属性可以没有
      // 创建了一个 Properties 对象,后面可以用到
      Properties defaults = context.getChildrenAsProperties();
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");
//      System.out.println(resource+": adsf");
//      System.out.println(url +": adsf");
      if (resource != null && url != null) {
        // url 和 resource 不能同时存在
        throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
      }
      // 加载resource或者url属性中指定的 properties 文件
      if (resource != null) {
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        defaults.putAll(Resources.getUrlAsProperties(url));
      }
      Properties vars = configuration.getVariables();
      if (vars != null) {
        // 和 Configuration中的 variables 属性合并
        defaults.putAll(vars);
      }

//      for (Map.Entry item: defaults.entrySet()
//           ) {
//        System.out.println(item.getKey()+":"+item.getValue());
//      }
      // 更新对应的属性信息
      parser.setVariables(defaults);
      configuration.setVariables(defaults);
    }
  }

这样我们就知道了我们解析的配置文件都存到什么地方(Configuration),怎么存的(Mapper)了。

打开session

    // 3.根据SqlSessionFactory对象获取SqlSession对象
    SqlSession sqlSession = factory.openSession();

SqlSessionFactory是一个接口,他的实现类是DefaultSqlSessionFactory。里面实现openSession的方法如下:

  @Override
  public SqlSession openSession() {  // 执行器才是真和数据库操作,干活的getDefaultExecutorType
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

openSessionFromDataSource代码如下,通过事务控制,创建了一个执行器和一个DefaultSqlSession类的实例。

 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();
    }
  }

执行器executor才是真正的完成数据库的操作。点进去newExecutor,里面负责配置执行器的类型,和二级缓存还有插件。

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
  } else {
    // 默认 SimpleExecutor
    executor = new SimpleExecutor(this, transaction);
  }
  // 二级缓存开关,settings 中的 cacheEnabled 默认是 true
  // 映射文件中的<cache>标签只是创建了Cache对象,
  // cacheEnabled才会真正对Excutor缓存的增强。
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  // 植入插件的逻辑,至此,四大对象已经全部拦截完毕
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

执行sqlSession中的方法

// 4.通过SqlSession中提供的 API方法来操作数据库
List<User> list = sqlSession.selectList("mapper.UserMapper.selectUserList");

这里指定了全路径,然后就属于sqlSession的方法就执行完成了,程序把具体的执行过程交给了执行器executor进行。

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      // statement就是我们的全路径名?
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 如果 cacheEnabled = true(默认),Executor会被 CachingExecutor装饰
      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();
    }
  }

执行器是一个接口有两个实现方法,其中第一个是BaseExecutor,另一个是CachingExecutor,先看CachingExecutor,因为整体流程是有cache就用cache,没有就是base。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  // 获取SQL
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  // 创建CacheKey:什么样的SQL是同一条SQL? >>
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

query具体的执行是看到有缓存就用二级缓存,如果有二级缓存,就直接把缓存中的数据加载进来。如果没有的话,就执行BaseExecutor中的query,也就是查询数据库。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  Cache cache = ms.getCache();
  // cache 对象是在哪里创建的?  XMLMapperBuilder类 xmlconfigurationElement()
  // 由 <cache> 标签决定
  if (cache != null) {
    // flushCache="true" 清空一级二级缓存 >>
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      // 获取二级缓存
      // 缓存通过 TransactionalCacheManager、TransactionalCache 管理
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        // 写入二级缓存
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 走到 SimpleExecutor | ReuseExecutor | BatchExecutor
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

一级缓存查询的逻辑:先查询一级缓存,如果一级缓存中没有结果(list为空)就进行真正的数据库查询

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  // 异常体系之 ErrorContext
  ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    // flushCache="true"时,即使是查询,也清空一级缓存
    clearLocalCache();
  }
  List<E> list;
  try {
    // 防止递归查询重复处理缓存  queryStack代表深度
    queryStack++;
    // 查询一级缓存
    // ResultHandler 和 ResultSetHandler的区别 localCache.getObject(key)是一级缓存中存在的数据
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      // 一级缓存也没有的话,真正的查询流程
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    queryStack--;
  }
  if (queryStack == 0) {
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    // issue #601
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}
为什么先二级缓存再一级缓存而不是先一级缓存?
二级缓存是进程级别的,一级缓存是会话级别的(session),生命周期长短是不一样的,二级缓存只要服务没重启就在,一级缓存一次会话结束就无了。二级缓存命中的概率是99%,一级缓存命中可能只有1%概率。

标签:缓存,resource,核心,流程,ms,MyBatis,return,null,加载
From: https://www.cnblogs.com/ylw-blogs/p/17785219.html

相关文章

  • GO语言的流程控制语句
    ifif5>9{fmt.Println("5>9")}如果逻辑表达式成立,就会执行{}里的内容。逻辑表达式不需要加()。{必须紧跟在逻辑表达式后面,不能另起一行。ifc,d,e:=5,9,2;c<d&&(c>e||c>3){//初始化多个局部变量。复杂的逻辑表达式fmt.Println("fit")}......
  • Redis深度历险 核心原理与应用实践-笔记
     1.2.25种基础数据结构string(字符串)字符串string是Redis最简单的数据结构,其内部表示就是一个字符数组。Redis所有的数据结构都是以唯一的key字符串作为名称,然后通过这唯一的key来获取相应的value数据。不同类型的数据结构差异就在于value的结果不一样。Redis的字符串是动......
  • 云原生架构实战03 核心实战
    1、资源创建方式命令行YAML2.Namespace名称空间隔离资源kubectlcreatenshellokubectldeletenshelloapiVersion:v1kind:Namespacemetadata:name:hellokubectlgetnskubectlgetpods-Akubectldeletensmy-istio-ns3、Pod运行中的一组容器,Pod是kubernetes中应用......
  • 企业ERP系统流程图
    ERP系统是什么:ERP系统是一个以管理会计为核心的信息系统,识别和规划企业资源,从而获取客户订单,完成加工和交付,最后得到客户付款。ERP管理软件将企业内部所有资源整合在一起,对采购、生产、成本、库存、分销、运输、财务、人力资源进行规划,从而达到最佳资源组合,取得最佳效益。企业处......
  • 实体类使用临时字段 myBatis jpa Hibernate
    Mybatis-Plus  使用数据库不存在的字段,可在实体类的属性加上@TableField注解** @TableField(exist=false)**jpaHibernate** @Transient**......
  • mybatis的一级缓存和事务注解失效导致的查询结果缺失
    事情是这样的,测试发现有个查询接口,第一次调的时候没能返回数据,第二次调就可以正常返回。这个接口的功能是查询用户的现有福利数据。具体点的逻辑是1,查询数据库,mybatis,xml里面写的关联查询,主表和子表关联。2,判断查询结果,如果没有子表部分的信息,则按照业务逻辑生成子表数......
  • 自定义MyBatis拦截器更改表名
    byemanjusakafrom​https://www.emanjusaka.top/archives/10彼岸花开可奈何本文欢迎分享与聚合,全文转载请留下原文地址。自定义MyBatis拦截器可以在方法执行前后插入自己的逻辑,这非常有利于扩展和定制MyBatis的功能。本篇文章实现自定义一个拦截器去改变要插入或者查询......
  • BLE低功耗蓝牙数据包结构以及BLE流程分析
    来源: https://mp.weixin.qq.com/s/5z6KmAY_n8X8hED4eC3M-g 摘要本文没有按部就班分析蓝牙协议,而是采用循序渐进的方式,力争通过BLEPDU来分析BLE协议和BLE流程,以便在嵌入式开发和移动应用开发中,能熟悉BLE协议以及够理解这些平台中的high-level的API,特别是当想进一步深入了......
  • MyBatis-Plus和shardingsphere一起用。子查询取别名读取不到的问题。
    https://github.com/baomidou/mybatis-plus/issues/2585在使用MP和Shardingsphere的某些版本中,可能会出现join子查询表取别名之后,在where中用这个别名报错 Cannotfindownerfromtable.//重点是外层SQL不要出现*,不要使用别名,需要的字段都写清楚(内外层sql都要写清楚),......
  • 架构师论文各主题核心要点(必背)
    论基于构件的软件开发方法问题:各种构件技术的优点、缺点,展望构件技术的发展趋势。回答:构件技术是指通过组装一系列可复用的软件构件来构造软件系统的软件技术。通过运用软件技术,开发人员可以有效地进行软件复用,减少重复开发,缩短开发时间,降低软件的开发成本。主流的软件架构有三种......