『手撕 Mybatis 源码』05 - SqlSession 执行主流程

news2024/11/22 10:13:07

SqlSession 执行主流程

获取 BoundSql

  1. 经过加载完所有配置之后,继续梳理执行 sql 的过程
public class MybatisTest {
  @Test
  public void test1() throws IOException {
	...
	// 4. 委派给 Executor 来执行,Executor 执行时又会调用很多其他组件(参数设置、解析 sql 的获取,sql 的执行、结果集的封装)
	User user = sqlSession.selectOne("user.findUserById", 1);
    ...
  }
}
  1. 当调用 selectOne() 时,底层将会调用的是 selectOne(java.lang.String, java.lang.Object)
public class DefaultSqlSession implements SqlSession {
  ...
  @Override
  public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.selectList(statement, parameter);
  	...
  }
}
  1. 继续调用时,会先装入默认的分页器 RowBounds,然后继续调用重载的 selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)
public class DefaultSqlSession implements SqlSession {
  ...
  @Override
  public <E> List<E> selectList(String statement, Object parameter) {
    // 调用重载方法
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }
}
  1. 继续调用,也会装入空的 ResultHandler,继续调用重载的 selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
public class DefaultSqlSession implements SqlSession {
  ...
  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    // 参数4:结果集处理器
    return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
  }
}
  1. 通过传入 statementId 即 “user.findUserById”,然后 configuration 对象拿到 MappedStatement 对象,然后利用执行器来执行查询
public class DefaultSqlSession implements SqlSession {
  ...
  private final Configuration configuration;
  private final Executor executor;
  ...
  private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  	 ...
     // 根据传入的 statementId 即 user.findUserById,获取 MappedStatement 对象
     MappedStatement ms = configuration.getMappedStatement(statement);
     // 调用执行器的查询方法
     // wrapCollection(parameter) 是用来装饰集合或者数组参数
     return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  }
}
  1. 在执行器执行的查询时,是调用经过封装后的 CachingExecutorquery() 方法。该方法会从 MappedStatement 对象中拿到对应的 BoundSql 对象
public class CachingExecutor implements Executor {
  ...
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取绑定的 SQL 语句,比如 "SELECT * FROM user WHERE id = ? "
    // 其中 parameterObject 是 1
    BoundSql boundSql = ms.getBoundSql(parameterObject);
	...
  }
}
  • 其中,MappedStatement 对象会将参数 parameterObject 传入,然后委托 SqlSource 来获取 BoundSql,从上面的流程下来是不涉及动态 sql,所以这个 SqlSourceStaticSqlSource
public final class MappedStatement { 
  ...
  private SqlSource sqlSource;
  ...
  public BoundSql getBoundSql(Object parameterObject) {
    // sqlSource 是解析 SELECT id, name FROM  user WHERE id = #{id} 之后得到的源集
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    ...
 }
}
  • StaticSqlSource 会把 configurationsqlparameterMappingsparameterObject 对象封装到 BoundSql 内并且返回
public class StaticSqlSource implements SqlSource {

  private final String sql;
  private final List<ParameterMapping> parameterMappings;
  private final Configuration configuration;
  
  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    /*
    	sql:SELECT id, name FROM  user WHERE id = ?
    	parameterMappings:ParameterMapping{property='id', mode=IN, javaType=class java.lang.Integer, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
    	parameterObject:现在是 1
    */
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }
}
  • 拿到 BoundSql 后,先检查参数是不是一个对象映射,需要获取参数对应的映射位,但是现在的映射文件的 sql 输入只是个整形,所以这里执行是空,最后 MappedStatement 对象直接返回 BoundSql 对象,然后 CachingExecutor 就拿到对应的 BoundSql
public final class MappedStatement { 
  ...
  private SqlSource sqlSource;
  ...
  public BoundSql getBoundSql(Object parameterObject) {
  	// 拿到了 boundSql 
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    }

    // 这次是空,这里检查参数的映射对象,即 ResultMap
    for (ParameterMapping pm : boundSql.getParameterMappings()) {
      String rmId = pm.getResultMapId();
      ...
    }
    // 直接返回
    return boundSql;
  }
}
  1. 总结
    在这里插入图片描述

生成 CacheKey

  1. 问题
  • CacheKey 是由几部分构成的,怎么生成的?
  • 怎么完成的 CacheKey 比较,怎么保证 CacheKey 的唯一性?
  1. 对于 SqlSession 而言,它会把实际执行交给 Executor,而我们知道在 MyBatis 中会话级别是有缓存的,那么这个缓存 Key 是怎么构造,就是在 CachingExecutor 当中,根据 MappedStatementparameterObject 实际参数、rowBounds 分页对象以及 BoundSql 来构造的
public class CachingExecutor implements Executor {
  ...
  //第一步
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取绑定的 SQL 语句,比如 "SELECT * FROM user WHERE id = ? "
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 生成缓存Key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    ...
  }
}
  1. CachingExecutor 中,它会委派 SimpleExecutor 来创建缓存建
public class CachingExecutor implements Executor {

  private final Executor delegate;
  ...
  @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
     // delegate 这个属性就是被包装的 executor,现在是 simpleExecutor
    return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql); 
  }
}
  1. SimpleExecutor 首先会创建一个 CacheKey 对象,这个对象会初始化各种算子参数,用于后面的缓存计算,缓存 Key 的组成部分如下
public class CacheKey implements Cloneable, Serializable {

  private static final int DEFAULT_MULTIPLIER = 37;
  private static final int DEFAULT_HASHCODE = 17;
  ...
  private final int multiplier; // 参与 hash 运算的乘数
  private int hashcode; // cachekey 的 hash 值,在 update 函数中实时算出来
  private long checksum; // 校验和,hash 值的和
  private int count; // updateList 的中的元素个数
  // 根据该集合中的元素判断两个 cacheKey 是否相同
  private List<Object> updateList;

  public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLIER;
    this.count = 0;
    this.updateList = new ArrayList<>();
  }
}
  1. 回到 SimpleExecutor,它其实调用的是父类 BaseExecutorcreateCacheKey() 方法,会根据 MappedStatementparameterObject 实际参数、rowBounds 分页对象以及 BoundSql 来计算 CacheKey 内的哈希值
public abstract class BaseExecutor implements Executor {
  ...
  @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 创建cacheKey对象
    CacheKey cacheKey = new CacheKey();
    // id
    cacheKey.update(ms.getId());
    // 分页参数
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    // sql
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        // 参数的值
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // 当前环境的值也会设置
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }
}
  • 其中 CacheKeyupdate() 方法会把输入的各种参数计算一次哈希,然后把值也存到里面去
public class CacheKey implements Cloneable, Serializable {
  ...
  public void update(Object object) {
    // 获取参数的object的hash值
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

    // 更新count 、checksum以及hashcode的值
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;

    // 将对象添加到list集合中
    updateList.add(object);
  }
}
  • 生成的哈希值如下
    在这里插入图片描述
  1. 总结
  • 最后生成的 Key 找缓存值流程如下
    在这里插入图片描述
  • 生成 CacheKey 流程
    在这里插入图片描述

缓存优先级

  1. 问题
  • 如果开启了一二级缓存,究竟是调用一级缓存优先还是二级缓存?
  1. 首先配置文件中,开启二级缓存
<mapper namespace="user">

  <cache></cache>
  ...
</mapper>
  1. 拿到 CacheKeyparameterObjectMappedStatementBoundSqlRowBounds 时候,开始执行真正的查询
public class CachingExecutor implements Executor {
  ...
  // 第一步
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    ...
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
}
  1. query() 方法
  • 首先会从 MappedStatement 拿到二级缓存,然后检查对应的 sql 是否配置了 flushCache=true,是的话,先把二级缓存清空了
  • 然后判断对应语句是否使用缓存 useCache,默认是开启的
  • 然后根据 StatementType 确定是否处理出参参数,这里是 PREPARED 类型,不是存储过程(CALLABLE 类型),所以不处理
  • 再从二级缓存中查询数据,没有委托给 SimpleExecutor 查询一级缓存和数据库,最后把查出来的数据放回到二级缓存(暂时是存到 map 集合,实际还没存到二级缓存)
public class CachingExecutor implements Executor {

  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
  ...
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // 获取二级缓存,需要在配置文件中配置 <cache></cache> 标签
    //   <select id="findUserById" parameterType="int" resultType="com.itheima.pojo.User" flushCache="true">
    Cache cache = ms.getCache();
    if (cache != null) {
      // 刷新(每次查询前清空)二级缓存 (存在缓存且 flushCache 为true时)
      flushCacheIfRequired(ms);
      // 默认就是 true    
      // <select id="findUserById" parameterType="int" resultType="com.itheima.pojo.User" useCache="true">
      if (ms.isUseCache() && resultHandler == null) { 
		// 处理输出参数,存储过程才会处理,这里 StatementType 是 PREPARED 类型
        ensureNoOutParams(ms, boundSql);
        // 从二级缓存中查询数据
        List<E> list = (List<E>) tcm.getObject(cache, key);
        // 如果二级缓存中没有查询到数据,则查询一级缓存及数据库
        if (list == null) {
          // 委托给 BaseExecutor 执行
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 将查询结果 要存到二级缓存中(注意:此处只是存到map集合中,没有真正存到二级缓存中)
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // 委托给 BaseExecutor 执行
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
}
  1. 委托给 SimpleExecutor 父类的 query() 方法
  • 首先判断 sql 是否配置了 flushCacheRequired,是的话会在执行器执行之前,清空本地以及缓存
  • 然后从一级缓存中获取数据
  • 如果有缓存结果,再判断是否是存储过程类型(CALLABLE 类型),是的话处理输出参数,否则的话直接从数据库里面查询结果
public abstract class BaseExecutor implements Executor {
  protected PerpetualCache localCache;
  ...
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    // 如果该执行器已经关闭,则抛出异常
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 1. 如果配置了 flushCacheRequired 为 true,则会在执行器执行之前就清空本地一级缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      // 1.1. 清空缓存
      clearLocalCache();
    }
    List<E> list;
    try {
      // 2. 查询堆栈 + 1
      queryStack++;
      // 从一级缓存中获取数据
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        // 3.1. 已有缓存结果,则处理本地缓存结果输出参数(只有存储过程会走)
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // 3.2. 没有缓存结果,则从数据库查询结果
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      // 查询堆栈数 -1
      queryStack--;
    }
    ...
  }
}
  1. 总结
    在这里插入图片描述

StatementHandler 预处理得到 Statement 对象

  1. SimpleExecutor 从缓存中拿不到到数据,就需要从 db 中获取
public abstract class BaseExecutor implements Executor {
  ...
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ...
    try {
      ...
      } else {
        // 3.2. 没有缓存结果,则从数据库查询结果
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
  }
}
  1. 然后执行 SimpleExecutordoQuery(),这是由 SimpleExecutor 实现的。其中查询前会在本地缓存中,添加占位符,拿到数据后再移除
public abstract class BaseExecutor implements Executor {
  ...
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 1. 首先向本地缓存中存入一个 ExecutionPlaceholder 的枚举类占位 value
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // 2. 执行 doQuery 方法
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      // 3. 执行完成移除这个 key
      localCache.removeObject(key);
    }
    // 4. 查询结果存入缓存中
    localCache.putObject(key, list);
    // 5. 如果 MappedStatement 的类型为 CALLABLE,则向 localOutputParameterCache 缓存中存入 value 为 parameter 的缓存
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }
}
  1. SimpleExecutor 先从 MappedStatement 拿到 Configuration 对象,然后通过 Configuration 对象创建 StatementHandler 语句处理器
public class SimpleExecutor extends BaseExecutor {
  
  protected Executor wrapper;
  ...
  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      // 1. 获取配置实例
      Configuration configuration = ms.getConfiguration();
      // 2. new 一个 StatementHandler 实例
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      ...
    } finally {
      // 5. 关闭statement
      closeStatement(stmt);
    }
  }
}
  1. StatementHandler 创建很关键,它会先通过 RoutingStatementHandler 以及插件拦截装饰后返回 StatementHandler 对象
  • 【重点】: 这里的插件机制是 MyBatis 扩展的关键地方之一
public class Configuration {
  ...
  protected final InterceptorChain interceptorChain = new InterceptorChain();
  ...
  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    // 创建路由功能的 StatementHandler,根据 MappedStatement 中的 StatementType,可以在 sql 中配置
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    // 插件机制:对核心对象进行拦截
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }
}
  • 其中 StatementHandler 组织如下
    在这里插入图片描述
    • BaseStatementHandler: 基础语句处理器(抽象类),它基本把语句处理器接口的核心部分都实现了,包括配置绑定、执行器绑定、映射器绑定、参数处理器构建、结果集处理器构建、语句超时设置、语句关闭等,并另外定义了新的方法 instantiateStatement() 供不同子类实现以便获取不同类型的语句连接,子类可以普通执行 SQL 语句,也可以做预编译执行,还可以执行存储过程等
    • SimpleStatementHandler:普通语句处理器,继承 BaseStatementHandler 抽象类,对应 java.sql.Statement 对象的处理,处理普通的不带动态参数运行的 SQL,即执行简单拼接的字符串语句,同时由于 Statement 的特性,SimpleStatementHandler 每次执行都需要编译 SQL (注意:我们知道 SQL 的执行是需要编译和解析的)
    • PreparedStatementHandler:预编译语句处理器,继承 BaseStatementHandler 抽象类,对应 java.sql.PrepareStatement 对象的处理,相比上面的普通语句处理器,它支持可变参数 SQL 执行,由于 PrepareStatement 的特性,它会进行预编译,在缓存中一旦发现有预编译的命令,会直接解析执行,所以减少了再次编译环节,能够有效提高系统性能,并预防 SQL 注入攻击(所以是系统默认也是我们推荐的语句处理器)
    • CallableStatementHandler:存储过程处理器,继承 BaseStatementHandler 抽象类,对应 java.sql.CallableStatement 对象的处理,很明了,它是用来调用存储过程的,增加了存储过程的函数调用以及输出/输入参数的处理支持
    • RoutingStatementHandler:路由语句处理器,直接实现了 StatementHandler 接口,作用如其名称,确确实实只是起到了路由功能,并把上面介绍到的三个语句处理器实例作为自身的委托对象而已,所以执行器在构建语句处理器时,都是直接 new 了 RoutingStatementHandler 实例
  1. RoutingStatementHandler 会根据 sql 的 statementType 选择对应处理的 handler
public class RoutingStatementHandler implements StatementHandler {

  private final StatementHandler delegate;

  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    // 可以在 Mapper.xml 对 sql 指定 statementType
    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }
  }
}
  1. 获取到 StatementHandler 之后,就需要执行第 3 步创建 Statement 对象
public class SimpleExecutor extends BaseExecutor {
  ...
  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      // 1. 获取配置实例
      Configuration configuration = ms.getConfiguration();
      // 2. new 一个 StatementHandler 实例
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 3. 准备处理器,主要包括创建 statement 以及动态参数的设置
      stmt = prepareStatement(handler, ms.getStatementLog());
      ...
    } finally {
      // 5. 关闭statement
      closeStatement(stmt);
    }
  }
}
  1. SimpleExecutor 获取 Statement 对象前,会通过 JDBCTransaction 先获取 Connection 对象,即通过 DataSource 返回 Connection 对象。如果开启了日志调试模式,返回的 Connection 对象是经过代理的。然后 RoutingStatementHandler 做预处理得到预编译的 Statement 对象,并参数化后返回 Statement 对象
public class SimpleExecutor extends BaseExecutor {
  
  protected Transaction transaction;
  ...
  // 这个是父类的方法
  protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) { // 是否进行日志调试
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }
  ...
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // 1. 获取代理后(增加日志功能)的Connection对象
    Connection connection = getConnection(statementLog);
    // 2. 创建 Statement 对象(可能是一个 SimpleStatement,一个 PreparedStatement 或 CallableStatement)
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 3. 参数化处理
    handler.parameterize(stmt);
    // 4. 返回执行前最后准备好的Statement对象
    return stmt;
  }
}
  1. RoutingStatementHandler 再次委派 PreparedStatmentHandler 来处理
public class RoutingStatementHandler implements StatementHandler {

  private final StatementHandler delegate;
  ...
  @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    return delegate.prepare(connection, transactionTimeout);
  }
}
  1. PreparedStatmentHandler 会通过拿到 BoundSql 中的 sql 之后,通过 Connection 对 sql 进行预编译,得到 PrepareStatement 并返回
public class PreparedStatementHandler extends BaseStatementHandler {
  ...
  protected BoundSql boundSql;
  // 这个是父类 BaseStatementHandler 的方法
  @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      statement = instantiateStatement(connection);
      ...
      return statement; // 获取预编译对象
    } catch (SQLException e) {
      ...
    }
  }
  ...
  @Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
       ...
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
      return connection.prepareStatement(sql); //# 获取预编译对象
    } else {
      ...
    }
  }
}
  1. 总结
    在这里插入图片描述

Statement 参数化设置

  1. 上面已经阐述到 SimpleExecutor 获取预编译 Statement 对象,下面继续看看参数化的过程,下面会继续交由 RoutingStatementHandlerStatement 对象参数化处理
public class SimpleExecutor extends BaseExecutor {
  ...
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    ...
    // 3. 参数化处理
    handler.parameterize(stmt);
    // 4. 返回执行前最后准备好的Statement对象
    return stmt;
  }
}
  1. RoutingStatementHandler 委派 PreparedStatementHandler 来执行参数设置,内部调用的是 ParameterHandler 来处理参数化
public class PreparedStatementHandler extends BaseStatementHandler {
  
  protected final ParameterHandler parameterHandler;
  ...
  @Override
  public void parameterize(Statement statement) throws SQLException {
    parameterHandler.setParameters((PreparedStatement) statement);
  }
}
  1. ParameterHandler 参数化处理流程
  • 首先从 BoundSql 中,拿到参数化映射列表 ParameterMappings,这个列表是有序的,因为都是从原来的 sql 中按序提取的,然后遍历这个表
  • 拿到这个表之后,取到属性名后,进行一系列校验,包括查看 parameterObject 是不是为空,TypeHandlerRegistry 有没有对应参数的处理方法
  • ParameterMapping 拿到对应 TypeHandlerJdbcType,然后通过 TypeHandler 来参数化
  • 最后把参数化后的 Statement 对象返回到 SimpleExecutor
public class DefaultParameterHandler implements ParameterHandler {
  // 持有 typeHandler 注册器
  private final TypeHandlerRegistry typeHandlerRegistry;
  // 持有MappedStatement实例,这是一个静态的xml的一个数据库操作节点的静态信息而已
  private final MappedStatement mappedStatement;
  // 当前实际执行前的参数对象
  private final Object parameterObject;
  // 动态语言被执行后的结果 sql
  private final BoundSql boundSql;
  private final Configuration configuration;
  ...
  
  @Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    // 1. 获取boundSql中的参数映射信息列表
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      // 1.1. 遍历参数映射列表,这个列表信息就是我们xml文件中定义的某个查询语句的所有参数映射信息,注意这个List中的参数映射元素的顺序是和真实xml中sql的参数顺序对应的
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        // 1.2. 只有入参类型才会设置 PreparedStatement
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          // 取出参数名,这里比如说是'id'
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) {
          	...
           // 有没有对应的这个类型转换处理器
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            // 1.3. 这一步的工作就是从当前实际传入的参数中获取到指定key('id')的value值,比如是'15800000000'
            // 经过测试,其实就是从 map 或者 对象,拿到这个对应的值
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }

          // 2. 获取该参数对应的typeHandler
          TypeHandler typeHandler = parameterMapping.getTypeHandler();

          // 2.1. 获取该参数对应的jdbcType
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            // 3. 重点是调用每个参数对应的 typeHandler 的 setParameter 方法为该ps设置正确的参数值
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }
}
  1. 拿到参数化的 Statement 对象后,通过 RoutingStatementHandler 真正执行查询
public class SimpleExecutor extends BaseExecutor {
  ...
  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      ...
      // 2
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      ...
      // 4. 执行真正的数据库操作调用
      return handler.query(stmt, resultHandler);
    } finally {
      // 5. 关闭statement
      closeStatement(stmt);
    }
  }
}
  1. RoutingStatementHandler 再次委派 PreparedStatementHandler 执行查询。PreparedStatementHandler 拿到 PreparedStatement 后,直接执行查询。然后通过 ResultSetHandler 处理结果集,最后返回查询结果
public class PreparedStatementHandler extends BaseStatementHandler {

  protected final ResultSetHandler resultSetHandler;
  ...
  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    // 结果集处理
    return resultSetHandler.handleResultSets(ps);
  }
}
  1. 总结
    在这里插入图片描述

解析结果集

  1. 问题
  • 什么逻辑来完成的查询结果处理
  1. 继续回到 PreparedStatementHandler 查询执行,经过执行查询获得结果集之后,需要 ResultSetHandler 对查询结果进行处理
public abstract class BaseStatementHandler implements StatementHandler {
  
  protected final ResultSetHandler resultSetHandler;
  ...
  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    // 1. 结果集处理
    return resultSetHandler.handleResultSets(ps);
  }
}
  1. ResultSetHandler 实际的实现是 DefaultResultSetHandler,处理的流程如下
  • 首先对 Statement 获取第一个结果集,并且把结果集包装成 ResultSetWrapper,它会把结果集的 columnNames、classNames、jdbcTypes 属性包装到里面
  • 通过 MappedStatement 拿到 Mapper.xml 在 sql 语句中配置的、所有要映射的 ResultMap
  • 然后遍历所有的 ResultMap,根据 ResultSetWrapper 映射的所有结果集存放到 multipleResults 局部变量集合中
  • 最后返回 multipleResults,如果只有一个结果集,就从 multipleResults 取出第一个
public class DefaultResultSetHandler implements ResultSetHandler {

  private final MappedStatement mappedStatement;
  private final Configuration configuration;
  ...
  private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException {

    ResultSet rs = stmt.getResultSet();
    while (rs == null) {
    
      if (stmt.getMoreResults()) {
        rs = stmt.getResultSet();
      } else {
        if (stmt.getUpdateCount() == -1) {
          break;
        }
      }
    }
    // 包装结果集
    return rs != null ? new ResultSetWrapper(rs, configuration) : null;
  }
  
  // HANDLE RESULT SETS
  // 将 Statement 执行后产生的结果集(可能有多个结果集)映射为结果列表
  // (1)获取到 ResultSet 结果集对象  (2)获取映射关系  (3)根据映射关系封装实体

  @Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    // 创建结果容器
    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    // 1. 这里是获取第一个结果集,将传统 JDBC 的 ResultSet 包装成一个包含结果列元信息的 ResultSetWrapper 对象
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    // 2. 这里是获取所有要映射的 ResultMap(按照逗号分割出来的),例如在 Mapper.xml 配置的 <resultMap>
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    // 要映射的 ResultMap 的数量
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    // 3. 循环处理每个 ResultMap,从第一个开始处理
    while (rsw != null && resultMapCount > resultSetCount) {
      // 得到结果映射信息(取出第一个结果集)
      ResultMap resultMap = resultMaps.get(resultSetCount);
      // 4. 根据映射规则对结果集进行 pojo 转化(最后放入 multipleResults 结果集中)
      handleResultSet(rsw, resultMap, multipleResults, null);
      // 处理下个结果集
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    // 对应 <select> 标签的 resultSets 属性,一般不使用该属性,忽略
    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
       ...
    }

    // 5. 如果只有一个结果集合,则直接从多结果集中取出第一个
    return collapseSingleResultList(multipleResults);
  }

  @SuppressWarnings("unchecked")
  private List<Object> collapseSingleResultList(List<Object> multipleResults) {
    return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;
  }
}
  1. 继续深挖根据映射规则进行结果集,首先通过 ObjectFactory 初始化 DefaultResultHandler,然后对结果集进行映射,转换的结果存入 DefaultResultHandler 中,最后把结果放入 multipleResults
public class DefaultResultSetHandler implements ResultSetHandler {
  private final ResultHandler<?> resultHandler;
  private final ObjectFactory objectFactory;
  private final RowBounds rowBounds;
  ...
  private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      if (parentMapping != null) {
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          // 1. 实例化 DefaultResultHandler
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          // 2. 对结果集进行映射,转换的结果存入 defaultResultHandler
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          // 3. multipleResults 最终的结构 List<List<User>>
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          // 存储过程相关,不太需要关注
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      // issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
  }
}
  1. 接下来开始对结果集进行映射,首先判断是否有内置嵌套的结果映射,如果不是,则执行简单结果映射
public class DefaultResultSetHandler implements ResultSetHandler {
  ...
  public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    // 是否有内置嵌套的结果映射
    if (resultMap.hasNestedResultMaps()) {
      ensureNoRowBounds();
      checkResultHandler();
      // 嵌套结果映射
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      // 1. 简单结果映射,这次只看这个处理
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
  }
}
  1. 执行简单结果映射,先获取结果集信息,然后根据分页信息只提取部分数据,然后遍历 resultSet 的每一行数据转化成 POJO,再保存映射结果
public class DefaultResultSetHandler implements ResultSetHandler {
  ...
  private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    // 1. 获取结果集信息
    ResultSet resultSet = rsw.getResultSet();
    // 2. 根据分页信息,提取相应数据
    skipRows(resultSet, rowBounds);
    /*
       3. 处理和赋值多条记录
     */
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
      // 通过<resultMap>标签的子标签<discriminator>对结果映射进行鉴别
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      // 4. 将查询结果封装到 POJO 中,关键位置
      Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
      // 5. 保存映射结果
      storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
  }
}
  1. 执行查询结果封装到 POJO。首先通过 ObjectFactory 创建结果映射的 PO 类对象,然后通过 Configuration 创建 MetaObject 对象,并把 PO 类(即代码里的 rowValue,其实相当于给 PO 多加了点元信息)存到里面去,然后根据 resultMap 结果映射、ResultSetWrapper 封装好的结果集,把值映射到创建到的 PO 类里面去。这三者的关系可以看作 ResultSetWrapper 是 JDBC 对象值,resultMap 是 JDBC 与 Java PO 属性关系映射表,MetaObject 包含元信息的 PO 类对象
public class DefaultResultSetHandler implements ResultSetHandler {
  ...
  private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    // 延迟加载的映射信息
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    // 1. 根据 resultType 的值创建要映射的 PO 类对象
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      // 2. 获取 MetaObject 对象,为后面赋值做准备,也把 rowValue 封进去了
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      boolean foundValues = this.useConstructorMappings;
      // 3. 是否应用自动映射,也就是通过 resultType 进行映射
      if (shouldApplyAutomaticMappings(resultMap, false)) {
        // 4. 根据 columnName 和 type 属性名映射赋值,存到 rowValue 中
        foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
      }
      // 根据我们配置 ResultMap 的 column 和 property 映射赋值
      // 如果映射存在 nestedQueryId,会调用 getNestedQueryMappingValue 方法获取返回值
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
      foundValues = lazyLoader.size() > 0 || foundValues;
      rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    // 5. 返回结果
    return rowValue;
  }
}
  • 创建对象过程其实就是委派给 ObjectFactory 来创建的,也就是上面第 1 步
public class DefaultResultSetHandler implements ResultSetHandler {
  ...
  private final ObjectFactory objectFactory;
  ...
  private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    this.useConstructorMappings = false; // reset previous mapping result
    final List<Class<?>> constructorArgTypes = new ArrayList<>();
    final List<Object> constructorArgs = new ArrayList<>();
    // 1. 创建结果映射的PO类对象
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      // 获取要映射的 PO 类的属性信息
      final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
      for (ResultMapping propertyMapping : propertyMappings) {
        // issue gcode #109 && issue #149
        // 延迟加载处理
        if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
          // 通过动态代理工厂,创建延迟加载的代理对象
          resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
          break;
        }
      }
    }
    this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty();
    // 2. 返回结果
    return resultObject;
  }
  
  private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)
      throws SQLException {
    final Class<?> resultType = resultMap.getType();
    final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
    final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
    if (hasTypeHandlerForResultObject(rsw, resultType)) {
		...
    } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
      // 1. 对象工厂创建对象
      return objectFactory.create(resultType);
    } else if (shouldApplyAutomaticMappings(resultMap, false)) {
      return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs);
    }
    ...
  }
}
  • 由于因为这次测试源码的 case 在 Mapper.xml 中定义的 sql 的返回映射属性是 resultType 所以 shouldApplyAutomaticMappings() 返回 true,所以会通过 applyAutomaticMappings 执行 columnName 和 type 属性名映射赋值,存到 rowValue
    • 首先会通过 createAutomaticMappings() 得到属性映射列表 在这里插入图片描述
    • 然后遍历这个属性映射列表,通过 TypeHandlerResultSet 拿出属性值
    • 最后通过 MetaObject 将属性值设置到 rowValue
public class DefaultResultSetHandler implements ResultSetHandler {
  ...
  private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
    // 1. 创建属性映射表
    List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
    boolean foundValues = false;
    if (!autoMapping.isEmpty()) {
      // 2. 遍历属性映射表
      for (UnMappedColumnAutoMapping mapping : autoMapping) {
        // 3. 从 ResultSet 拿到值
        final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
        if (value != null) {
          foundValues = true;
        }
        if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
          // 4. 设置到 rowValue 中
          metaObject.setValue(mapping.property, value);
        }
      }
    }
    return foundValues;
  }
}
  1. 得到解析后的结果,返回给 DefaultResultSetHandler 之后,就可以把值映射值拿到,得到最终结果
  2. 总结
    在这里插入图片描述

SqlSession 关闭

  1. 通过 Sqlsession 拿到结果之后,执行完业务流程,就会将 Sqlsession 进行 close()
public class MybatisTest {

  @Test
  public void test1() throws IOException {
    ...
    // 4. 关闭 会话
    sqlSession.close();
  }

}
  1. Sqlsession 关闭,其实就是对 Executor 进行一个关闭清理处理
public class DefaultSqlSession implements SqlSession {

  private final Executor executor;
  ...
  @Override
  public void close() {
    try {
      // 1. 执行关闭
      executor.close(isCommitOrRollbackRequired(false));
      closeCursors();
      dirty = false;
    } finally {
      ErrorContext.instance().reset();
    }
  }
}
  1. 然后 CachingExecutor 会对事务管理器进行最后的数据 commit 或者 rollback,然后关闭会话
public class CachingExecutor implements Executor {
  // 1. 这里还是 SimpleExecutor
  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
  ...
  @Override
  public void close(boolean forceRollback) {
    try {
      // issues #499, #524 and #573
      if (forceRollback) {
        tcm.rollback();
      } else {
        tcm.commit();
      }
    } finally {
      delegate.close(forceRollback);
    }
  }
}

完整流程总结

在这里插入图片描述

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

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

相关文章

ceph分布式存储

1、存储基础 //单机存储设备 ●DAS&#xff08;直接附加存储&#xff0c;是直接接到计算机的主板总线上去的存储&#xff09; IDE、SATA、SCSI、SAS、USB 接口的磁盘 所谓接口就是一种存储设备驱动下的磁盘设备&#xff0c;提供块级别的存储 ●NAS&#xff08;网络附加存储&am…

LCHub 6 月低代码平台排行榜发布

LCHub低代码平台排行榜 2023 国产低代码名录和产品信息一览 2023国产低代码平台排行榜 低代码最新视频课程 最新解读报告:2023年6月低代码平台排行榜:维格表 伙伴云上升最快 共有120个低代码平台参与排名, 点击查看排名规则更新 TOP 10 低代码平台 6月 LCHub 指数走势

【linux基础15】用户管理

文章目录 一. 用户和组1. 用户和组介绍用户分类UIDGID 2. /etc/passwd和/etc/shadow用户信息文件&#xff1a;密码文件&#xff1a; 二、linux账号管理1. 用户操作1.1. 新增用户1.2. 指定UID、添加所属组、执行家目录1.3. 设置密码&#xff1a;passwd1.4 修改用户家目录&#x…

安卓大作业 图书管理APP

系列文章 安卓大作业 图书管理APP 文章目录 系列文章1&#xff0e;背景2&#xff0e;功能3. 源代码获取 1&#xff0e;背景 本次实验设计的是一个图书管理系统&#xff0c;系统的整体目录如下&#xff1a; 2&#xff0e;功能 针对于每个java类或者Activity进行说明&#x…

春招后,功能测试还能找到工作了吗?

在一线大厂&#xff0c;没有测试这个岗位&#xff0c;只有测开这个岗位。这几年&#xff0c;各互联网大厂技术高速更新迭代&#xff0c;软件测试行业也正处于转型期。传统的功能测试技术逐步淘汰&#xff0c;各种新的测试技术层出不穷&#xff0c;测试人员的薪资也水涨船高。与…

网络安全从业人员会被AI智能取代吗?

随着ChatGPT的火爆&#xff0c;很多人开始担心网络安全从业人员会被AI取代。如果说网络安全挖洞的话&#xff0c;AI可能真的能取代。但是网络安全不仅仅只是挖洞&#xff0c;所以AI只是能缓解网络安全人员不足的情况&#xff0c;但是是不会取代人类的作用的。 就拿最近很火的C…

【详解】Java中的queue和deque、ArrayDeque

一 、队列(queue)简述 队列(queue)是一种常用的数据结构&#xff0c;在Java里面Queue是一个接口&#xff0c;它只是定义了一个基本的Queue应该有哪些功能规约。可以将队列看做是一种特殊的线性表&#xff0c;该结构遵循的先进先出原则。 Java中&#xff0c;LinkedList实现了Q…

RabbitMQ入门案例之发布订阅模式

前言 本文章主要介绍RabbitMQ的发布订阅模式&#xff0c;该模式下&#xff0c;消息为广播形式&#xff0c;一经发布则会进入交换机绑定的队列中&#xff0c;详细介绍可以阅读官方文档。 官网文档地址&#xff1a;https://rabbitmq.com/getstarted.html 什么是发布与订阅模式 …

对比K近邻算法与决策树算法在MNIST数据集上的分类性能

目录 1. 作者介绍2. K近邻算法与决策树算法介绍2.1 K近邻&#xff08;KNN&#xff09;简介2.2 决策树算法简介2.3 MNIST数据集简介&#xff1a; 3. K近邻算法和决策树算法在Mnist数据集分类实验对比3.1 K近邻算法对Mnist数据集分类实验3.2 K近邻代码实现3.3 决策树算法实验3.4 …

Vue3:组件高级(上)

Vue3&#xff1a;组件高级&#xff08;上&#xff09; Date: May 20, 2023 Sum: watch倾听器、组件的生命周期、组件之间的数据共享、vue3.x中全局配置axios 目标&#xff1a; 能够掌握 watch 侦听器的基本使用 能够知道 vue 中常用的生命周期函数 能够知道如何实现组件之间…

写 bug 速度提升200%!吊爆的 IDEA 使用技巧

背景 Java 开发过程经常需要编写有固定格式的代码&#xff0c;例如说声明一个私有变量&#xff0c;logger或者bean等等。 对于这种小范围的代码生成&#xff0c;我们可以利用 IDEA 提供的 Live Templates功能。 刚开始觉得它只是一个简单的Code Snippet&#xff0c;后来发现…

msf渗透练习-震网三代

说明&#xff1a; 本章内容&#xff0c;仅供学习&#xff0c;不要用于非法用途&#xff08;做个好白帽&#xff09; &#xff08;一&#xff09;震网三代漏洞 “震网三代”官方漏洞编号是CVE-2017-8464&#xff0c;2017年6月13日&#xff0c;微软官方发布编号为CVE-2017-8464的…

Redis Cluster集群运维-03

1、Redis集群方案比较 哨兵模式 在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态&#xff0c;如果master节点异 常&#xff0c;则会做主从切换&#xff0c;将某一台slave作为master&#xff0c;哨兵的配置略微复杂&#xff0c;并且性能和高可…

【CSS】常见的选择器

1.标签选择器 语法 标签 { }作用 标签选择器用于选择某种标签比如 选择p标签&#xff0c;并设置背景颜色 p { background-color:yellow; }例子 选择div标签&#xff0c;并将其字体大小设置为100px&#xff0c;字体设置为"微软雅黑"&#xff0c;文字颜色设置为r…

怎么学习渗透测试?路线是什么

我知道很多人肯定觉得&#xff0c;报班什么的太贵了&#xff0c;但是人家贵有贵的道理 owap讲来讲去&#xff0c;还是那个样&#xff0c;但是有人给你解答问题是两个概念&#xff0c;有时候一个虚拟机都能卡死你很久&#xff0c;我就随便说说&#xff0c;我给你们想的学习路线…

2023网络安全工程师面试题汇总(附答案)

又到了毕业季&#xff0c;大四的漂亮学姐即将下架&#xff0c;大一的小学妹还在来的路上&#xff0c;每逢这时候我心中总是有些小惆怅和小激动…… 作为学长&#xff0c;还是要给这些马上要初出茅庐的学弟学妹们&#xff0c;说说走出校园、走向职场要注意哪些方面。 走出校园后…

基于XC7Z100的PCIe采集卡(GMSL FMC采集卡)

GMSL 图像采集卡 特性 ● PCIe Gen2.0 X8 总线&#xff1b; ● 支持V4L2调用&#xff1b; ● 1路CAN接口&#xff1b; ● 6路/12路 GMSL1/2摄像头输入&#xff0c;最高可达8MP&#xff1b; ● 2路可定义相机同步触发输入/输出&#xff1b; 优势 ● 采用PCIe主卡与FMC子…

服务器是什么?它是用来干什么的?

作者&#xff1a;Insist-- 个人主页&#xff1a;insist--个人主页 作者会持续更新网络知识和python基础知识&#xff0c;期待你的关注 目录 一、服务器是什么&#xff1f; 二、服务器的作用 1、提高访问速度 2、提高安全性 三、云服务器与物理服务器 1、云服务器 云服务…

[架构之路-210]- 人人都是产品经理 - 互联网产品需求分析思路和方法笔记

目录 前言&#xff1a; 一、产品需求分析思路和方法--产品需求 1、产品需求的内涵 ①什么是产品&#xff1f; ②什么是需求&#xff1f; ③需求的产品的关系 ④案例分析&#xff1a; ⑤理解需求的误区 2、需求的分类及层次、规律、拆解用户需求 ①需求分类 ②需求层…

算法刷题-链表-设计链表

设计链表 707.设计链表思路代码其他语言版本 听说这道题目把链表常见的五个操作都覆盖了&#xff1f; 707.设计链表 力扣题目链接 题意&#xff1a; 在链表类中实现这些功能&#xff1a; get(index)&#xff1a;获取链表中第 index 个节点的值。如果索引无效&#xff0c;则…