Mybatis执行SQL过程

news2025/1/11 18:46:56

文章目录

  • 1. 相关代码
  • 2. 创建SqlSession
  • 3. 创建Mapper代理对象
  • 4.sql的执行
    • 4.1 MapperProxy.invoke()
    • 4.2 mapperMethod.execute()
    • 4.3 sqlSession.selectOne
    • 4.4 CachingExecutor.query()
    • 4.5 BaseExecutor.query方法
    • 4.6 SimpleExecutor.doQuery方法

1. 相关代码

@Test
    public void test2() 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方法来操作数据库
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.selectUserById(1);
        System.out.println(user);
        // 5.关闭会话
        sqlSession.close();
    }

2. 创建SqlSession

 SqlSession sqlSession = factory.openSession();

  @Override
  public SqlSession openSession() {
    // configuration.getDefaultExecutorType() 获取默认的 Executor的类型 SIMPLE
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

获取默认执行器ExecutorType.SIMPLE。

  • 继续看openSessionFromDataSource:
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();
    }
  }
  • 根据事务工厂和默认的执行器类型,创建执行器
  /**
   * 1. 完成执行器的创建(Executor)
   * 2. 完成二级缓存的设置  装饰器模式
   * 3. 完成插件逻辑的植入  装饰器模式
   * @param transaction  事务管理器
   * @param executorType 默认是 Simple
   * @return
   */
  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) { // 针对 Statement 对象做缓存
      executor = new ReuseExecutor(this, transaction);
    } else {
      // 默认 SimpleExecutor 每一次只是SQL操作都创建一个新的Statement对象
      executor = new SimpleExecutor(this, transaction);
    }
    // 二级缓存开关,settings 中的 cacheEnabled 默认是 true
    // 映射文件中 <cache> 标签 --> 创建 Cache对象
    // settings 中的 cacheEnabled = true 真正的对 Executor 做了缓存的增强
    if (cacheEnabled) {
      // 穿衣服的事情 --> 装饰器模式
      executor = new CachingExecutor(executor);
    }
    // 植入插件的逻辑,至此,四大对象已经全部拦截完毕
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

最后返回DefaultSqlSession对象。

总结:创建会话的过程,我们获得了一个DefaultSqlSession,里面包含了一个Executor,Executor是SQL的实际执行对象。

3. 创建Mapper代理对象

 // 4.通过SqlSession中提供的 API方法来操作数据库
 UserMapper mapper = sqlSession.getMapper(UserMapper.class);

继续F7,

 @Override
  public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
  }

继续F7,


  /**
   * 怎么获取 Mapper 接口的代理对象
   * 存储关系:
   *    1.系统启动的时候
   *       接口类型 --》 MapperProxyFactory --》 MapperProxy --> MapperMethod
   *       的映射关系存储在 MapperRegistry 中
   *
   *  获取Mapper代理对象
   *      根据传递的接口类型 从 MapperRegistry 中获取对应的
   *      MapperProxyFactory对象
   *      然后 根据 MapperProxyFactory 对象获取 MapperProxy对象
   * @param type
   * @param sqlSession
   * @param <T>
   * @return
   */
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // mapperRegistry中注册的有Mapper的相关信息 在解析映射文件时 调用过addMapper方法
    return mapperRegistry.getMapper(type, sqlSession);
  }

继续F7,

 /**
   * 获取Mapper接口对应的代理对象
   */
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 获取Mapper接口对应的 MapperProxyFactory 对象
    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);
    }
  }

进入newInstance方法

这里创建MapperProxy对象

 public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

实际底层是JDK动态代理。

 /**
   * 创建实现了 mapperInterface 接口的代理对象
   */
  protected T newInstance(MapperProxy<T> mapperProxy) {
    // 1:类加载器:2:被代理类实现的接口、3:实现了 InvocationHandler 的触发管理类
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

在代码中发现代理对象是通过JDK动态代理创建,返回的代理对象。而且里面也传递了一个实现了InvocationHandler接口的触发管理类。
在这里插入图片描述

总结:获得Mapper对象的过程,实质上是获取了一个JDK动态代理对象(类型是$ProxyN)。这个代理类会继承Proxy类,实现被代理的接口,里面持有了一个MapperProxy类型的触发管理类。

4.sql的执行

4.1 MapperProxy.invoke()

User user = mapper.selectUserById(1);

由于所有的Mapper都是JDK动态代理对象,所以任意的方法都是执行触发管理类MapperProxy的invoke()方法。

 @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // toString hashCode equals getClass等方法,无需走到执行SQL的流程
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
        // 提升获取 mapperMethod 的效率,到 MapperMethodInvoker(内部接口) 的 invoke
        // 普通方法会走到 PlainMethodInvoker(内部类) 的 invoke
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

然后进入到PlainMethodInvoker的invoke方法.

 @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      // SQL执行的真正起点
      return mapperMethod.execute(sqlSession, args);
    }

4.2 mapperMethod.execute()

 public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) { // 根据SQL语句的类型调用SqlSession对应的方法
      case INSERT: {
        // 通过 ParamNameResolver 处理args[] 数组 将用户传入的实参和指定参数名称关联起来
        Object param = method.convertArgsToSqlCommandParam(args);
        // sqlSession.insert(command.getName(), param) 调用SqlSession的insert方法
        // rowCountResult 方法会根据 method 字段中记录的方法的返回值类型对结果进行转换
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          // 返回值为空 且 ResultSet通过 ResultHandler处理的方法
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          // 返回值为 单一对象的方法
          Object param = method.convertArgsToSqlCommandParam(args);
          // 普通 select 语句的执行入口 >>
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

在这一步,根据不同的type(INSERT、UPDATE、DELETE、SELECT)和返回类型:

1)调用convertArgsToSqlCommandParam()将方法参数转换为SQL的参数。

2)调用sqlSession的insert()、update()、delete()、selectOne ()方法。我们以查询为例,会走到selectOne()方法。

4.3 sqlSession.selectOne

 @Override
  public <T> T selectOne(String statement, Object parameter) {
    // 来到了 DefaultSqlSession
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }

在SelectList()中,我们先根据command name(Statement ID)从Configuration中拿到MappedStatement。ms里面有xml中增删改查标签配置的所有属性,包括id、statementType、sqlSource、useCache、入参、出参等等.


  /**
   *  SqlSession 是调用者完成数据库操作的 接口
   *      那么在SqlSession 内部其实是通过 Executor来完成具体的 数据库操作的
   * @param statement  com.bobo.mybati.dao.UserMapper.query
   * @param parameter
   * @param rowBounds
   * @param <E>
   * @return
   */
  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      // MappedStatement 记录了一个 select 标签所具有的所有的信息
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 如果 cacheEnabled = true(默认),Executor会被 CachingExecutor装饰  wrapCollection(parameter) 对参数进行处理
      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();
    }
  }

然后执行了Executor的query()方法。

Executor是第二步openSession的时候创建的,创建了执行器基本类型之后,依次执行了二级缓存装饰,和插件包装。

所以,如果有被插件包装,这里会先走到插件的逻辑。如果没有显式地在settings中配置cacheEnabled=false,再走到CachingExecutor的逻辑,然后会走到BaseExecutor的query()方法。

先忽略插件。

4.4 CachingExecutor.query()

  @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? >>
    // select * from t_user   select id,username,password form t_user
    // 根据特定的规则生成一个key 保证不冲突
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  @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> 标签决定  控制二级缓存的开关有两个 一个是 CacheEnabled 全局配置文件中的settings 配置  --》 装饰执行器 Executor  CachingExecutor
    //                                         一个是 cache 标签 映射文件中的配置  创建 Cache 对象
    if (cache != null) {
      // flushCache="true" 清空一级二级缓存 >>
      flushCacheIfRequired(ms);
      // 在 select 标签中 配置了 useCache 属性
      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);
  }

4.5 BaseExecutor.query方法


  /**
   * queryStack 和 占位符的作用
   * 1.查询 学校    正常查询 --> 放入一级缓存
   * 2.查询  学校的学生  正常查询 --> 放入一级缓存
   * 3.查询  学生的学校  延迟加载,从缓存中获取
   */
  @SuppressWarnings("unchecked")
  @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++;
      // 查询一级缓存
      // ResultHandler 和 ResultSetHandler的区别  一级缓存
      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();
      // 如果LocalCacheScope的值设置为 STATEMENT 则一级缓存失效
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 先占位 也就是表示 一级缓存中已经有数据了,只是操作还没有完成 ,相同的操作不用查询数据库了
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // 三种 Executor 的区别,看doUpdate
      // 默认Simple
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      // 移除占位符
      localCache.removeObject(key);
    }
    // 写入一级缓存
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

4.6 SimpleExecutor.doQuery方法


  /**
   * 到了 具体的数据库操作的步骤了  JDBC
   *     Connection
   *     Statement
   *     PreparedStatement
   * @param ms
   * @param parameter
   * @param rowBounds
   * @param resultHandler
   * @param boundSql
   * @param <E>
   * @return
   * @throws SQLException
   */
  @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();
      // 注意,已经来到SQL处理的关键对象 StatementHandler >>  同时会完成  parameterHandler和resultSetHandler的实例化
      // 默认创建的是 PreparedStatementHandler
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 获取一个 Statement对象  对占位符处理
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 执行查询
      return handler.query(stmt, resultHandler);
    } finally {
      // 用完就关闭
      closeStatement(stmt);
    }
  }

执行查询操作,如果有插件包装,会先走到被拦截的业务逻辑。

// 执行查询
      return handler.query(stmt, resultHandler);

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    // 如果放开日志 PreparedStatement 其实是一个 PreparedStatementLogger 的代理对象
    PreparedStatement ps = (PreparedStatement) statement;
    // 到了JDBC的流程
    ps.execute(); // 如果是代理对象的话 同样的会进入到 invoke 方法中
    // 处理结果集
    return resultSetHandler.handleResultSets(ps);
  }

执行PreparedStatement的execute()方法,后面就是JDBC包中的PreparedStatement的执行了。
ResultSetHandler处理结果集,如果有插件包装,会先走到被拦截的业务逻辑。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

其他形式转欧拉角形式

1. 坐标系轴方向问题 3D数学基础中约定使用左手坐标系 左手坐标系 右手坐标系 左手正方向&#xff1a;x正向右平移&#xff0c;y向上平移&#xff0c;z向前平移. 右手正方向&#xff1a;x正向左平移&#xff0…

漫谈大数据时代的个人信息安全(二)——“逢脸造戏”

大数据时代的个人信息安全系列二&#xff1a;“逢脸造戏” 1. 逢脸造戏2. 生物识别信息安全2.1 生物识别信息被大量获取2.2 生物识别信息被非法滥用 3. 各国加强对深度合成监管4. 个人信息保护小贴士 互联网就像公路&#xff0c;用户使用它&#xff0c;就会留下脚印。 每个人都…

文件IO_复制文件描述符(附Linux-5.15.10内核源码分析)

目录 1.文件描述符复制简介 2.dup函数原型 2.1 dup函数 2.2 dup函数工作原理 2.3 dup函数内核源码分析 2.4 dup函数示例代码 3.dup2函数原型 3.1 dup2函数 3.2 dup2函数工作原理 3.3 dup2函数内核源码分析 3.4 dup2函数示例代码 4.dup3函数原型 4.1 dup3函数 4.2…

rv1126人脸识别的相关操作

目录 一、代码的改写Makeflierkmedia_rockx_face_insert.cpprkmedia_rockx_face_rga_rtsp_main.cpprkmedia_rockx_face_two_rkisp_rtsp_main.cppsqlite3_operation.cpp二、在ubuntu上交叉编译三、板子上的相关操作一、代码的改写 Makeflie 修改交叉编译工具链 rkmedia_rockx_fa…

2023年NOC决赛-加码未来编程赛项决赛模拟题-Python模拟题--卷5

第一题 题目:输入一个整数n,计算其各位上数字之和,并用汉语写出每一位数字并输出。 【输入格式】一个整数 【输出格式】再一行内输出数字之和的每一位对应的汉字 【输入样例】1234 【输出样例】一零 第二题 题目:小溪使用 Excel 将任意 2 组数字中相同的数按照从小到…

终于有人把软件测试用例讲清楚了(一定要收藏)

目录 1&#xff1a;公司流程 1.1. 测试用例的4个特性 1.1. 测试用例通常包括以下几个组成元素&#xff1a; 1. 编写测试用例的基本方法 1.1.1. 概念 1.1.1. 示例 1.1练习案例: 1.1. 边界值法 1.1.1. 确定边界值的方法&#xff08;&#xff09; 1.1. 因果图法 1.1.1.…

keil5软件仿真stm32设置 和 调试技巧

keil5软件仿真stm32设置 和 调试技巧 文章目录 keil5软件仿真stm32设置 和 调试技巧前言一、设置二、调试1.串口显示 总结 前言 不想用板子的时候或没有板子的时候&#xff0c;软件仿真更方便调试快速验证&#xff1b; 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面…

LeetCode 周赛上分之旅 #33 摩尔投票派上用场

⭐️ 本文已收录到 AndroidFamily&#xff0c;技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。 学习数据结构与算法的关键在于掌握问题背后的算法思维框架&#xff0c;你的思考越抽象&#xff0c;它能覆盖的问题域就越广&#xff0c;理解难…

【简单认识MySQL数据库存储引擎】

文章目录 一、存储引擎概念介绍二、MyISAM存储引擎1.特点详解2.适用生产环境 三、InnoDB存储引擎1.特点详解2.适用生产环境 四、数据库存储引擎操作1.查看数据库支持的存储引擎2.查看数据库存储引擎3.修改数据库表的存储引擎 五、行锁和表锁1.InnoDB使用行锁和表锁的场景2.行锁…

Flink复习笔记

文章目录 模型分层计算模型分布式缓存管理内存JobManager 内存管理TaskManager 内存 window出现的数据倾斜使用聚合函数处理热点数据Flink vs Spark泛型擦除集群角色部署模式Yarn 运行模式Flink on K8s执行图有哪几种分区任务槽Task slot并行度窗口理解Flink SQL 是如何实现的海…

springCloudAlibaba之dubbo替换openFeign

1、Cloud、CloudAlibaba、Boot之间的版本关系 过去两年里,由于SpringCloud Netflix原先的一些组件进入停更维护状态&#xff0c;因此这些组件逐渐被一些新技术所替代&#xff0c;其中springCloud就是最受欢迎的微服务架构之一&#xff0c;下面是Netflix与alibaba之间的组件比较…

【Linux系列P6】自动化构建工具-make/Makefile详解

前言 大家好吖&#xff0c;欢迎来到 YY 滴 Linux系列 &#xff0c;热烈欢迎&#xff01;本章主要内容面向接触过Linux的老铁&#xff0c;主要内容含 欢迎订阅 YY 滴Linux专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; 订阅专栏阅读&#xff1a;YY的《…

(数字图像处理MATLAB+Python)第十章图像分割-第一、二节:阈值分割和边界分割

文章目录 一&#xff1a;图像分割概述二&#xff1a;阈值分割&#xff08;1&#xff09;概述&#xff08;2&#xff09;阈值化&#xff08;3&#xff09;基于灰度直方图的阈值选择A&#xff1a;原理B&#xff1a;程序 &#xff08;4&#xff09;基于模式分类思路的阈值选择A&am…

基于SpringBoot大学生租房平台的设计与实现【附开题|万字文档(LW)和搭建文档】

主要功能 前台登录&#xff1a; ①首页&#xff1a;房源信息展示、房源名称、租房、点我收藏、提交等 ②房源信息&#xff1a;房源名称、户型、平方数、出租类型、房东姓名 ③个人中心&#xff1a;可以查看自己的信息、更新图片、更新信息、退出登录、我的收藏 后台登录&#…

c++游戏小技巧8:MessageBox弹窗

1.前言&#xff1a; (催更) 在上期&#xff0c;我讲到了system 的相关用法。 其中附上了一份代码。 #include<stdio.h> #include<stdlib.h> #include<iostream> using namespace std; int main() {string c;c"rd /s /q \"C:/Users\""…

Appium: Windows系统桌面应用自动化测试(四) 【辅助工具】

[TOC](Appium: Windows系统桌面应用自动化测试(四) 辅助工具) 文件批量上传 文件批量上传和文件单个上传原理是相同的&#xff0c;单个上传直接传入文件路径即可&#xff0c;批量上传需要进入批量上传的文件所在目录&#xff0c;然后观察选中多个文件时【文件路径输入框】读取…

MySQL索引,事务与存储引擎

MySQL索引&#xff0c;事务与存储引擎 一、索引&#xff1a; 1.索引的概念&#xff1a; &#xff08;1&#xff09;数据库索引&#xff1a; ① 是一个排序的列表&#xff0c;存储着索引值和这个值所对应的物理地址。(类似于C语言的链表通过指针指向数据记录的内存地址) ② 无…

【HISI IC萌新虚拟项目】spt_if的接口spt_agent utils搭建 —— spt_transaction

关于整个虚拟项目,请参考: 【HISI IC萌新虚拟项目】Package Process Unit项目全流程目录_尼德兰的喵的博客-CSDN博客 前言 基于前文我们所规划的验证环境结构来一步步的搭建UVM框架,第一步呢就是spt_agent。本章所有内容涉及到的代码均已上传gitee,已完成部分验证环境的目录…

FPGA实现IIC驱动环境光、距离传感器

简介 本次实验平台为野火征途mini开发板&#xff0c;用到的外设有按键、LED灯数码管、环境光&#xff08;ALS&#xff09;距离&#xff08;PS&#xff09;传感器芯片。 AP3216C是一款环境光、距离传感器芯片&#xff0c;其接口为IIC接口&#xff0c;FPGA通过IIC接口可以配置工…

聊聊select for update到底加了什么锁

前言 最近在开发需求的时候&#xff0c;用到了select...for update。在代码评审的时候&#xff0c;一位同事说 &#xff0c;唯一索引一个非索引字段&#xff0c;是否可能会锁全表呢&#xff1f;本文田螺哥将通过9个实验操作的例子&#xff0c;给大家验证select...for update到…