Mybatis源码分析(十三)Mybatis的四大组件

P2110011.JPG
系列文章:

文章状态时间描述
(一)Mybatis 基本使用已复习2022-12-14对Mybtais的基本使用,能够开发
(二)Mybatis-config.xml的初始化已复习2023-02-10对我们编写的mybatis配置文件的解析
(三)SqlSessionFactory的初始化已复习2023-02-11SqlSession会话工厂的初始化
(四)Mapper文件的解析已复习2023-02-12主要对我们编写的Mapper.xml进行解析
(五)SqlSession的创建已复习2023-02-13主要介绍构建DefaultSqlSessionFactory
(六)Mapper的接口代理已复习2023-02-14如何通过动态代理来执行我们编写的方法
(七)MapperMethod的INSERT分析已复习2023-02-15通过代理对象来执行Insert语句,返回结果
(八)MapperMethod的Select分析已复习2023-02-16通过代理对象来执行Select语句,返回结果
(九)Mybatis的PreparedStatement已复习2023-02-17预处理语句的常见,以及与数据库打交道
(十)Mybatis的结果隐射已复习2023-02-18数据库结果与实体类对象的转换
(十一)Mybatis的一级缓存与二级缓存已复习2023-02-24Mybatis中一级缓存与二级缓存
(十二)Mybatis的插件开发及原理分析已复习2023-02-25Mybatis中的插件运行机制与开发
(十三)Mybatis中的四大组件梳理计划中Mybatis中的四大组件的梳理
(十四)Mybatis中的设计模式梳理计划中Mybatis中设计模式的整理
(十五)Spring-Mybatis整理计划中Spring与Mybatis整合
(十六)Mybatis疑惑总结计划中我遇到的疑惑与问题

前面我们基本的流程都已经介绍完毕,下面我们来梳理一下前面的知识,加深自己的理解,以及重复记忆

下面我们以代码和原理的方式来解读这些组件,首先我们来介绍Mybatis中重要组件执行器Executor

一 Executor

7fb19d8cb61494b4b17d6f74c8dd5611_webp.webp
Executor是Mybatis执行者接口,他包含的功能有:
基本功能:改、查,没有增删是因为所有的增删操作都可以归结为改。
缓存维护:包括创建缓存Key、清理缓存、判断缓存是否存在。
事务管理:提交、回滚、关闭、批处理刷新。
Executor有6个实现类,这里先介绍三个重要的实现子类。分别是:SimpleExecutor(简单执行器)、ReuseExecutor(重用执行器)、BatchExecutor(批处理执行器)。
首先我们来看看类结果图
image.png

💯💯接口方法


  // 数据更新操作,其中数据的增加、删除、更新均可由该方法实现
  int update(MappedStatement ms, Object parameter) throws SQLException;
  // 数据查询操作,返回结果为列表形式
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
  // 数据查询操作,返回结果为列表形式
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
  // 数据查询操作,返回结果为游标形式
  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
  // 清理缓存
  List<BatchResult> flushStatements() throws SQLException;
  // 提交事务
  void commit(boolean required) throws SQLException;
  // 回滚事务
  void rollback(boolean required) throws SQLException;
  // 创建当前查询的缓存键值
  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
  // 本地缓存是否有指定值
  boolean isCached(MappedStatement ms, CacheKey key);
  // 清理本地缓存
  void clearLocalCache();
  // 懒加载
  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
  // 获取事务
  Transaction getTransaction();
  // 关闭执行器
  void close(boolean forceRollback);
  // 判断执行器是否关闭
  boolean isClosed();
  // 设置执行器包装
  void setExecutorWrapper(Executor executor);

下面我们从案例入门来讲解几种不同的执行器,首先我们完成基本信息的初始化

📌📌案例

测试类中的初始化操作,完成配置文件的解析,构建会话工厂等等信息

    SqlSessionFactory sqlSessionFactory;
    Configuration configuration;
    JdbcTransaction jdbcTransaction;
    MappedStatement mappedStatement;


    @BeforeEach
    public void init() {
        // 1.获取构建器
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        // 2.获取配置文件的流信息
        InputStream resourceAsStream = MybatisDemo02ApplicationTests.class.getResourceAsStream("/mybatis-config.xml");
        // 3.解析XML 并构造会话工厂
         sqlSessionFactory = factoryBuilder.build(resourceAsStream);
        // 4.获取工厂配置
         configuration = sqlSessionFactory.getConfiguration();
        // 5.构建jdbc事务
         jdbcTransaction = new JdbcTransaction(sqlSessionFactory.openSession().getConnection());
        // 6.获取Mapper映射
         mappedStatement = configuration.getMappedStatement("com.shu.UserMapper.queryById");
    }

在单元测试之中 @BeforeEach,完成对SqlSessionFactoryBuilder,Configuration,JdbcTransaction,MappedStatement的初始化,下面我们只关注于执行器的变化,使用方式,以及比较的不同

1.1 SimpleExecutor

Mybatis默认的执行器,它每处理一次会话当中的sql请求都会通过StatementHandler构建一个新的statment。这个是默认的执行器,很简单,首先让我们来开启日志打印,可以详细的看到不同执行器的执行情况

👳👳配置

配置文件中开启日志打印

<settings>
        <!-- 打印sql日志 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>

👀👀测试代码

	@Test//简单执行器
    public void simpleTest() throws SQLException {
        // 创建一个简单的执行器
        SimpleExecutor simpleExecutor = new SimpleExecutor(configuration, jdbcTransaction);
        log.info("执行器类型:{}", simpleExecutor.getClass().getName());
        // 执行查询
        List<Object> query = simpleExecutor.doQuery(mappedStatement, 1, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, mappedStatement.getBoundSql(1));
        log.info("查询结果:{}", query);
    }

image.png
我们可以看到执行结果的返回

🚀🚀分析

前面的初始化流程,以及会话工厂的创建,请参考前面的内容,我们这里详细解释执行器,首先我们来看看SimpleExecutor的创建
SimpleExecutor

// 构造器
public SimpleExecutor(Configuration configuration, Transaction transaction) {
    super(configuration, transaction);
  }

我们可以看到他调用了父类的方法,因此我们来看看他的父类BaseExecutor
BaseExecutor

 protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<>();
     // 初始化本地缓存,实际上内部维护了一个HashMap
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }

下面调用具体的调用方法,doQuery,下面我们只介绍相关步骤,具体的流程前参考前面的文章
SimpleExecutor

 @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();
      // 创建一个隐射处理器  
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 参数预处理  
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 具体执行  
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

有兴趣的话,自己去通过源码调试获取结果,或者参考前面的文章
image.png

🐬🐬总结

SimpleExecutor 继承了 BaseExecutor 抽象类 它是最简单的 Executor 接口实现。Executor 使用了模板方法模式,一级缓存等固定不变的操作都封装到了 BaseExecutor 中,在SimpleExecutor 中就不必再关系一级缓存等操作,只需要专注实现4个基本方法的实现即可。

1.2 ReuseExecutor

可重用执行器,内部维护了一个statementMap来记录我们的执行语句,来减少语句的预编译,直观的效果就是,同一次查询,sql预编译了一次,减少Sql的预编译,在一定程度上提高了效率

👀👀测试代码

    @Test//重用执行器
    public void reuseTest() throws SQLException {
        // 创建一个重用执行器
        ReuseExecutor reuseExecutor = new ReuseExecutor(configuration, jdbcTransaction);
        log.info("执行器类型:{}", reuseExecutor.getClass().getName());
        // 执行查询
        List<Object> query = reuseExecutor.doQuery(mappedStatement, 1, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, mappedStatement.getBoundSql(1));
        log.info("查询结果:{}", query);
        // 执行查询
        List<Object> query2 = reuseExecutor.doQuery(mappedStatement, 1, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, mappedStatement.getBoundSql(1));
        log.info("查询结果:{}", query2);
    }

仔细观察结果,我们可以发现它的Sql预编译了一次
image.png
我们来看看为啥只执行了一次,他的原因是啥?

🚀🚀分析

ReuseExecutor

// 缓存的编译Sql
private final Map<String, Statement> statementMap = new HashMap<>();

// 预处理参数
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    // 查询下缓存中是否存在,Sql语句作为Key,查询是否以及与编译过了  
    if (hasStatementFor(sql)) {
       // 获取一编译语句  
      stmt = getStatement(sql);
        // 更新查询超时以应用事务超时。
      applyTransactionTimeout(stmt);
    } else {
      Connection connection = getConnection(statementLog);
      stmt = handler.prepare(connection, transaction.getTimeout());
      // 添加到缓存  
      putStatement(sql, stmt);
    }
    handler.parameterize(stmt);
    return stmt;
  }



  private void putStatement(String sql, Statement stmt) {
    statementMap.put(sql, stmt);
  }

简单来说:就是内部维护了一个HashMap作为缓存,每次先去缓存中查询一下是否存在,存在就直接返回,没有的话,就在执行预编译,好了再次缓存,以便下次使用

🚀🚀总结

重用执行器,相较于 SimpleExecutor 多了 Statement 的缓存功能,其内部维护一个 Map<String, Statement>,每次编译完成的 Statement 都会进行缓存,不会关闭

1.3 BatchExecutor

首先需要明确一点 BachExecutor 是基于 JDBC 的 addBatch、executeBatch 功能的执行器,所以 BachExecutor 只能用于更新(insert|delete|update),不能用于查询(select)

👀👀测试代码

    @Test//批量执行器
    public void batchTest() throws SQLException {
        // 创建一个批量执行器
        BatchExecutor batchExecutor = new BatchExecutor(configuration, jdbcTransaction);
        log.info("执行器类型:{}", batchExecutor.getClass().getName());
        // 执行插入
        for (int i = 0; i < 10; i++) {
            int update = batchExecutor.doUpdate(mappedStatement, new User(1, "张三", "18", 1 + i, 2, "123456"));
        }
    	// 刷新批处理
        batchExecutor.doFlushStatements(true);
    }

image.png
我们来看看的他的执行原理,批量执行,其实依赖于JDBC 的 addBatch、executeBatch
BatchExecutor

public class BatchExecutor extends BaseExecutor {
 
  public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;
 
  /* Statement链表**/
  private final List<Statement> statementList = new ArrayList<Statement>();
  
  /* batch结果链表**/
  private final List<BatchResult> batchResultList = new ArrayList<BatchResult>();
  private String currentSql;
  private MappedStatement currentStatement;
 
  public BatchExecutor(Configuration configuration, Transaction transaction) {
    super(configuration, transaction);
  }

 // 更新方法
  @Override
  public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
	//获得配置信息
    final Configuration configuration = ms.getConfiguration();
	//获得StatementHandler
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
    final BoundSql boundSql = handler.getBoundSql();
	//获得Sql语句
    final String sql = boundSql.getSql();
    final Statement stmt;
	//如果sql语句等于当前sql MappedStatement 等于当前Map碰到Statement
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
	  
      int last = statementList.size() - 1;
	  //获得最后一个
      stmt = statementList.get(last);
	  handler.parameterize(stmt);//fix Issues 322
	  //有相同的MappedStatement和参数
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
	  //如果不存在就创建一个批处理操作
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection);
      handler.parameterize(stmt);    //fix Issues 322
      currentSql = sql;
      currentStatement = ms;
	  //添加批量处理操作
      statementList.add(stmt);
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
  // handler.parameterize(stmt);
    //最终是调用jdbc的批处理操作
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
  }
 
  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException {
    Statement stmt = null;
    try {
      flushStatements();
	  //获得配置信息
      Configuration configuration = ms.getConfiguration();
	  //获得StatementHandler
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
	  //获得连接
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection);
	  //获得Statement
      handler.parameterize(stmt);
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
 
  /* 刷新Statement,记录执行次数*/
  @Override
  public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
      List<BatchResult> results = new ArrayList<BatchResult>();
      if (isRollback) {
        return Collections.emptyList();
      }
	  //如果进行了批量处理
      for (int i = 0, n = statementList.size(); i < n; i++) {
        Statement stmt = statementList.get(i);
        BatchResult batchResult = batchResultList.get(i);
        try {
		  //记录批量处理执行操作的条数
          batchResult.setUpdateCounts(stmt.executeBatch());
          MappedStatement ms = batchResult.getMappedStatement();
		  //参数对象集合
          List<Object> parameterObjects = batchResult.getParameterObjects();
		  //生成key
          KeyGenerator keyGenerator = ms.getKeyGenerator();
          if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
            Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
            jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
          } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
            for (Object parameter : parameterObjects) {
              keyGenerator.processAfter(this, ms, stmt, parameter);
            }
          }
        } catch (BatchUpdateException e) {
          StringBuilder message = new StringBuilder();
          message.append(batchResult.getMappedStatement().getId())
              .append(" (batch index #")
              .append(i + 1)
              .append(")")
              .append(" failed.");
          if (i > 0) {
            message.append(" ")
                .append(i)
                .append(" prior sub executor(s) completed successfully, but will be rolled back.");
          }
          throw new BatchExecutorException(message.toString(), e, results, batchResult);
        }
		//记录操作
        results.add(batchResult);
      }
      return results;
    } finally {
      for (Statement stmt : statementList) {
        closeStatement(stmt);
      }
      currentSql = null;
      statementList.clear();
      batchResultList.clear();
    }
  }
 
}

🚀🚀总结

BatchExecutor 的批处理添加过程相当于添加了一个没有返回值的异步任务,那么在什么时候执行异步任务,将数据更新到数据库呢,答案是处理 update 的任何操作,包括 select、commit、close等任何操作,具体执行的方法就是 doFlushStatements此外需要注意的是 Batch 方式插入使用 useGeneratedKeys 获取主键,在提交完任务之后,并不能马上取到,因为此时 sql 语句还在缓存中没有真正执行,当执行完 Flush 之后,会通过回调的方式反射设置主键

1.4 效率对比

几种执行器效率对比

数据量batchReusersimpleforeachforeach100
1003691481516870
1000485735911679148
10000274540644666386071002
50000883817788199077964443703

从上面的结果对比可以看到:

  • 整体而言 reuser 比 simple 多了缓存功能,所以无论批处理的大小,其效率都要高一些。
  • 此外在批处理量小的时候使用 foreach,效果还是可以的,但是当批量交大时,sql 编译的时间就大大增加了,当 foreach 固定批大小 + reuser 时,每次的 Statement 就可以重用,从表中也可以看到效率也时最高的。
  • batch 的优点则是所有的更新语句都能用。
  • 所以在配置的时候建议默认使用 reuser,而使用 foreach 和 batch 需要根据具体场景分析,如果更新比较多的时候,可以在批量更新的时候单独指定 ExecutorType.BATCH,如果批量插入很多的时候,可以固定批大小。

1.5 BaseExecutor

首先Mybatis默认开启一级缓存,其次,执行器的设计分层遵循了软件设计的 单一职责 原则。BaseExecutor 只管理一级缓存,而具体的数据库交互逻辑,是交由更低层的三个执行器处理的(Simple/Reuse/Batch)。
BaseExecutor

  protected BaseExecutor(Configuration configuration, Transaction transaction) {
    // 事物对象
    this.transaction = transaction;
    // 延迟加载队列
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    // 一级缓存
    this.localCache = new PerpetualCache("LocalCache");
    // 本地输出参数缓存
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    // 执行器状态标识对象
    this.closed = false;
    // mybatis 配置对象
    this.configuration = configuration;
    this.wrapper = this;
  }

我们可以通过源码可以发现,PerpetualCache实际上对HashMap的封装

public class PerpetualCache implements Cache {
  // Cache的id,一般为所在的namespace
  private final String id;
  // 用来存储要缓存的信息
  private Map<Object, Object> cache = new HashMap<>();

  public PerpetualCache(String id) {
    this.id = id;
  }
}

我们来看看查询方法,根据Sql,参数等信息,生成缓存Key在本地缓存中查询是否存在,如果命中,直接返回对象,没有的话,进行数据库查询,将结果写入到缓存中


    /**
   * 更新数据库数据,INSERT/UPDATE/DELETE三种操作都会调用该方法
   * @param ms 映射语句
   * @param parameter 参数对象
   * @return 数据库操作结果
   * @throws SQLException
   */
  @Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource())
            .activity("executing an update").object(ms.getId());
    if (closed) {
      // 执行器已经关闭
      throw new ExecutorException("Executor was closed.");
    }
    // 清理本地缓存
    clearLocalCache();
    // 返回调用子类进行操作
    return doUpdate(ms, parameter);
  }


	/**
   * 执行查询操作
   * @param ms 映射语句对象
   * @param parameter 参数对象
   * @param rowBounds 翻页限制
   * @param resultHandler 结果处理器
   * @param <E> 输出结果类型
   * @return 查询结果
   * @throws SQLException
   */
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 生成缓存的键
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }

  /**
   * 查询数据库中的数据
   * @param ms 映射语句
   * @param parameter 参数对象
   * @param rowBounds 翻页限制条件
   * @param resultHandler 结果处理器
   * @param key 缓存的键
   * @param boundSql 查询语句
   * @param <E> 结果类型
   * @return 结果列表
   * @throws SQLException
   */
  @SuppressWarnings("unchecked")
  @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.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) { // 新的查询栈且要求清除缓存
      // 清除一级缓存
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // 尝试从本地缓存获取结果
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        // 本地缓存中有结果,则对于CALLABLE语句还需要绑定到IN/INOUT参数上
        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();
      }
      deferredLoads.clear();
      // 如果本地缓存的作用域为STATEMENT,则立刻清除本地缓存
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache();
      }
    }
    return list;
  }

详细的内容,前参考前面的一级缓存与二级缓存

🚀🚀总结

  • BaseExecutor 处于 mybatis 执行器体系的中间层。其设计与实现遵循了 单一职责 原则。具体体现在 baseExecutor 主要聚焦在处理一级缓存的逻辑上,而与数据库交互的具体实现依赖于另外三个底层执行器(simple/reuse/batch)。
  • 一级缓存默认是开启的,作用域默认会话层级(session)。任意更新操作都会清空一级缓存中的所有数据。一级缓存有一个 mybatis 的自定义实现类
  • 影响一级缓存命中的因素有namespaceid,mybatis 分页参数 limit/offset,sql,sql 入参,mybatis 环境配置参数

1.6 CachingExecutor

CachingExecutor的成员变量有个Executor实例,这显然是个装饰器模式,这的类就是在其他Executor实例的方法进行了flushCacheIfRequired(),也就是刷新缓存,所以这个类在其他类上添加了缓存的功能,从query()方法中也能看出先查找缓存,缓存没有再进行调用Executor实例的query()进行数据的查询,首先我们是否开启二级缓存,如果开启了二级缓存,首先会从二级缓存中获取数据

  <cache/>

CachingExecutor


  /**
   * 更新数据库数据,INSERT/UPDATE/DELETE三种操作都会调用该方法
   * @param ms 映射语句
   * @param parameterObject 参数对象
   * @return 数据库操作结果
   * @throws SQLException
   */
  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    // 根据要求判断语句执行前是否要清除二级缓存,如果需要,清除二级缓存
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  @Override
  public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.queryCursor(ms, parameter, rowBounds);
  }

  /**
   * 查询数据库中的数据
   * @param ms 映射语句
   * @param parameterObject 参数对象
   * @param rowBounds 翻页限制条件
   * @param resultHandler 结果处理器
   * @param key 缓存的键
   * @param boundSql 查询语句
   * @param <E> 结果类型
   * @return 结果列表
   * @throws SQLException
   */
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // 获取MappedStatement对应的缓存,可能的结果有:该命名空间的缓存、共享的其它命名空间的缓存、无缓存
    Cache cache = ms.getCache();
    // 如果映射文件未设置<cache>或<cache-ref>则,此处cache变量为null
    if (cache != null) { // 存在缓存
      // 根据要求判断语句执行前是否要清除二级缓存,如果需要,清除二级缓存
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) { // 该语句使用缓存且没有输出结果处理器
        // 二级缓存不支持含有输出参数的CALLABLE语句,故在这里进行判断
        ensureNoOutParams(ms, boundSql);
        // 从缓存中读取结果
        @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;
      }
    }
    // 交由被包装的实际执行器执行
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

TransactionalCacheManager

public class TransactionalCacheManager {

  // 管理多个缓存的映射
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
}

TransactionalCache

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);

  // 被装饰的对象
  private final Cache delegate;
  // 事务提交后是否直接清理缓存
  private boolean clearOnCommit;
  // 事务提交时需要写入缓存的数据
  private final Map<Object, Object> entriesToAddOnCommit;
  // 缓存查询未命中的数据
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
  }
}

实际上二级缓存的实现调用了事务管理器的TransactionalCacheManager进行管理,从源码上看实际上维护了一个TransactionalCache中的HashMap来管理。具体的内容请参考前面的二级缓存

🚀🚀总结

存储二级缓存对象的时候是放到TransactionalCache.entriesToAddOnCommit这个map中,但是每次查询的时候是直接从TransactionalCache.delegate中去查询的,所以这个二级缓存查询数据库后,设置缓存值是没有立刻生效的,主要是因为直接存到 delegate 会导致脏数据问题。

上一篇文章我们梳理了执行器的基本原理,下面我们来看看ParameterHandler处理器的基本作用,StatementHandler 是四大组件中最重要的一个对象,负责操作 Statement 对象与数据库进行交流,在工作时还会使用 ParameterHandler 和 ResultSetHandler 对参数进行映射,对结果进行实体类的绑定

二 StatementHandler

首先我们来看看类图:
image.png
我们再来看看他的接口方法:
StatementHandler

public interface StatementHandler {

  // 从Connection中创建一个Statement
  Statement prepare(Connection connection, Integer transactionTimeout)
      throws SQLException;

  // 为Statement绑定实参
  void parameterize(Statement statement)
      throws SQLException;

  // 批量执行操作
  void batch(Statement statement)
      throws SQLException;

  // 执行增、删、改操作
  int update(Statement statement)
      throws SQLException;

  // 执行查找操作,返回list
  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  // 执行查询操作,返回迭代游标
  <E> Cursor<E> queryCursor(Statement statement)
      throws SQLException;

  // 获取BoundSql对象
  BoundSql getBoundSql();

  // 获取参数处理器
  ParameterHandler getParameterHandler();

}

根据上面的类图结果,我们可以发现他的结构体系与Exector相似,分别有两个实现类 BaseStatementHandler 和 RoutingStatementHandler,BaseStatementHandler 有三个实现类, 他们分别是 SimpleStatementHandler、PreparedStatementHandler 和 CallableStatementHandler。

  • RoutingStatementHandler: RoutingStatementHandler 并没有对 Statement 对象进行使用,只是根据StatementType 来创建一个代理,代理的就是对应Handler的三种实现类。在MyBatis工作时,使用的StatementHandler 接口对象实际上就是 RoutingStatementHandler 对象。
  • BaseStatementHandler: 是 StatementHandler 接口的另一个实现类.本身是一个抽象类.用于简化StatementHandler 接口实现的难度,属于适配器设计模式体现,它主要有三个实现类
  • SimpleStatementHandler: 管理 Statement 对象并向数据库中推送不需要预编译的SQL语句。
  • PreparedStatementHandler: 管理 Statement 对象并向数据中推送需要预编译的SQL语句。
  • CallableStatementHandler:管理 Statement 对象并调用数据库中的存储过程。

2.1 RoutingStatementHandler

让我们回到当调用执行器的DoUpdate方法,通过Configuration来创建一个隐射语句处理器关键代码:
SimpleExecutor


  @Override
  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
       // 获取解析好的配置文件 
      Configuration configuration = ms.getConfiguration();
        // 获取一个处理器对象
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
       // 调用处理器的方法返回一个 Statement
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.update(stmt);
    } finally {
      closeStatement(stmt);
    }
  }

好的我们看到了关键代码,我们下面看看:
Configuration

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
       //  策越模式
      StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
      //  插件
      statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

RoutingStatementHandler

  // 根据语句类型选取出的被代理类的对象
  private final StatementHandler delegate;

  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    // 根据语句类型选择被代理对象
    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());
    }
  }

然后根据我们配置文件中的类型,来创建不同的对象返回,当然默认值为PREPARED一个需要预编译的Sql

🌈🌈总结

RoutingStatementHandler的作用十分简单,充当一个分发的角色,有不同的类型返回不对的对象,来执行

2.2 BaseStatementHandler

要定义了从Connection中获取Statement的方法,而对于具体的Statement操作则未定义,主要采用适配器模式来实现,下面我们来看看调用过程吧,首先是构造器实例化
在这其中初始化了后面的两大重要组件:ParameterHandler与ResultSetHandler,具体的实现我们后面再看
BaseStatementHandler

  protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    this.configuration = mappedStatement.getConfiguration();
    this.executor = executor;
    this.mappedStatement = mappedStatement;
    this.rowBounds = rowBounds;

    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.objectFactory = configuration.getObjectFactory();

    if (boundSql == null) { // issue #435, get the key before calculating the statement
      // 如果是前置主键自增,则在这里进行获得自增的键值
      generateKeys(parameterObject);
      // 获取BoundSql对象
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }

    this.boundSql = boundSql;

    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
  }

我们再来梳理一下Jdbc的执行流程:

  1. 从Connection获取一个Statement
  2. 为Statement设置参数

那我们来看看他是怎样获取的一个Statement的?
BaseStatementHandler

  // 从连接中获取一个Statement,并设置事务超时时间
  @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
       // 实际上他还是调用他的实现类类  
      statement = instantiateStatement(connection);
      //  设置超时时间
      setStatementTimeout(statement, transactionTimeout);
       // 读取大小 
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }

  // 从Connection中实例化Statement
  protected abstract Statement instantiateStatement(Connection connection) throws SQLException;

到这就可能设计到驱动包的源码的知识,这部分我们不会多讲,我们只是梳理Mybatis的执行过程,下面我们PreparedStatementHandler以基本的案例为例
PreparedStatementHandler

  @Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
     // 自增主键     
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
         // 调用   
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
      return connection.prepareStatement(sql);
    } else {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    }
  }

下面的流程就是从连接对象中预处理对象。具体的逻辑请参考前面的逻辑分析

🌈🌈总结

简单描述一下update 方法的执行过程:

  1. MyBatis 接收到 update 请求后会先找到 CachingExecutor 缓存执行器查询是否需要刷新缓存,然后找到BaseExecutor 执行 update 方法;
  2. BaseExecutor 基础执行器会清空一级缓存,然后交给再根据执行器的类型找到对应的执行器,继续执行 update 方法;
  3. 具体的执行器会先创建 Configuration 对象,根据 Configuration 对象调用 newStatementHandler 方法,返回 statementHandler 的句柄;
  4. 具体的执行器会调用 prepareStatement 方法,找到本类的 prepareStatement 方法后,再有prepareStatement 方法调用 StatementHandler 的子类 BaseStatementHandler 中的 prepare 方法
  5. BaseStatementHandler 中的 prepare 方法会调用 instantiateStatement 实例化具体的 Statement 对象并返回给具体的执行器对象
  6. 由具体的执行器对象调用 parameterize 方法给参数进行赋值。

三 ParameterHandler

上面我们介绍了StatementHandler 组件最主要的作用在于创建 Statement 对象与数据库进行交流,还会使用 ParameterHandler 进行参数配置,使用 ResultSetHandler 把查询结果与实体类进行绑定。

3.1 ParameterHandler

ParameterHandler

public interface ParameterHandler {

  // 获取参数对象
  Object getParameterObject();

  // 设置参数对象
  void setParameters(PreparedStatement ps)
      throws SQLException;
}

下面我们来看看他的实现类,注意ParameterHandler的创建时机在BaseStatementHandler的初始化的时候
image.png
DefaultParameterHandler

public class DefaultParameterHandler implements ParameterHandler {

  // 类型处理器注册表
  private final TypeHandlerRegistry typeHandlerRegistry;
  // MappedStatement对象(包含完整的增删改查节点信息)
  private final MappedStatement mappedStatement;
  // 参数对象
  private final Object parameterObject;
  // BoundSql对象(包含SQL语句、参数、实参信息)
  private final BoundSql boundSql;
  // 配置信息
  private final Configuration configuration;

  public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    this.mappedStatement = mappedStatement;
    this.configuration = mappedStatement.getConfiguration();
    this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
    this.parameterObject = parameterObject;
    this.boundSql = boundSql;
  }

  @Override
  public Object getParameterObject() {
    return parameterObject;
  }

  /**
   * 为语句设置参数
   * @param ps 语句
   */
  @Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    // 取出参数列表
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        // ParameterMode.OUT是CallableStatement的输出参数,已经单独注册。故忽略
        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);
          }
          // 确定该参数的处理器
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            // 此方法最终根据参数类型,调用java.sql.PreparedStatement类中的参数赋值方法,对SQL语句中的参数赋值
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

}

总的来说,其实很简单,通过我们传入的参数,找对应的参数处理器,设置参数

四 ResultSetHandler

  • 终于到了最后一个组件,我们想一想在我们使用Jdbc的时候,在执行预处理语句之后返回的并不是我们想要的结果,我们需要自己来选择返回什么类型的结果,而Mybatis中采用ResultSetHandler来解析我们想要的结果。
  • 首先会经过 Executor 执行器,它主要负责管理创建 StatementHandler 对象,然后由 StatementHandler 对象做数据库的连接以及生成 Statement 对象,并解析 SQL 参数,由 ParameterHandler 对象负责把 Mapper 方法中的参数映射到 XML 中的 SQL 语句中,那么是不是还少了一个步骤,就能完成一个完整的 SQL 请求了?没错,这最后一步就是 SQL 结果集的处理工作,也就是 ResultSetHandler 的主要工作

4.1 ResultSetHandler

首先我们来看看接口方法,而他的实现类与ParameterHandler一样,只有一个默认的实现类DefaultResultSetHandler
ResultSetHandler

public interface ResultSetHandler {

  // 将Statement的执行结果处理为List
  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  // 将Statement的执行结果处理为Map
  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  // 处理存储过程的输出结果
  void handleOutputParameters(CallableStatement cs) throws SQLException;

}

image.png

🚀🚀初始化

与上面一样ResultSetHandler的初始化在BaseStatementHandler的构造器中完成
BaseStatementHandler

protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    this.configuration = mappedStatement.getConfiguration();
    this.executor = executor;
    this.mappedStatement = mappedStatement;
    this.rowBounds = rowBounds;

    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.objectFactory = configuration.getObjectFactory();

    if (boundSql == null) { // issue #435, get the key before calculating the statement
      // 如果是前置主键自增,则在这里进行获得自增的键值
      generateKeys(parameterObject);
      // 获取BoundSql对象
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }

    this.boundSql = boundSql;
	// 初始化参数处理器
    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    // 初始化结果处理器
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
  }

Configuration

  public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
    // 创建结果处理器   
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    // 插件过滤链  
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

下面我们来讲解他的默认类:DefaultResultSetHandler

4.2 DefaultResultSetHandler

MyBatis 只有一个默认的实现类就是 DefaultResultSetHandler,ResultSetHandler 主要负责处理两件事

  1. 处理 Statement 执行后产生的结果集,生成结果列表
  2. 处理存储过程执行后的输出参数

下面我们针对关键代码进行分析,结果集的返回
DefaultResultSetHandler

/**
   * 处理Statement得到的多结果集(也可能是单结果集,这是多结果集的一种简化形式),最终得到结果列表
   * @param stmt Statement语句
   * @return 结果列表
   * @throws SQLException
   */
  @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;
    // 可能会有多个结果集,先取出第一个结果集
    ResultSetWrapper rsw = getFirstResultSet(stmt);
    // 查询语句对应的resultMap节点,可能含有多个
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    // 合法性校验(存在输出结果集的情况下,resultMapCount不能为0)
    validateResultMapsCount(rsw, resultMapCount);
    // 循环遍历每一个设置了resultMap的结果集
    while (rsw != null && resultMapCount > resultSetCount) {
      // 获得当前结果集对应的resultMap
      ResultMap resultMap = resultMaps.get(resultSetCount);
      // 进行结果集的处理
      handleResultSet(rsw, resultMap, multipleResults, null);
      // 获取下一结果集
      rsw = getNextResultSet(stmt);
      // 清理上一条结果集的环境
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    // 获取多结果集中所有结果集的名称
    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      // 循环遍历每一个没有设置resultMap的结果集
      while (rsw != null && resultSetCount < resultSets.length) {
        // 获取该结果集对应的父级resultMap中的resultMapping(注:resultMapping用来描述对象属性的映射关系)
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          // 获取被嵌套的resultMap的编号
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          // 处理嵌套映射
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }
    // 判断是否是单结果集:如果是则返回结果列表;如果否则返回结果集列表
    return collapseSingleResultList(multipleResults);
  }
// 将一行记录转化为对象

  /**
   * 将一条记录转化为一个对象
   * @param rsw 结果集包装
   * @param resultMap 结果映射
   * @param columnPrefix 列前缀
   * @return 转化得到的对象
   * @throws SQLException
   */
  private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    // 创建一个延迟加载器
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    // 创建这一行记录对应的对象
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      // 根据对象得到其MetaObject
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      boolean foundValues = this.useConstructorMappings;
      // 是否允许自动映射未明示的字段
      if (shouldApplyAutomaticMappings(resultMap, false)) {
        // 自动映射未明示的字段
        foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
      }
      // 按照明示的字段进行重新映射
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
      foundValues = lazyLoader.size() > 0 || foundValues;
      rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
  }

需要详细的分析过程,请参考前面的文章,下面我们来梳理一下过程

  • 首先获取一个数据库返回的结果对象,通过反射方式构造一个空的对象,带有属性
  • 然后根据字段类型,调用不同的类型处理器获取值,在为对象填充属性,最后添加记录到multipleResults中,一个结果处理完毕,接着循环处理
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

长安不及十里

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

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

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

打赏作者

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

抵扣说明:

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

余额充值