走进 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 绑定,即从 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博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/481352.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

网站引入 Prism,使得代码高亮显示,并一键复制代码块

曾几何时&#xff0c;苦恼如何将本地写好的博文&#xff0c;更好的展示读者屏幕前&#xff1f;若只是简简单单的文章&#xff0c;其实还是很好的解决它的&#xff01;可是&#xff0c;像我们这样写技术文章&#xff08;有点牵强&#xff09;的&#xff0c;在文章内容嵌入部分代…

【文献分享】Quantum Self-Consistent Ab-Initio Lattice Dynamics

题目&#xff1a;Quantum Self-Consistent Ab-Initio Lattice Dynamics 链接&#xff1a;Redirecting 量子自洽从头算晶格动力学 量子自洽Ab-Initio晶格动力学软件包&#xff08;QSCAILD&#xff09;是一个python库&#xff0c;用于计算晶体中与温度相关的有效2级和3级原子间…

【Java多线程】多线程的三种实现方式和多线程常用方法

目录 1、多线程的三种实现方式 1.1、继承Thread类的方式进行实现 1.2、实现Runnable接口的方式进行实现 1.3、利用Callable接口和Future接口方式实现 1.4、三种实现方式的优缺点 2、多线程常用方法 1、多线程的三种实现方式 在main()方法中&#xff0c;你可以创建和启动…

STL —— string(2)

本篇文章主要讲解string的用法。 目录 1. 迭代器&#xff08;Iterators&#xff09; 1.1 begin() 和 end() 1.2 rbegin() 和 rend() 2. 容量操作&#xff08;capacity&#xff09; 2.1 size()、length()、maxsize() 2.2 capacity() 2.3 empty()、clear() 2.4 reserve…

罗德与施瓦茨联合广和通全面验证RedCap模组FG132系列先进性能

近日&#xff0c;罗德与施瓦茨联合广和通完成Redcap(Reduce Capability)功能和性能验证。本次测试使用R&SCMX500 OBT(One Box Tester)无线通信测试仪&#xff0c;主要验证广和通RedCap模组FG132系列射频性能以及IP层吞吐量&#xff0c;包括RedCap上下行吞吐量和射频指标如矢…

【技巧】ChatGPT Prompt 提示语大全

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 主要来自&#xff1a;https://github.com/f/awesome-chatgpt-prompts ChatGPT SEO提示 Contributed by: StoryChief AI Reference: 7 Powerful ChatGPT Prompts to Create SEO Content Faster 供稿人&#xff1a;…

ruoyi-nbcio-plus基于vue3的flowable增加开始节点的表单绑定修改

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 http://122.227.135.243:9666/ 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a…

Python文件读写操作

文件操作注意点 注意点&#xff1a; 1. for line in file --> 会将偏移量移到末尾 2. buffering1 --> 缓冲区中遇到换行就刷新&#xff0c;即向磁盘中写入 3. 读操作结束后&#xff0c;文本偏移量就会移动到读操作结束位置 """编写一个程序,循环不停的写入…

快速区分清楚图形渲染中的AABB,KD树和BVH这些概念

快速区分清楚图形渲染中的AABB&#xff0c;KD树和BVH这些概念 主要想形象去区分好这些术语&#xff0c;目的是扫盲&#xff0c;先开好坑&#xff0c;内容持续填充。 0.先摆出这些词的全称 AABB&#xff1a; 原名&#xff1a;axis aligned bounding box&#xff1b;中文直译名…

Java语法学习八之认识String类

String类的重要性 在C语言中已经涉及到字符串了&#xff0c;但是在C语言中要表示字符串只能使用字符数组或者字符指针&#xff0c;可以使用标准库提供的字符串系列函数完成大部分操作&#xff0c;但是这种将数据和操作数据方法分离开的方式不符合面相对象的思想&#xff0c;而…

高效的Gitlab Flow最佳实践

文章目录 一、git flow二、github flow三、gitlab flow四、基于gitlab flow的最佳实践1.语义化版本号2.测试发布3.bug修复 参考 业界包含三种flow&#xff1a; Git flowGithub flowGitlab flow 三种工作流程&#xff0c;有一个共同点&#xff1a;都采用"功能驱动式开发&…

计算机二级Python基础操作题

题目来源&#xff1a;计算机二级Python半个月抱佛脚大法&#xff08;内呈上真题版&#xff09; - 知乎 第4&#xff0c;5&#xff0c;6&#xff0c;7&#xff0c;9&#xff0c;10&#xff0c;11套 1. 基础题1 sinput() print("{:\"^30x}".format(eval(s))) b …

xilinx linux AXI GPIO 驱动学习

vivado工程 vivado 配置一个 AXI GPIO&#xff0c; 全输出&#xff0c;宽度为1 设备树解读 生成的对应pl.dtsi设备树文件如下 axi_gpio: gpio40020000 {#gpio-cells <2>;clock-names "s_axi_aclk";clocks <&clkc 15>;compatible "xlnx,…

海外盲盒App开发:探索惊喜与乐趣的跨国新体验

在全球化日益加速的今天&#xff0c;人们对新鲜、有趣、充满惊喜的体验需求不断增长。盲盒作为一种充满神秘与乐趣的玩法&#xff0c;正迅速在全球范围内走红。为了满足广大海外用户的盲盒体验需求&#xff0c;我们致力于开发一款海外盲盒App&#xff0c;为用户带来跨国界的惊喜…

C#,图论与图算法,计算无向连通图中长度为n环的算法与源代码

1 无向连通图中长度为n环 给定一个无向连通图和一个数n,计算图中长度为n的环的总数。长度为n的循环仅表示该循环包含n个顶点和n条边。我们必须统计存在的所有这样的环。 为了解决这个问题,可以有效地使用DFS(深度优先搜索)。使用DFS,我们可以找到特定源(或起点)的长度…

高精度AI火灾烟雾检测算法,助力打造更加安全的楼宇环境

一、方案背景 近日&#xff0c;南京居民楼火灾事故导致15人死亡的新闻闹得沸沸扬扬&#xff0c;这一事件又激起了大家对楼宇火灾隐患的进一步担忧。事后我们除了思考政府、消防及物业部门应对此事的解决办法&#xff0c;我们还应该思考如何利用现有的技术帮助人们减少此类事情的…

手撕算法-无重复字符的最长子串

描述 分析 滑动窗口&#xff0c;记录窗口中的所有出现的字符&#xff0c;然后窗口左边界固定&#xff0c;右边界右滑&#xff0c;如果&#xff0c;窗口中不存在新的字符&#xff0c;则右滑成功&#xff0c;否则左边界右滑&#xff0c;直到窗口中不存在右边界的值。 描述感觉不…

Linux初学(八)磁盘管理

一、磁盘管理 1.1 简介 磁盘的工作原理&#xff1a; 添加磁盘对磁盘进行分区格式化磁盘挂载和使用磁盘 磁盘的类型&#xff1a; 固态机械 磁盘的接口类型&#xff1a; IDESTSTSCSI 磁盘工作的原理&#xff1a; 磁盘&#xff0c;特别是硬盘&#xff0c;和内存不同&#xff0c;…

【网络基础】网络层基本协议介绍

目录 一、IP数据包 1.1 网络层的功能 1.2 IP数据包格式 二、ICMP协议介绍 2.1 作用 2.2 常用命令 2.2.1 Ping命令 2.2.2 tracert命令 2.3 广播域 三、ARP协议介绍 3.1 作用 3.2 原理 一、IP数据包 1.1 网络层的功能 定义了基于IP协议的逻辑地址&#xff0c;就是I…

IDEA使用手册

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…