Mybatis源码 ------ MapperProxyFactory,sql解析,参数解析,结果映射

  已经了解完了SqlSessionFactory的初始化,接着看openSession方法,获取到sqlSession对象。

public SqlSession openSession() {
	//configuration.getDefaultExecutorType()
	//获取ExecutorType,默认为ExecutorType.SIMPLE,也可以在配置文件中自己配置
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
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);
      //实例化Executor对象
      final Executor executor = configuration.newExecutor(tx, execType);
      //返回DefaultSqlSession对象,持有executor对象
      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();
    }
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
	//默认返回SimpleExecutor
    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) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    //这里可以通过设置mybatis插件,生成executor 的代理对象,后续文章再详细说
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

  接着看到DefaultSqlSession类的getMapper方法,获取到接口的实现类(代理对象的生产工厂MapperProxyFactory)。

public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
	//从knownMappers根据接口类型获取到对应的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);
    }
}
public T newInstance(SqlSession sqlSession) {
	//实例化MapperProxy对象
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
    //生成jdk动态代理对象
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

  着重来看MapperProxy类的invoke方法

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (method.isDefault()) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //获取mapperMethod 对象
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
	//methodCache(map)放入method和MapperMethod
    return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    //MapperMethod持有SqlCommand和MethodSignature
	
	//SqlCommand包含name属性(MappedStatement的id)和type属性(sqlCommandType)
    this.command = new SqlCommand(config, mapperInterface, method);
    //MethodSignature包含方法相关的信息
    this.method = new MethodSignature(config, mapperInterface, method);
}
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
	  //判断返回值类型
      Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
      if (resolvedReturnType instanceof Class<?>) {
        this.returnType = (Class<?>) resolvedReturnType;
      } else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
      } else {
        this.returnType = method.getReturnType();
      }
      this.returnsVoid = void.class.equals(this.returnType);
      this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
      this.returnsCursor = Cursor.class.equals(this.returnType);
      this.returnsOptional = Optional.class.equals(this.returnType);
      this.mapKey = getMapKey(method);
      this.returnsMap = this.mapKey != null;
      this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
      this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
      
      //ParamNameResolver用于解析参数名称
      this.paramNameResolver = new ParamNameResolver(configuration, method);
}
public ParamNameResolver(Configuration config, Method method) {
	//获取所有参数类型
    final Class<?>[] paramTypes = method.getParameterTypes();
    //获取参数和参数的注解,作为二维数组保存(一个参数可以有多个注解)
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();
    int paramCount = paramAnnotations.length;
    // get names from @Param annotations
    //循环处理所有的参数
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
      if (isSpecialParameter(paramTypes[paramIndex])) {
        // skip special parameters
        continue;
      }
      String name = null;
      //循环参数所有的注解
      for (Annotation annotation : paramAnnotations[paramIndex]) {
      	//如果注解是@Param
        if (annotation instanceof Param) {
          hasParamAnnotation = true;
          //获取注解配置的value值,作为参数名称
          name = ((Param) annotation).value();
          break;
        }
      }
      //如果没有@Param注解
      if (name == null) {
        // @Param was not specified.
        //Configuration的useActualParamName默认为true
        if (config.isUseActualParamName()) {
          //获取实际的参数名称,arg0,agr1这种
          name = getActualParamName(method, paramIndex);
        }
        //可以修改配置属性useActualParamName为false
        if (name == null) {
          // use the parameter index as the name ("0", "1", ...)
          // gcode issue #71
          //name为map当前的size
          name = String.valueOf(map.size());
        }
      }
      //建立参数下标位置和name的映射关系
      map.put(paramIndex, name);
    }
    //map赋值给参数名称解析器的names属性
    names = Collections.unmodifiableSortedMap(map);
}

//再看到mapperMethod的execute
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    //判断sqlCommand的类型,调用不同的处理逻辑
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        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()) {
          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);
          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;
}
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    //使用参数名称解析器解析入参,建立参数名称和参数值的映射关系
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
      //command.getName()获取sqlCommand的name属性,就是MappedStatement的id
      //接着看
      result = sqlSession.selectList(command.getName(), param);
    }
    // issue #510 Collections & arrays support
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        return convertToArray(result);
      } else {
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
}
public Object convertArgsToSqlCommandParam(Object[] args) {
      return paramNameResolver.getNamedParams(args);
}
public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    //没有参数
    if (args == null || paramCount == 0) {
      return null;
    //没有@Param注解修饰且只有一个参数,直接返回该参数
    } else if (!hasParamAnnotation && paramCount == 1) {
      return args[names.firstKey()];
    } else {
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      //循环names容器(参数下标和参数名称)
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        //建立参数名称和实际参数值的映射关系
        param.put(entry.getValue(), args[entry.getKey()]);
        // add generic param names (param1, param2, ...)
        final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          //还会建立param1,param2这种参数名称对应实际参数的映射关系
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      //返回参数名称和实际参数值的map
      return param;
    }
}

  接着看DefaultSqlSession的selectList方法

public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      //获取MappedStatement对象
      MappedStatement ms = configuration.getMappedStatement(statement);
      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();
    }
}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
	//获取BoundSql对象
    BoundSql boundSql = ms.getBoundSql(parameter);
    //生成缓存key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    //调用executor的query方法
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public BoundSql getBoundSql(Object parameterObject) {
    //接着看
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    }

    // check for nested result maps in parameter mappings (issue #30)
    for (ParameterMapping pm : boundSql.getParameterMappings()) {
      String rmId = pm.getResultMapId();
      if (rmId != null) {
        ResultMap rm = configuration.getResultMap(rmId);
        if (rm != null) {
          hasNestedResultMaps |= rm.hasNestedResultMaps();
        }
      }
    }
		
    return boundSql;
}

//含有${}、动态标签的sql,则是DynamicSqlSource,否则是RawSqlSource
//直接看DynamicSqlSource的getBoundSql方法
public BoundSql getBoundSql(Object parameterObject) {
    //创建DynamicContext 对象,含有sqlBuilder属性,由于拼接解析后的sql段 ,含有bindings属性持有了parameterObject(参数名传和参数的映射(比如解析if标签就有可能需要使用))
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //rootSqlNode包含各个sqlNode(各个实现类处理不同的标签和文本),不同层级的标签和sql文件内容和空格换行,树状结构
    //遍历解析各个标签,将解析的文本内容逐步添加到sqlBuilder中
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    //context.getSql()将sqlBuilder转为sql字符串
    //将sql中#{}占位符替换为?,且将解析的#{a,javatype=,jdbctype=}参数信息和对应typeHandler封装到ParameterMapping中,
    //返回为新的StaticSqlSource对象 
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    //调用StaticSqlSource的getBoundSql方法,返回新的BoundSql对象
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    //将bindings参数信息设置到metaParameters属性中
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    //返回boundSql
    return boundSql;
}
//再看到RawSqlSource
public class RawSqlSource implements SqlSource {

  private final SqlSource sqlSource;
	
  public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    //将sql中#{}占位符替换为?,且将解析的#{a,javatype=,jdbctype=}参数信息和对应typeHandler封装到ParameterMapping中,
    //返回为新的StaticSqlSource对象 
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
  }
	
  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    DynamicContext context = new DynamicContext(configuration, null);
    //解析动态标签
    rootSqlNode.apply(context);
    return context.getSql();
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    //调用StaticSqlSource的getBoundSql方法,返回新的BoundSql对象
    return sqlSource.getBoundSql(parameterObject);
  }

}

  接着看BaseExecutor的query方法

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会大于1
      queryStack++;
      //从一级缓存里面获取数据
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
      	//处理CALLABLE类型的sql(复杂的业务逻辑不要去使用存储过程,难以调试和维护),要绑定传出传入参数
        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,则清除缓存。默认为session,一级缓存启动
      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;
    //先放入缓存key,表示正在查询
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      //查询数据库
      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;
}
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();
      //返回RoutingStatementHandler对象
      //这里可以通过设置mybatis插件返回RoutingStatementHandler的代理对象,后续文章再详细说
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      //看这里
      stmt = prepareStatement(handler, ms.getStatementLog());
      //调用StatementHandler 的query方法
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
}

  接着看prepareStatement方法,日志打印功能

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    //获取连接对象
    Connection connection = getConnection(statementLog);
    //设置连接的超时时间和fetchSize,使用连接对象返回Statement对象
    stmt = handler.prepare(connection, transaction.getTimeout());
    //将sql中的?替换为具体的参数
    handler.parameterize(stmt);
    return stmt;
}
protected Connection getConnection(Log statementLog) throws SQLException {
	//从事务对象获取数据源对象再获取连接对象
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      //对连接对象生成代理对象,增加日志打印功能
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
}
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
    //看ConnectionLogger对象invoke方法
    InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    //返回连接对象的代理对象
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
}
public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
      //Object方法不管
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      //调用prepareStatement,createStatement,prepareCall方法时,打印日志
      if ("prepareStatement".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        //以PreparedStatementLogger为例,再看这里,对stmt又生成了代理对象
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("createStatement".equals(method.getName())) {
        Statement stmt = (Statement) method.invoke(connection, params);
        stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else {
         //其他方法不管
        return method.invoke(connection, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
}
public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
	//看到PreparedStatementLogger的invoke方法
    InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
    ClassLoader cl = PreparedStatement.class.getClassLoader();
    //返回代理对象
    return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
}
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    
    //一样的,拦截特定的方法打印日志
		
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      if (EXECUTE_METHODS.contains(method.getName())) {
        if (isDebugEnabled()) {
          debug("Parameters: " + getParameterValueString(), true);
        }
        clearColumnInfo();
        if ("executeQuery".equals(method.getName())) {
          ResultSet rs = (ResultSet) method.invoke(statement, params);
          //再看这里,对rs又生成了代理对象
          return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
        } else {
          return method.invoke(statement, params);
        }
      } else if (SET_METHODS.contains(method.getName())) {
        if ("setNull".equals(method.getName())) {
          setColumn(params[0], null);
        } else {
          setColumn(params[0], params[1]);
        }
        return method.invoke(statement, params);
      } else if ("getResultSet".equals(method.getName())) {
        ResultSet rs = (ResultSet) method.invoke(statement, params);
        return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
      } else if ("getUpdateCount".equals(method.getName())) {
        int updateCount = (Integer) method.invoke(statement, params);
        if (updateCount != -1) {
          debug("   Updates: " + updateCount, false);
        }
        return updateCount;
      } else {
        return method.invoke(statement, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
}
 public static ResultSet newInstance(ResultSet rs, Log statementLog, int queryStack) {
    //看到ResultSetLogger的invoke方法
    InvocationHandler handler = new ResultSetLogger(rs, statementLog, queryStack);
    ClassLoader cl = ResultSet.class.getClassLoader();
    return (ResultSet) Proxy.newProxyInstance(cl, new Class[]{ResultSet.class}, handler);
}
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      //调用resultset的next获取数据时,打印日志
      Object o = method.invoke(rs, params);
      if ("next".equals(method.getName())) {
        if ((Boolean) o) {
          rows++;
          if (isTraceEnabled()) {
            ResultSetMetaData rsmd = rs.getMetaData();
            final int columnCount = rsmd.getColumnCount();
            if (first) {
              first = false;
              printColumnHeaders(rsmd, columnCount);
            }
            printColumnValues(columnCount);
          }
        } else {
          debug("     Total: " + rows, false);
        }
      }
      clearColumnInfo();
      return o;
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
}

  接着看StatementHandler的parameterize方法


public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    //ParameterHandler也可以通过mybatis插件生成代理对象
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }

public void parameterize(Statement statement) throws SQLException {
    parameterHandler.setParameters((PreparedStatement) statement);
}
public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    //获取sql所需的参数信息,参数名称参数类型等
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      //循环处理所有参数
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        //是否为出参值
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          //获取属性名称
          String propertyName = parameterMapping.getProperty();
          //首先判断additionalParameters中是否含有该参数
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          //判断是否有实际的参数可以用来处理
          } else if (parameterObject == null) {
            value = null;
          //判断改参数类是否在类型注册中有对应的处理类(TypeHandlerRegistry静态代码块内置了常见的包装类型和基本类型,可以添加自定义typehandler)
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          //一般传了pojo,map的,会使用Object对应的unknownTypeHandler设置参数值
          } else {
            //将实际参数转化为metaObject,字段名称和对应的字段值
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            //使用属性名获取到对应的值
            value = metaObject.getValue(propertyName);
          }
          //写了#{a,jdbctype=,typeHandler=},这里会有值
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
          	//使用对应的typeHandler设置参数值
          	//这里传入了实际参数值的value
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
}
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    if (parameter == null) {
      if (jdbcType == null) {
        throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
      }
      try {
        //设置空值
        ps.setNull(i, jdbcType.TYPE_CODE);
      } catch (SQLException e) {
        throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
              + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
              + "Cause: " + e, e);
      }
    } else {
      try {
        //设置非空值
        setNonNullParameter(ps, i, parameter, jdbcType);
      } catch (Exception e) {
        throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
              + "Try setting a different JdbcType for this parameter or a different configuration property. "
              + "Cause: " + e, e);
      }
    }
}

//看到UnknownTypeHandler的setNonNullParameter方法

public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
      throws SQLException {
    TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
    //使用对应的typeHandler去设置参数值
    handler.setParameter(ps, i, parameter, jdbcType);
}
private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) {
    TypeHandler<?> handler;
    if (parameter == null) {
      handler = OBJECT_TYPE_HANDLER;
    } else {
      //直接是用实际的参数值获取获取class类型
      //再使用该参数类型获取到typeHandler
      handler = typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType);
      // check if handler is null (issue #270)
      if (handler == null || handler instanceof UnknownTypeHandler) {
        handler = OBJECT_TYPE_HANDLER;
      }
    }
    return handler;
}

  接着看StatementHandler的query方法

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
    //ResultSetHandler 也可以通过mybatis插件生成代理对象
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}
  
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    //执行查询
    ps.execute();
    //对查询结果进行处理,封装为我们需要的类型
    return resultSetHandler.handleResultSets(ps);
}
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;
    //获取resultSet,包装为ResultSetWrapper返回
    //可以有多个resultSet(存储过程可能返回多个),一般只有一个
    ResultSetWrapper rsw = getFirstResultSet(stmt);
	
	//获取mappedStatement的所有ResultMap
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    
    while (rsw != null && resultMapCount > resultSetCount) {
      //获取到ResultMap
      ResultMap resultMap = resultMaps.get(resultSetCount);
      //看这里
      handleResultSet(rsw, resultMap, multipleResults, null);
      //处理下一个resultSet
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }
	
	//配置了resultSets属性的情况,处理多个结果集映射关系。
    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }
	
	//返回最终的结果
    return collapseSingleResultList(multipleResults);
}
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      //有父ResultMap或者嵌套ResultMap
      if (parentMapping != null) {
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        //没有配置resultHandler去处理结果集
        if (resultHandler == null) {
          //默认的DefaultResultHandler 
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          //接着看
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          //multipleResults放入了list的中的数据
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      // issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
}
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    //是否有嵌套的ResultMap
    if (resultMap.hasNestedResultMaps()) {
      ensureNoRowBounds();
      checkResultHandler();
      //嵌套resultMap
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      //没有嵌套resultMap
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
}
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    //获取到结果集
    ResultSet resultSet = rsw.getResultSet();
    //mybatis分页,调用next跳过非当前页的resultSet 
    skipRows(resultSet, rowBounds);
    //循环获取resultSet的数据,不能超过rowBounds的定义limit的数量
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
      //处理鉴别器
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      //将resultSet包装为我们需要的对象类型或者map类型,返回回来
      Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
      //存入返回的数据到list中
      storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
}
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类型,持有了rowValue
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      boolean foundValues = this.useConstructorMappings;
      //如果配置了字段自动映射
      if (shouldApplyAutomaticMappings(resultMap, false)) {
        foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
      }
      //使用resuleMap的映射关系,将结果填充到对象
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
      foundValues = lazyLoader.size() > 0 || foundValues;
      rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
}
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<>();
    //创建实际的对象,属性为空,后续将resultset的值填充进来
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      //获取所有的ResultMapping
      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(); // set current mapping result
    return resultObject;
}
//来看到代理对象的具体拦截逻辑
public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
      final Class<?> type = target.getClass();
      //看到这个callback的intercept方法
      EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
      Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
      PropertyCopier.copyBeanProperties(type, target, enhanced);
      return enhanced;
}
public Object intercept(Object enhanced, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
      //获取方法名
      final String methodName = method.getName();
      try {
        synchronized (lazyLoader) {
          //如果方法名称是writeReplace,需要将原本对象的属性赋值给新的对象
          if (WRITE_REPLACE_METHOD.equals(methodName)) {
            Object original;
            if (constructorArgTypes.isEmpty()) {
              original = objectFactory.create(type);
            } else {
              original = objectFactory.create(type, constructorArgTypes, constructorArgs);
            }
            PropertyCopier.copyBeanProperties(type, enhanced, original);
            if (lazyLoader.size() > 0) {
              return new CglibSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
            } else {
              return original;
            }
          } 
          //看这里
		  else {
		    //lazyLoader的loaderMap不为空
            if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
              if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                lazyLoader.loadAll();
              } 
              //如果是set方法
              else if (PropertyNamer.isSetter(methodName)) {
                final String property = PropertyNamer.methodToProperty(methodName);
                //lazyLoader移除该属性
                lazyLoader.remove(property);
              } 
              //如果是get方法
              else if (PropertyNamer.isGetter(methodName)) {
                final String property = PropertyNamer.methodToProperty(methodName);
                //lazyLoader存在该属性
                if (lazyLoader.hasLoader(property)) {
                  //看这里
                  lazyLoader.load(property);
                }
              }
            }
          }
        }
        //调用被代理方法
        return methodProxy.invokeSuper(enhanced, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
}
public boolean load(String property) throws SQLException {
    //从lazyLoader移除该属性
    LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
    if (pair != null) {
      //看这里
      pair.load();
      return true;
    }
    return false;
}
public void load(final Object userObject) throws SQLException {
      if (this.metaResultObject == null || this.resultLoader == null) {
        if (this.mappedParameter == null) {
          throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "
                  + "required parameter of mapped statement ["
                  + this.mappedStatement + "] is not serializable.");
        }

        final Configuration config = this.getConfiguration();
        //获取嵌套查询的MappedStatement 
        final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
        if (ms == null) {
          throw new ExecutorException("Cannot lazy load property [" + this.property
                  + "] of deserialized object [" + userObject.getClass()
                  + "] because configuration does not contain statement ["
                  + this.mappedStatement + "]");
        }
		
        this.metaResultObject = config.newMetaObject(userObject);
        //包装为ResultLoader
        this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
                metaResultObject.getSetterType(this.property), null, null);
      }

      /* We are using a new executor because we may be (and likely are) on a new thread
       * and executors aren't thread safe. (Is this sufficient?)
       *
       * A better approach would be making executors thread safe. */
      if (this.serializationCheck == null) {
        final ResultLoader old = this.resultLoader;
        this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,
                old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
      }
	  
	  
      //反射调用set方法 将结果值设置到metaResultObject的对象(rowValue)中
      this.metaResultObject.setValue(property, this.resultLoader.loadResult());
}
public Object loadResult() throws SQLException {
    //查询出值
    List<Object> list = selectList();
    //将值转换为targetType类型返回
    resultObject = resultExtractor.extractObjectFromList(list, targetType);
    return resultObject;
}
private <E> List<E> selectList() throws SQLException {
    Executor localExecutor = executor;
    if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {
      localExecutor = newExecutor();
    }
    try {
      //调用Executor的query方法查询数据
      return localExecutor.query(mappedStatement, parameterObject, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey, boundSql);
    } finally {
      if (localExecutor != executor) {
        localExecutor.close(false);
      }
    }
}


//看回对象属性填充的过程
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
    final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
    boolean foundValues = false;
    final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
    //循环处理所有ResultMapping所有映射关系
    for (ResultMapping propertyMapping : propertyMappings) {
      //有配置columnPrefix的,给column名拼接上columnPrefix
      String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
      if (propertyMapping.getNestedResultMapId() != null) {
        // the user added a column attribute to a nested result map, ignore it
        column = null;
      }
      if (propertyMapping.isCompositeResult()
          || (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
          || propertyMapping.getResultSet() != null) {
          //获取到对应的返回值
        Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
        // issue #541 make property optional
        final String property = propertyMapping.getProperty();
        if (property == null) {
          continue;
        } else if (value == DEFERRED) {
          foundValues = true;
          continue;
        }
        if (value != null) {
          foundValues = true;
        }
        if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {
          //反射调用set方法 将结果值设置到metaObject的对象(rowValue)中
          // gcode issue #377, call setter on nulls (value is not 'found')
          metaObject.setValue(property, value);
        }
      }
    }
    return foundValues;
}
private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
    //含有嵌套查询,比如collection和association标签配置了select属性
    if (propertyMapping.getNestedQueryId() != null) {
	      return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
    } 
    //有配置resultset属性
    else if (propertyMapping.getResultSet() != null) {
      addPendingChildRelation(rs, metaResultObject, propertyMapping);   // TODO is that OK?
      return DEFERRED;
    } 
    else {
      //获取到结果值映射对应的TypeHandler,解析resultMap的时候,就已经确定了typeHandler的值,根据实体类的对应类型
      final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
      //拼接字段名的columnPrefix
      final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
      //调用到rs对应方法获取结果,比如StringTypeHandler,调用rs.getString(columnName)
      return typeHandler.getResult(rs, column);
    }
}
private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {
    if (parentMapping != null) {
      //存在父级,则把子映射的值完成的值,赋值给父级
      linkToParents(rs, parentMapping, rowValue);
    } else {
      //看这里
      callResultHandler(resultHandler, resultContext, rowValue);
    }
}
private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {
	//将结果对象赋值给resultContext的resultObject属性
    resultContext.nextResultObject(rowValue);
    //获取resultContext的resultObject取出,添加到defaultResultHandler的list中
    ((ResultHandler<Object>) resultHandler).handleResult(resultContext);
}

//再来看到含有嵌套查询的情况
private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
    //获取嵌套查询id 
    final String nestedQueryId = propertyMapping.getNestedQueryId();
    final String property = propertyMapping.getProperty();
    //根据id获取的嵌套查询的MappedStatement 
    final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
    //获取嵌套查询参数的类型
    final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
    //根据外层查询结果rs,获取到嵌套查询需要的实际参数值
    final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
    Object value = null;
    //参数不为空
    if (nestedQueryParameterObject != null) {
      //获取BoundSql 
      final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
      //生成CacheKey 
      final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
      //获取查询结果的类型
      final Class<?> targetType = propertyMapping.getJavaType();
      //判断缓存是否存在
      if (executor.isCached(nestedQuery, key)) {
        executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
        value = DEFERRED;
      } else {
        //封装属性到ResultLoader中
        final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
        //判断是否懒加载 fetchType = lazy
        if (propertyMapping.isLazy()) {
          //将该属性加入到lazyLoader的loaderMap中
          lazyLoader.addLoader(property, metaResultObject, resultLoader);
          value = DEFERRED;
        } else {
          //非懒加载直接查询出值
          value = resultLoader.loadResult();
        }
      }
    }
    return value;
}
public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    DeferredLoad deferredLoad = new DeferredLoad(resultObject, property, key, localCache, configuration, targetType);
    //如果缓存中的key存在,且值不是EXECUTION_PLACEHOLDER
    if (deferredLoad.canLoad()) {
      //加载缓存的数据
      deferredLoad.load();
    } else {
      //暂存起来
      deferredLoads.add(new DeferredLoad(resultObject, property, key, localCache, configuration, targetType));
    }
 }

  到此,通过mybatis解析sql和参数,映射数据的主要流程就结束了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值