SPARKSQL3.0-Unresolved[Parsed]阶段源码剖析

news2025/1/11 23:50:58

一、前言

上两节介绍了Antlr4的简单使用以及spark中如何构建SessionState,如果没有看过建议先了解上两节的使用,否则看本节会比较吃力

[SPARKSQL3.0-Antlr4由浅入深&SparkSQL语法解析]

[SPARKSQL3.0-SessionState构建源码剖析]

那么在Unresolved阶段,spark主要做了两件事:

1、将sql字符串通过antrl4转化成AST语法树

2、将AST语法树经过spark自定义访问者模式转化成logicalPlan【logicalPlan可以理解为精简版的语法树】

注意:该阶段中语法树中仅仅是数据结构,不包含任何数据信息

二、sql -> antlr4阶段

先来一个简单的示例:

val sparkConf = new SparkConf().setMaster("local[*]").setAppName("sparkSQL")

val spark = SparkSession.builder()
  .config(sparkConf)
  .getOrCreate()

case class Person(name: String, age: Int)

Seq(Person("Jack", 12), Person("James", 21), Person("Mac", 30)).toDS().createTempView("person")

spark.sql("SELECT * FROM PERSON WHERE AGE > 18").explain(true) // 打印执行计划

在示例中我们创建了一张person,并写了一个简单的sql语句,执行后结果打印:

== Parsed Logical Plan ==
'Project [*]
+- 'Filter ('AGE > 18)
   +- 'UnresolvedRelation `PERSON`

== Analyzed Logical Plan ==
name: string, age: int
Project [name#10, age#11]
+- Filter (AGE#11 > 18)
   +- SubqueryAlias person
      +- LocalRelation [name#10, age#11]

......

其中 == Parsed Logical Plan == 就是Unresolved阶段,那么sql语句是如何转换成上面的样子呢?

先来看一下sql语句经过antlr4之后生成的AST语法树:

image-20220706163357169

这是由IDEA的antlr4插件根据spark的SqlBaseParser.g4文件形成的语法树,详细看Antlr4一节

接下来我们debug一下看生成此树的过程,首先进入spark.sql函数:

def sql(sqlText: String): DataFrame = withActive {
    val tracker = new QueryPlanningTracker
    val plan = tracker.measurePhase(QueryPlanningTracker.PARSING) {  // measurePhase函数用来统计各个阶段的耗时
      sessionState.sqlParser.parsePlan(sqlText)	// 真正执行parse
    }
    Dataset.ofRows(self, plan, tracker)
  }

tracker.measurePhase函数主要用于统计spark-sql各个阶段的耗时,如下:

image-20220706164443801

真正执行的是sessionState.sqlParser.parsePlan(sqlText),sessionState.sqlParser变量实际上是SparkSqlParser,这在SessionState创建一节已经讲过

SparkSqlParser类中并没有parsePlan函数,此处是由父类AbstractSqlParser实现:

image-20220706164828228

parsePlan函数调用了parse函数,并且代入一个函数参数,函数中的变量astBuilder为抽象函数,并要求类型为AstBuilder

protected def astBuilder: AstBuilder

此函数由子类SparkSqlParser实现:构建SparkSqlAstBuilder

class SparkSqlParser(conf: SQLConf) extends AbstractSqlParser(conf) {
  val astBuilder = new SparkSqlAstBuilder(conf)

  private val substitutor = new VariableSubstitution(conf)

  protected override def parse[T](command: String)(toResult: SqlBaseParser => T): T = {
    super.parse(substitutor.substitute(command))(toResult)
  }
}

SparkSqlAstBuilder是AstBuilder的子类,而AstBuilder正是spark-antlr4生成的访问器父类SqlBaseBaseVisitor,继承关系如下:

image-20220706165252064

image-20220706165430136

在AstBuilder 和 SparkSqlAstBuilder类中,spark实现了自定义解析AST语法树函数

回过头我们再来看AbstractSqlParser的parsePlan函数,其执行是调用了parse函数:

override def parsePlan(sqlText: String): LogicalPlan = parse(sqlText) { parser =>
    astBuilder.visitSingleStatement(parser.singleStatement()) match { // spark的g4文件所有语法的根节点就是singleStatement
      case plan: LogicalPlan => plan
      case _ =>
        val position = Origin(None, None)
        throw new ParseException(Option(sqlText), "Unsupported SQL statement", position, position)
    }
  }


protected def parse[T](command: String)(toResult: SqlBaseParser => T): T = {
    logDebug(s"Parsing command: $command")
		// SqlBaseLexer,解析关键词及各种标识符
    val lexer = new SqlBaseLexer(new UpperCaseCharStream(CharStreams.fromString(command)))
    lexer.removeErrorListeners()
    lexer.addErrorListener(ParseErrorListener)
    lexer.legacy_setops_precedence_enbled = conf.setOpsPrecedenceEnforced
    lexer.legacy_exponent_literal_as_decimal_enabled = conf.exponentLiteralAsDecimalEnabled
    lexer.SQL_standard_keyword_behavior = conf.ansiEnabled
		// 词法符号的缓冲器,存储词法分析器生成的词法符号
    val tokenStream = new CommonTokenStream(lexer)
		// SqlBaseParser,构建antlr4的语法解析器
    val parser = new SqlBaseParser(tokenStream)
    parser.addParseListener(PostProcessor)
    parser.removeErrorListeners()
    parser.addErrorListener(ParseErrorListener)
    parser.legacy_setops_precedence_enbled = conf.setOpsPrecedenceEnforced
    parser.legacy_exponent_literal_as_decimal_enabled = conf.exponentLiteralAsDecimalEnabled
    parser.SQL_standard_keyword_behavior = conf.ansiEnabled

    try {
      try {
        // 首先,尝试使用可能更快的SLL预测模式进行解析
        parser.getInterpreter.setPredictionMode(PredictionMode.SLL)
        toResult(parser) // 此处就用到了第二个传参函数
      }
      catch {
        case e: ParseCancellationException =>
          // if we fail, parse with LL mode
          tokenStream.seek(0) // rewind input stream
          parser.reset()

          // 如果失败,用LL模式解析
          parser.getInterpreter.setPredictionMode(PredictionMode.LL)
          toResult(parser)
      }
    }
    catch {
      case e: ParseException if e.command.isDefined =>
        throw e
      case e: ParseException =>
        throw e.withCommand(command)
      case e: AnalysisException =>
        val position = Origin(e.line, e.startPosition)
        throw new ParseException(Option(command), e.message, position, position)
    }
  }
}

以上代码中核心是toResult(parser)函数,toResult函数便是AbstractSqlParser-parsePlan函数中的第二个参数:

image-20220707121103283

而toResult(parser)函数中调用了astBuilder.visitSingleStatement(parser.singleStatement()),如下:

override def visitSingleStatement(ctx: SingleStatementContext): LogicalPlan = withOrigin(ctx) {
    visit(ctx.statement).asInstanceOf[LogicalPlan] // 到这就开始调用visit函数,不断的递归调用到各种节点
  }

那么到这我们可以debug一下生成的SingleStatementContext节点是什么:

image-20220706172156836

可以看到其children有两个节点,正好对应语法树:

image-20220706172249610

再往下看:我们直接到RegularQuerySpecificationContext节点,至此可以看到antlr4生成语法树的整体结构

image-20220706172418018

image-20220706172452166

至此我们看到antlr4生成的SPARK-AST语法树全貌,但这只是根据.g4文件生成的语法树,而我们最终返回的是logicalPlan:

== Parsed Logical Plan ==
'Project [*]
+- 'Filter ('AGE > 18)
   +- 'UnresolvedRelation `PERSON`

这中间的变化就涉及到了antlr4 -> logicalPlan阶段

三、antlr4 -> logicalPlan阶段

上面我们看到了RegularQuerySpecificationContext节点,在此示例中整个语法树中就RegularQuerySpecificationContext节点的访问函数最有用,上面的节点实现函数都是不断递归往下级寻找,如下:

override def visitSingleStatement(ctx: SingleStatementContext): LogicalPlan = withOrigin(ctx) {
    visit(ctx.statement).asInstanceOf[LogicalPlan] // 递归子类,并且强转为LogicalPlan类型
}

@Override public T visitStatementDefault(SqlBaseParser.StatementDefaultContext ctx) { 
  return visitChildren(ctx);  // 递归子类
}

@Override public T visitQuery(SqlBaseParser.QueryContext ctx) { 
  return visitChildren(ctx);  // 递归子类
}

@Override public T visitQueryTermDefault(SqlBaseParser.QueryTermDefaultContext ctx) { 
  return visitChildren(ctx);  // 递归子类
}

......

那么直到RegularQuerySpecificationContext节点才真正实现了如何构建logicalPlan

override def visitRegularQuerySpecification(
      ctx: RegularQuerySpecificationContext): LogicalPlan = withOrigin(ctx) {
    val from = OneRowRelation().optional(ctx.fromClause) {
      visitFromClause(ctx.fromClause)  // 首先取出fromClause节点,直接访问visitFromClause函数
    }
  	// 调用withSelectQuerySpecification函数,将selectClause、whereClause等可能包含的节点传入
    withSelectQuerySpecification(  
      ctx,
      ctx.selectClause,
      ctx.lateralView,
      ctx.whereClause,
      ctx.aggregationClause,
      ctx.havingClause,
      ctx.windowClause,
      from
    )
  }

先看visitFromClause函数:此函数是所有from语法的主要执行函数,包括了sql-from语法中的:表名/left/right/inner/ join等解析

override def visitFromClause(ctx: FromClauseContext): LogicalPlan = withOrigin(ctx) {
    val from = ctx.relation.asScala.foldLeft(null: LogicalPlan) { (left, relation) =>
      val right = plan(relation.relationPrimary)
      val join = right.optionalMap(left)(Join(_, _, Inner, None, JoinHint.NONE))
      // 是对 relation 语法规则内部的 JOIN 语句进行处理,由于本示例sql不包含join语法此函数不做过多介绍
      withJoinRelations(join, relation)
    }
    if (ctx.pivotClause() != null) {
      if (!ctx.lateralView.isEmpty) {
        throw new ParseException("LATERAL cannot be used together with PIVOT in FROM clause", ctx)
      }
      withPivot(ctx.pivotClause, from)
    } else {
      ctx.lateralView.asScala.foldLeft(from)(withGenerate)
    }
  }

visitFromClause 方法主要处理 fromClause 规则中的多个 relation节点,根据下图的语法树我们知道每个 relation 的子节点,其中使用了 foldLeft 依次对 relation 的各个节点处理。

image-20220706181458460

在此贴一下 foldLeft 的源码

def foldLeft[B](z: B)(op: (B, A) => B): B = {
  var result = z
  this foreach (x => result = op(result, x))
  result
}

注意,初始值使用的 null,然后迭代 relation 进行处理,此时处理的主要为:val right = plan(relation.relationPrimary)

image-20220706193508843

plan函数底层还是调用访问函数:relation.relationPrimary

protected def plan(tree: ParserRuleContext): LogicalPlan = typedVisit(tree)
...
protected def typedVisit[T](ctx: ParseTree): T = {
    ctx.accept(this).asInstanceOf[T]
}
...

// 此示例中此函数将返回'UnresolvedRelation [PERSON]
override def visitTableName(ctx: TableNameContext): LogicalPlan = withOrigin(ctx) {
  	// 不断获子节点字符,最终获得表名
  	val tableId = visitMultipartIdentifier(ctx.multipartIdentifier)
  	// 通过表名构建UnresolvedRelation节点, 根据mayApplyAliasPlan函数并判断是否有别名
    val table = mayApplyAliasPlan(ctx.tableAlias, UnresolvedRelation(tableId))
    table.optionalMap(ctx.sample)(withSample)
}

// 不断获子节点字符,最终获得表名
override def visitMultipartIdentifier(
      ctx: MultipartIdentifierContext): Seq[String] = withOrigin(ctx) {
    ctx.parts.asScala.map(_.getText)
}

// 返回所有子节点的组合文本, 此示例中会返回表名person字符串
public String getText() {
		if (getChildCount() == 0) {
			return "";
		}

		StringBuilder builder = new StringBuilder();
		for (int i = 0; i < getChildCount(); i++) {
			builder.append(getChild(i).getText());
		}

		return builder.toString();
}

递归后返回字符串Person

image-20220706193920780

visitTableName函数最终返回UnresolvedRelation [PERSON]

image-20220706194121261

最终visitTableName函数返回的logicalPlan = UnresolvedRelation [PERSON],再回到visitFromClause函数,可以看到from返回值正是UnresolvedRelation

image-20220707105738478

而ctx并没有ctx.pivotClause()节点,且ctx.lateralView返回的集合为0,故visitFromClause函数最终返回UnresolvedRelation [PERSON]

回到再上一层visitRegularQuerySpecification函数,可以看出from最终为:UnresolvedRelation [PERSON]

接下来看withSelectQuerySpecification函数,可以看出其将selectClause、whereClause和刚才得到的from节点 等可能包含的节点传入

image-20220707110313619

此函数的功能是将from、select、where等各个部分组装成一个logicalPlan,贴一下源码:

private def withSelectQuerySpecification(
      ctx: ParserRuleContext,
      selectClause: SelectClauseContext,
      lateralView: java.util.List[LateralViewContext],
      whereClause: WhereClauseContext,
      aggregationClause: AggregationClauseContext,
      havingClause: HavingClauseContext,
      windowClause: WindowClauseContext,
      relation: LogicalPlan): LogicalPlan = withOrigin(ctx) {
    // Add lateral views.
    val withLateralView = lateralView.asScala.foldLeft(relation)(withGenerate)

    // Add where.
    val withFilter = withLateralView.optionalMap(whereClause)(withWhereClause)

    val expressions = visitNamedExpressionSeq(selectClause.namedExpressionSeq)
    // Add aggregation or a project.
    val namedExpressions = expressions.map {
      case e: NamedExpression => e
      case e: Expression => UnresolvedAlias(e)
    }

    def createProject() = if (namedExpressions.nonEmpty) {
      Project(namedExpressions, withFilter)
    } else {
      withFilter
    }

    val withProject = if (aggregationClause == null && havingClause != null) {
      if (conf.getConf(SQLConf.LEGACY_HAVING_WITHOUT_GROUP_BY_AS_WHERE)) {
        // If the legacy conf is set, treat HAVING without GROUP BY as WHERE.
        withHavingClause(havingClause, createProject())
      } else {
        // According to SQL standard, HAVING without GROUP BY means global aggregate.
        withHavingClause(havingClause, Aggregate(Nil, namedExpressions, withFilter))
      }
    } else if (aggregationClause != null) {
      val aggregate = withAggregationClause(aggregationClause, namedExpressions, withFilter)
      aggregate.optionalMap(havingClause)(withHavingClause)
    } else {
      // When hitting this branch, `having` must be null.
      createProject()
    }

    // Distinct
    val withDistinct = if (
      selectClause.setQuantifier() != null &&
      selectClause.setQuantifier().DISTINCT() != null) {
      Distinct(withProject)
    } else {
      withProject
    }

    // Window
    val withWindow = withDistinct.optionalMap(windowClause)(withWindowClause)

    // Hint
    selectClause.hints.asScala.foldRight(withWindow)(withHints)
  }

可以看出由于示例中只有select/from/where三个节点,此函数中除了这三个节点外都为null

image-20220707110628401

image-20220707110654105

由于lateralView长度为0,故withLateralView = UnresolvedRelation [PERSON]

image-20220707111021881

变量withFilter 主要由withWhereClause函数进行拼接:

val withFilter = withLateralView.optionalMap(whereClause)(withWhereClause) // 调用withWhereClause函数进行拼接
...
private def withWhereClause(ctx: WhereClauseContext, plan: LogicalPlan): LogicalPlan = {
    Filter(expression(ctx.booleanExpression), plan) // 调用expression函数后,创建Filter类
}
...
protected def expression(ctx: ParserRuleContext): Expression = typedVisit(ctx) // 调用访问函数

protected def typedVisit[T](ctx: ParseTree): T = {
    ctx.accept(this).asInstanceOf[T] // 内部其实是调用了 visitPredicated函数
}
...
override def visitPredicated(ctx: PredicatedContext): Expression = withOrigin(ctx) {
    val e = expression(ctx.valueExpression) // 不断递归调用where的子类
    if (ctx.predicate != null) {
      withPredicate(e, ctx.predicate)
    } else {
      e
    }
  }

image-20220707114026067

where子节点的递归过程就不再赘述了,最终返回的e = GreaterThan[age > 18]

image-20220707114451883

经过Filter(expression(ctx.booleanExpression), plan)函数,最终拼成一个Filter【logicalPlan】返回给withFilter变量

image-20220707123453448

image-20220707114810505

此时的withFilter

image-20220707114943831

然后执行到了expressions变量,调用visitNamedExpressionSeq访问者函数执行namedExpressionSeq子节点:

val expressions = visitNamedExpressionSeq(selectClause.namedExpressionSeq)

image-20220707115204209

此函数依然是递归调用,过程不再赘述,其最终返回:UnresolvedStar

image-20220707115234578

由于聚合函数等条件判断皆不符合,直接访问内部createProject函数

image-20220707115443633

可以看到是将 name 表达式和 filter表达式构成了project【logicalPlan】

Project(namedExpressions, withFilter)

接下来各种语法判断

image-20220707115726630

由于本示例没有用到这些语法,故最终 withSelectQuerySpecification 函数返回为Project【logicalPlan】

同样上层visitRegularQuerySpecification返回为Project【logicalPlan】

层层返回,最终返回到sql函数中,plan返回为 Project【logicalPlan】

image-20220707120111426

可以看出返回的locagicalPlan 完全符合最开始程序打印的explan的结果

== Parsed Logical Plan ==
'Project [*]
+- 'Filter ('AGE > 18)
   +- 'UnresolvedRelation `PERSON`

至此sparksql - Unresolved阶段结束

接下来是Analyzer 【resovled logicialPlan】 解析阶段

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

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

相关文章

MySql查询的生命周期和性能优化思路

目录 前言 1. 为什么查询性能差 2. 一次查询的生命周期 2.1 客户端与服务端通信 2.2 查询缓存 2.3 解析器 2.4 预处理器 2.5 优化器 2.6 查询引擎 2.7 存储引擎 3. 查询性能优化的思路 4.总结 前言 一说到mysql的查询性能优化&#xff0c;相信很多人能说出来很多的技…

AT32F407/437使用FreeRTOS并实现ping客户端

示例目的 基于以太网络&#xff0c;实现ping客户端已检测网络联机。 支持型号 AT32F407xx AT32F437xx 主要使用外设 EMAC GPIO USART 快速使用方法硬件资源 1) 指示灯LED2/LED3 2) USART1(PA9/PA10) 3) AT-START-F407/ AT-START-F437实验板 4) 以太网连接线软件资源 1) SourceC…

sql函数coalesce和parse_url

学习函数系列&#xff1a; coalesce coalesce函数可以用来排除null值。 coalesce(a, b,c,d) 参数的个数没有限制 返回第一个参数中非null的值。 select help coalesce\G; [ 1. row ] name | COALESCE description | Syntax: COALESCE(value,…) Returns the first non-NUL…

15-JavaSE基础巩固练习:多态、接口、抽象类的综合练习

多态的综合练习 1、需求 狗类 属性&#xff1a;年龄&#xff0c;颜色行为&#xff1a; eat(String something)&#xff1a;表示吃东西lookHome()&#xff1a;看家 猫类 属性&#xff1a;年龄&#xff0c;颜色行为&#xff1a; eat(String something)&#xff1a;吃东西catch…

5G工业互联阶段二:5G产线工控网

5G深入核心生产环节的第二个阶段&#xff0c;主要是实现产线内部通信5G化。以工控5G化为主&#xff0c;并综合考虑数采、安全通信等。大致示意如下&#xff1a; 工艺部件工控通信5G化&#xff1a; 如上图所述&#xff0c;以产线主PLC为中心&#xff0c;大致分为主PLC到产线内机…

Spark 3.0 - 5.ML Pipeline 实战之电影影评情感分析

目录 一.引言 二.Stage1 - 数据准备 1.数据样式 2.读取数据 3.平均得分与 Top 5 4.训练集、测试集划分 三.Stage-2 - Comment 分词 1.Tokenizer &#x1f645;&#x1f3fb;‍♀️ 2.JieBa 分词 &#x1f646;&#x1f3fb;‍♀️ 2.1 Jieba 分词示例 2.2 自定义 Jie…

系统设计 system design 干货笔记

参考大佬的博客 https://www.lecloud.net/post/9246290032/scalability-for-dummies-part-3-cache 参考的github https://github.com/donnemartin/system-design-primer#step-2-review-the-scalability-article scalability 1 Clone 每台服务器都包含完全相同的代码库&#…

SOLIDWORKS 2023 3D Creator 云端结构设计新功能

3DEXPERIENCE平台更新版本已经与大家见面&#xff0c;今天微辰三维与大家分享3D Creator 云端结构设计新功能&#xff0c;让我们先一起来看看视频—— SOLIDWORKS 2023 3D 云端结构设计新功能点击观看3D Creator 云端结构设计新功能 如今&#xff0c;我们的设计生产工作不仅要面…

Linux进阶-Makefile

make工具&#xff1a;找出修改过的文件&#xff0c;根据依赖关系&#xff0c;找出受影响的相关文件&#xff0c;最后按照规则单独编译这些文件。 Makefile文件&#xff1a;记录依赖关系和编译规则。 Makefile本质&#xff1a;无论多么复杂的语法&#xff0c;都是为了更好地解决…

m认知无线电网络中频谱感知的按需路由算法matlab仿真

目录 1.算法概述 2.仿真效果预览 3.MATLAB部分代码预览 4.完整MATLAB程序 1.算法概述 使用无线电用户的频率范围在 9kHz 到 275GHz[3]&#xff0c;由于无线通信环境中的干扰、信道衰落和无线电收发设备自身属性等的影响&#xff0c;大部分无线电设备只能工作在 50GHz 以下。…

融媒体解决方案-最新全套文件

融媒体解决方案-最新全套文件一、建设背景二、建设思路三、建设方案二、获取 - 融媒体全套最新解决方案合集一、建设背景 随着互联网的快速发展&#xff0c;社会已步入全媒体时代&#xff0c;各媒体机构积极探索传统媒体转型之路。 为巩固壮大主流思想舆论&#xff0c;不断提…

对数的应用:放缩x轴或者y轴以更好地表达函数的结果

对数尺度的作用 yAxnyAx^nyAxn 在实验中 AAA 和 nnn 都是未知数&#xff0c;现在我想求出 AAA 和 nnn假设 n1.5,A1n1.5, A1n1.5,A1&#xff0c;那么我们可以做个图看看 x np.linspace(1,10,10) y 1 * x**3 plt.plot(y)如果我做实验恰好得到一些点&#xff0c;那么我很难知道…

【全志T113-S3_100ask】14-1 linux采集usb摄像头实现拍照(FFmpeg、fswebcam)

【全志T113-S3_100ask】14-1 linux采集usb摄像头实现拍照背景&#xff08;一&#xff09;FFmpeg1、简介&#xff1a;2、交叉编译FFmpeg3、测试&#xff08;二&#xff09;fswebcam1、背景2、交叉编译fswebcam3、测试背景 在开发板上有一个csi转dvp接口的摄像头&#xff0c;但是…

前端入门到放弃(VUE、ES6,简单到不得了)

VSCode 使用 1、安装常用插件 切换到插件标签页 安装一下基本插件 2、创建项目 vscode 很轻量级&#xff0c;本身没有新建项目的选项&#xff0c;创建一个空文件夹就可以当做一个项目 3、创建网页 创建文件&#xff0c;命名为 index.html 快捷键 !快速创建网页模板 h1 回…

精益管理学会|什么是ECRS改善方法?

ECRS是IE工程改善、精益生產管理改善的四大法宝。 针对现有的生产线进行改善时&#xff0c;常见的做法是对现有的生产线进行绘制各工站的工时山积表如下圖所見&#xff0c;然后对各工站的动作单元进行ECRS 改善。 E&#xff1a;不需要的可进行 Eliminate &#xff08;取消&…

Telegraf-Influxdb-Grafana容器化部署拓展(Https、AD域、告警集成)并监控Cisco设备指标

前言&#xff1a; 还记得在去年的笔记中提到过使用python的pysnmp模块&#xff0c;配合Influxdb&#xff0c;Grafana收集Cisco设备指标。链接如下&#xff1a;https://blog.csdn.net/tushanpeipei/article/details/117329794 。在该实例中&#xff0c;我们通过python编写脚本收…

第一节 Maven核心程序解压与配置

1、Maven 官网地址 首页&#xff1a; Maven – Maven Repositories (apache.org)https://maven.apache.org/repositories/index.html下载页面&#xff1a; Maven – Download Apache Mavenhttps://maven.apache.org/download.cgi下载链接&#xff1a; 具体下载地址&#xff…

【微信小程序】列表渲染wx:for

&#x1f3c6;今日学习目标&#xff1a;第十二期——列表渲染wx:for &#x1f603;创作者&#xff1a;颜颜yan_ ✨个人主页&#xff1a;颜颜yan_的个人主页 ⏰预计时间&#xff1a;20分钟 &#x1f389;专栏系列&#xff1a;我的第一个微信小程序 文章目录前言效果图< block…

同花顺_代码解析_交易系统_J01_08

本文通过对同花顺中现成代码进行解析&#xff0c;用以了解同花顺相关策略设计的思想 目录 J_01 MACD系统 J_02 布林带系统 J_03 趋向指标 J_04 乖离系统 J_05 KDJ系统 J_07 容量比率系统 J_08 威廉系统 J_01 MACD系统 分析MACD柱状线&#xff0c;由绿变红(负变正)&…

Bootstrap实例(四)

目录&#xff1a; &#xff08;1&#xff09;bootstrtap实例&#xff08;轮廓&#xff09; &#xff08;2&#xff09;bootstrap三列布局 &#xff08;3&#xff09;bootstrap&#xff08;标签页&#xff09; &#xff08;4&#xff09;bootstrap&#xff08;end&#xff0…