走进 Mybatis 内核世界:理解原理,释放更多生产力

        

目录

一、MyBatis 特点

二、 接口绑定实现原理

三、SpringBoot 加载 MyBatis 源码分析

四、MyBatis 执行性

五、MyBatis 分页原理

       5.1  逻辑分页(内存分页)

        5.2 物理分页

六、MyBatis 缓存

        6.1 一级缓存

        6.2 二级缓存


        

        MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

        由于没有屏蔽 sql,这对于追求高响应和性能的互联网系统十分重要,我们可以尽可能的通过sql去优化性能。

        本文介绍的前提是你已经有一定基础的 MyBatis 使用经验,关于如何使用就不过多介绍了。

一、MyBatis 特点

        动态映射,MyBatis 使用 xml 或注解来描述 SQL 语句,同时定义如何将查询结果映射到Java 对象。

        动态SQL,支持动态构建 SQL 语句,可以根据传入的条件参数在运行时生成不同的 SQL 语句。动态 SQL 语句标签包括:

  • <if>: 根据条件判断是否包含某段 SQL 子句。如果条件为真,则包含并执行内部的 SQL 片段。
  • <choose>、<when>、<otherwise> : 类似于 Java 中的 switch-case 语句,根据多个条件选择性地包含一个或多个 SQL 片段。
  • <where>: 用于动态地拼接 WHERE 条件子句,当且仅当有至少一个条件满足时才会包含WHERE 关键字和其后的条件。
  • <set>: 在 UPDATE 语句中动态设置更新列的值,根据传入参数决定哪些字段需要更新,并自动添加 SET 关键字以及逗号分隔符。
  • <foreach>: 遍历集合对象(如List、数组等),可以用来动态构建 in 查询或者批量插入、更新操作中的值列表。
  • <trim>、<trim prefix="" prefixOverrides=""> : 剔除 SQL 片段开头或结尾指定字符(比如空格或特定字符)以及在内容中移除某些前缀字符,以确保SQL语法正确。
  • <bind>: 绑定变量到OGNL表达式,可以在SQL语句中引用这个绑定变量。
  • <sql>: 定义可重用的SQL片段,可以被其他动态标签引用,提高代码复用率。

        事务管理:可以通过配置实现自动或者手动事务管理。

        接口绑定:开发者可以自定义 DAO 接口,MyBatis 会根据方法和映射文件中的定义进行代理对象的创建,实现 SQ L执行与 Java 方法调用之间的映射。

        那接口绑定时如何实现的?

二、 接口绑定实现原理

        MyBatis 接口绑定(Mapper Interface Binding)是通过动态代理技术(动态代理请参考往期文章:一文掌握Java动态代理的奥秘与应用场景-CSDN博客)实现的。在 MyBatis 中,开发者通常会定义一个接口来表示数据访问层的操作,如查询、插入、更新和删除等,然后在 XML 映射文件中配置对应的 SQL 语句以及结果映射规则。

        当应用启动时,MyBatis 读取配置文件,并创建 SqlSessionFactory 实例。当从 SqlSessionFactory 获取 SqlSession 时,MyBatis 根据之前注册的 Mapper 接口信息,利用 Java 动态代理机制为每个 Mapper 接口生成一个代理对象。

        具体步骤如下:

        1. 接口扫描与注册

        MyBatis 通过 mapperLocations 配置属性指定 XML 映射文件的路径,解析这些文件并从中获取 Mapper 接口类名。然后,MyBatis 将这些接口注册到 Configuration 对象中的MapperRegistery 中,具体注册通过 @MapperScan 实现,详细介绍见下文。

        2. 动态代理生成

        当调用了SqlSession 的 getMapper 的方法获取某个 Mapper 接口实例时,MyBatis 根据MapperRegistery 中的信息生成一个实现了该接口的代理对象,

        这个代理对象内部持有一个 Executor 执行器,所有方法调用最终都会委托给这个执行器处理。

        3. SQL 执行与结果映射

        当调用 Mapper 接口的代理对象的方法时,实际上是调用了对象内部的方法处理器。方法处理器根据方法签名和XML映射文件中的定义,找到对应的 SQL 语句,准备参数,执行 SQL 查询或命令操作。

        执行完毕后,方法处理器根据结果映射规则将数据库返回的结果集转换成 Java 对象,并返回给客户端。

        MyBatis 通过动态代理技术巧妙地将业务逻辑层的接口方法调用转换成了对数据库的 CRUD 操作,大大简化了 DAO 层的开发工作量,并增强了代码的可读性和可维护性。

三、SpringBoot 加载 MyBatis 源码分析

         首先在项目中添加了MyBatis的Maven依赖,如下

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>version</version> 
</dependency>

        在 MyBatis starter 包中的 spring.factories 中有自动配置类

        

        MybatisAutoConfigration 中的代码中会去创建 SqlSessionFactory,但是有个需要注意注解 @ConditionalOnMissingBean 注解,如果用户没有自定义该 bean 的情况下才会去创建,通常情况下,我们在引入 Mybatis 时是会自定义 SqlSessionFactory 的。 

// MybatisAutoConfigration 类中定义获取SqlSessionFactory的方法
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
  SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
  factory.setDataSource(dataSource);
  factory.setVfs(SpringBootVFS.class);
  if (StringUtils.hasText(this.properties.getConfigLocation())) {
    factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
  }
  applyConfiguration(factory);
  if (this.properties.getConfigurationProperties() != null) {
    factory.setConfigurationProperties(this.properties.getConfigurationProperties());
  }
  if (!ObjectUtils.isEmpty(this.interceptors)) {
    factory.setPlugins(this.interceptors);
  }
  if (this.databaseIdProvider != null) {
    factory.setDatabaseIdProvider(this.databaseIdProvider);
  }
  if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
    factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
  }
  if (this.properties.getTypeAliasesSuperType() != null) {
    factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
  }
  if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
    factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
  }
  if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
    factory.setMapperLocations(this.properties.resolveMapperLocations());
  }

  return factory.getObject();
}

        通常我们自定义获取 SqlSessionFactory 如下

@Configuration
// 注意 MapperScan 注解
@MapperScan(basePackages = "cn.example.mapper",sqlSessionTemplateRef = "sqlSessionTemplate")
public class MybatisConfig {


    @Resource
    DataSource dataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        // 设置mapper.xml文件所在位置
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:Mapper/**.xml"));
        bean.setVfs(SpringBootVFS.class);
        bean.setPlugins(new Interceptor[]{myBatisSqlInterceptor()});
        // 实体类位置
        bean.setTypeAliasesPackage("com.example.entity");

        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        // 自动将数据库下划线转换为驼峰格式
        configuration.setMapUnderscoreToCamelCase(Boolean.TRUE);
        bean.setConfiguration(configuration);
        return bean.getObject();
    }
}

        当服务启动时,就会去创建 SqlSessionFactory 实例,注意 SqlSessionFactoryBean 中实现了 FactoryBean,所以最后会调用 getObject() 方法,继续进入方法内部。

@Override
public SqlSessionFactory getObject() throws Exception {
  if (this.sqlSessionFactory == null) {
    afterPropertiesSet();
  }

  return this.sqlSessionFactory;
}

// 继续进入 afterPropertiesSet() 方法
@Override
public void afterPropertiesSet() throws Exception {
  notNull(dataSource, "Property 'dataSource' is required");
  notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
  state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
            "Property 'configuration' and 'configLocation' can not specified with together");

  this.sqlSessionFactory = buildSqlSessionFactory();
}

// 然后就会继续调用 buildSqlSesionFactory() 方法
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

  final Configuration targetConfiguration;

  XMLConfigBuilder xmlConfigBuilder = null;
  if (this.configuration != null) {
    targetConfiguration = this.configuration;
    if (targetConfiguration.getVariables() == null) {
      targetConfiguration.setVariables(this.configurationProperties);
    } else if (this.configurationProperties != null) {
      targetConfiguration.getVariables().putAll(this.configurationProperties);
    }
  } else if (this.configLocation != null) {
    xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
    targetConfiguration = xmlConfigBuilder.getConfiguration();
  } else {
    LOGGER.debug(() -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
    targetConfiguration = new Configuration();
    Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
  }

  Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
  Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
  Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);

  if (hasLength(this.typeAliasesPackage)) {
    scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType)
        .forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
  }

  if (!isEmpty(this.typeAliases)) {
    Stream.of(this.typeAliases).forEach(typeAlias -> {
      targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
      LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
    });
  }

  if (!isEmpty(this.plugins)) {
    Stream.of(this.plugins).forEach(plugin -> {
      targetConfiguration.addInterceptor(plugin);
      LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
    });
  }

  if (hasLength(this.typeHandlersPackage)) {
    scanClasses(this.typeHandlersPackage, TypeHandler.class).stream()
        .filter(clazz -> !clazz.isInterface())
        .filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
        .filter(clazz -> ClassUtils.getConstructorIfAvailable(clazz) != null)
        .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
  }

  if (!isEmpty(this.typeHandlers)) {
    Stream.of(this.typeHandlers).forEach(typeHandler -> {
      targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
      LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
    });
  }

  if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls
    try {
      targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
    } catch (SQLException e) {
      throw new NestedIOException("Failed getting a databaseId", e);
    }
  }

  Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);

  if (xmlConfigBuilder != null) {
    try {
      // 这个方法会去解析 XML配置文件中的各个标签,进入这里的前提是自己配置了mybatis的配置,这里我们设置mapper.xml文件所在位置
      //  bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:Mapper/**.xml"));
      // 所以不会通过这个地方来解析,解析是根据配置是有优先级的
      xmlConfigBuilder.parse();
      LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'");
    } catch (Exception ex) {
      throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  targetConfiguration.setEnvironment(new Environment(this.environment,
      this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
      this.dataSource));
      
  // 这里的mapperLocations肯定不是空,因为上边设置了
  if (this.mapperLocations != null) {
    if (this.mapperLocations.length == 0) {
      LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
    } else {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }
        try {
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
          // 会在这里循环解析每个 mapper 配置文件    
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }
        LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
      }
    }
  } else {
    LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
  }

  return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}

        XMLConfigBuilder进行XML中标签解析

// 执行这个方法的话说明是通过自己配置了mybatis配置文件
private void parseConfiguration(XNode root) {
  try {
    //issue #117 read properties first
    propertiesElement(root.evalNode("properties"));
    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"));
    settingsElement(settings);
    // read it after objectFactory and objectWrapperFactory issue #631
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    // 解析mappers文件,解析时会有优先级
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

// 进入这个方法是通过指定了 mapper 文件的位置
public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }
  
  // 解析结果映射 ResultMap
  parsePendingResultMaps();
  // 解析缓存应用,包括一级缓存和二级缓存
  parsePendingCacheRefs();
  // 解析配置文件或注解(@Select等)中定义的SQL映射
  parsePendingStatements();
}

        @MapperScan 中有注解 @Import(MapperScannerRegistrar.class),进入MapperScannerRegistrar中,会去扫描你指定的mapper接口,这这个方法内部,MyBatis 就将接口交给了 Spring 来管理。

四、MyBatis 执行器

        我们可以指定 MyBatis 的执行器,不指定的话会有默认值,MyBatis 有三种执行器模式,分别是 SIMPLE(默认), REUSE, BATCH 。

        这三种模式分别对用着三种执行器,SimpleExecutor、BatchExecutor、ReuseExecutor。 

       SimpleExecutor 是每次都会关闭 statement,意味着下一次使用需要重新开启statement。ReuseExecutor 不会关闭 statement,而是把 statement 放到缓存中。缓存的 key 为 sql 语句,value 即为对应的 statement。也就是说不会每一次调用都去创建一个 Statement 对象,而是会重复利用以前创建好的(如果SQL相同的话),这也就是在很多数据连接池库中常见的 PSCache 概念 。

        在 BatchExecutor 中的 doupdate 并不会向前面两者那样执行返回行数,而是每次执行将statement 预存到有序集合,官方说明这个 executor 是用于执行存储过程的和批量操作的,因此这个方法是循环或者多次执行构建一个存储过程或批处理过程。

  • SimpleExecutor:是一种常规的执行器,每次执行都会创建一个statement,用完后关闭
  • ReuseExecutor:是可重用执行器,将statement存入map中,操作map中的statement而不会重新创建。
  • BatchExecutor:批处理执行器,doUpdate预处理存储过程或批处理操作,doQuery提交并执行过程

        SimpleExecutor 比 ReuseExecutor 的性能要差 , 因为 SimpleExecutor 没有做 PSCache。为什么做了 PSCache 性能就会高呢 , 因为当 SQL 越复杂占位符越多的时候预编译的时间也就越长,创建一个 PreparedStatement 对象的时间也就越长。BatchExecutor 是没有做 PSCache,BatchExecutor 与 SimpleExecutor 和 ReuseExecutor 还有一个区别就是 , BatchExecutor 的事务是没法自动提交的。因为 BatchExecutor 只有在调用了 SqlSession 的 commit 方法的时候,它才会去执行 executeBatch 方法。

        不过通常情况下,我们不用设置选择执行器,默认的就足够了。

五、MyBatis 分页原理

       5.1  逻辑分页(内存分页)

        MyBatis 提供了 RowBounds 对象来进行内存分页。在使用 RowBounds 时,MyBatis 会一次性从数据库加载所有满足条件的数据,然后在内存中根据 RowBounds 设置的偏移量(offset)和限制条数(limit)进行切片,只返回所需的那部分数据。这种方式在数据量较小的情况下是可行的,但当数据量非常大时,由于一次性加载所有数据到内存,可能会造成较大的内存压力和性能问题。

        5.2 物理分页

        物理分页则是指在发送给数据库的 SQL 查询语句中就包含分页相关的参数,使得数据库在执行查询时只返回所需的那部分数据。这是更推荐的方式,因为它减少了不必要的数据传输和内存消耗。

        MyBatis 插件实现分页:通过 MyBatis 插件机制,可以编写自定义的分页拦截器(。拦截器在执行 SQL 之前根据分页参数动态修改 SQL 语句,添加相应的数据库分页语法(如MySQL的LIMIT子句、Oracle的ROWNUM伪列和RANGE子查询等)。这样,数据库只会返回指定范围内的记录,提高了查询性能。

        对于大规模数据处理和性能优化,物理分页通常是更好的选择。MyBatis-Plus 就是对 MyBatis 进行增强的一个插件,它内置了易于使用的分页功能,开发者只需要传入分页参数,即可自动处理分页逻辑,降低了开发成本并提高了查询性能。

六、MyBatis 缓存

        缓存分为一级缓存和二级缓存,是用来提高查询效率、减少数据库访问次数的缓存机制。

        6.1 一级缓存

        MyBatis 的一级缓存是基于 SqlSession 级别的缓存,也就是本地缓存。在同一 SqlSession 生命周期内,执行相同的查询 SQL 时,MyBatis 会首先检查一级缓存中是否已经存在这个查询的结果。

        每个 SqlSession 中持有了 Executor,每个 Executor 中有一个 LocalCache。

        一级缓存的生命周期与 SqlSession 绑定,即从 SqlSession 创建开始,直到 SqlSession 关闭为止。一旦 SqlSession 关闭,一级缓存也随之清空。

        当第一次执行 SQL 语句后,查询结果会被存储在 SqlSession 的一级缓存中。再次执行相同的 SQL 时,如果一级缓存中已经有对应的查询结果,则不会去数据库查询,而是直接从缓存中获取。

        注意,执行 insert、update、delete 操作会清空一级缓存,因为这些操作可能会影响到之前的查询结果,为了保证数据一致性,MyBatis 在执行这些操作后会清除一级缓存的内容。

        6.2 二级缓存

        二级缓存是基于 namespace(命名空间)级别的缓存,它的生命周期比一级缓存长,可以被多个 SqlSession 共享。即使关闭了某个 SqlSession,只要缓存数据还有效,新的 SqlSession 在执行相同的 SQL 时仍然可以从二级缓存中获取数据。

        二级缓存默认是关闭的,需要在 MyBatis 的配置文件中启用全局二级缓存,并且在对应的 Mapper XML 文件中明确开启二级缓存配置,同时还可以配置缓存的存储实现类。

        二级缓存的生命周期与 Mapper Namespace 相关,只有当缓存中的数据被显式清除或超过了设定的有效期时才会失效。

        当查询数据时,MyBatis 会在一级缓存中查找,如果一级缓存中不存在,则会继续在二级缓存中查找,如果二级缓存中有相应结果,则返回缓存数据,否则才去数据库查询并将查询结果放入二级缓存。

        总之,一级缓存和二级缓存共同协作,可以在一定程度上减少对数据库的访问,提高系统性能,但同时也需要注意缓存带来的一致性问题,合理配置缓存的刷新策略和过期机制。

        综上所述,MyBatis 在现代 Java 应用开发中扮演着关键角色,它既简化了开发流程,又提升了开发效率和系统性能,因此在众多持久层框架中占据了重要地位。特别是在注重 SQL 性能优化、定制化程度较高以及追求开发效率的项目中,MyBatis 成为了开发者们的首选工具之一。

往期经典推荐

一文掌握Java动态代理的奥秘与应用场景-CSDN博客

即时编译器在JVM调优战场的决胜策略-CSDN博客

JVM内存模型深度解读-CSDN博客

SpringBoot开箱即用魔法:深度解析与实践自定义Starter-CSDN博客

直击Redis集群痛点:数据倾斜优化实战,打造高效分布式缓存架构-CSDN博客

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

超越不平凡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值