Skip to content

第32讲:OAL语言,原来定义创造一门新语言如此轻松(下)

了解了 OAL 语言的基础语法以及 official_analysis.oal 文件中典型的 OAL 语句之后,我们来看 official_analysis.oal 文件是如何被解析的。

在generate-tool-grammar 模块中会使 antlr4-maven-plugin 这个 Maven 插件处理 OALParser.g4 以及 OALLexer.g4 文件,得到相应的辅助类,如下图所示,这与前文 Antlr4 示例相同:

generate-tool 模块会使用上述辅助类识别 official_analysis.oal 文件并最终转换成 OALScripts 对象,相关的代码片段如下:

java
// 构造 official_analysis.oal 文件的完整路径
String scriptFilePath = StringUtil.join(File.separatorChar,
    modulePath, "src", "main", "resources", "official_analysis.oal");
// 创建 ScriptParser实例
ScriptParser scriptParser =  
      ScriptParser.createFromFile(scriptFilePath);
// 调用 parse()方法识别 official_analysis.oal文件
OALScripts oalScripts = scriptParser.parse();

在 ScriptParser.parse() 方法中可以看到,generate-tool 模块与前文示例一样,也是使用 Listener 模式遍历生成的抽象语法树(AST)。最后生成的 OALScripts 对象底层封装了一个 List<AnalysisResult>,每个 AnalysisResult 对应一条 OAL 语句。

下面以 instance_jvm_old_gc_time 这条 OAL 语句生成的 AST 为例介绍 OALListener 中各个回调方法的执行流程,下图是该语句生成的简化版 AST,其中的红色箭头标记了 ParseTreeWalker 遍历各个节点的路径:

另外,上图还按序标记了在对应节点上触发的 OALListener 方法,下面是这些方法的具体功能:

(1).enterAggregationStatement() 方法:创建该语句对应的 AnalysisResult 对象。

(2).exitVariable() 方法:填充 AnalysisResult 的 varName、metricsName、tableName 三个字段,会对大小写以及下划线进行处理。

(3).enterSource() 方法:填充 AnalysisResult 的 sourceName、sourceScopeId 两个字段。

(4).enterSourceAttribute() 方法:填充 AnalysisResult 的 sourceAttribute 字段。

(5).enterFilterStatement() 方法:创建 ConditionExpression 对象。

(6)~(8) 三个方法分别填充 ConditionExpression 对象中的三个字段。

(9).exitFilterStatement() 方法:将 ConditionExpression 添加到 AnalysisResult 中的 filterExpressionsParserResult 集合。

(10).enterFunctionName() 方法:填充 AnalysisResult 的 aggregationFunctionName 字段。

到此为止,该 AnalysisResult 填充的字段如下图所示:

(11).exitAggregationStatement() 方法:这里使用 DeepAnalysis 分析前 10 步从 OAL 语句获取到的信息,从而完整填充整个 AnalysisResult 对象。

  • 在 DeepAnalysis 中首先会根据 aggregationFunctionName 确定当前指标的类型并填充 metricsClassName 字段。示例中的 longAvg 会查找到 LongAvgMetrics 类,如下图所示:
  • 接下来会查找 LongAvgMetrics 类中 @Entrance 注解标注的入口方法,即 combine() 方法,创建相应的 EntryMethod 对象填充到 entryMethod 字段中。这里生成的 EntryMethod 对象不仅包含入口方法的名称,还会根据入口方法参数上的注解生成相应的参数表达式。

依然以 LongAvgMetrics 为例,combine() 方法的定义如下:

java
@Entrance
public void combine(@SourceFrom long summation, @ConstOne int count) {
    this.summation += summation;
    this.count += count;
}

之前我们只关心了方法内的具体逻辑,没有关注方法以及参数上的注解。@Entrance 注解标识了该方法为入口方法,@SourceFrom 标识了该参数来自 OAL 语句前面指定的 source.sourceAttribute,即 ServiceInstanceJVMGC.time,@ ConstOne 标识该参数固定为 1。

查找 @Entrance 标注的方法的逻辑比较简单,就是遍历 LongAvgMetrics 以及父类所有方法即可。这里来看处理 @SourceFrom 以及 @ConstOne 注解的相关代码如下:

java
EntryMethod entryMethod = new EntryMethod();
result.setEntryMethod(entryMethod); 
// @Entrance注解标注的入口方法名
entryMethod.setMethodName(entranceMethod.getName());

// 根据入口方法的参数设置参数代码
for (Parameter parameter : entranceMethod.getParameters()) {
    Annotation[] parameterAnnotations = parameter.getAnnotations();
    Annotation annotation = parameterAnnotations[0];
    if (annotation instanceof SourceFrom) {
        entryMethod.addArg("source." + ClassMethodUtil
             .toGetMethod(result.getSourceAttribute()) + "()");
    } else if (annotation instanceof ConstOne) {
        entryMethod.addArg("1");
    } 
    // 还有针对其他注解的处理,例如 @Expression、@ExpressionArg0等,不再展开
}

最终创建的 EntryMethod 对象如下图所示:

  • 扫描 LongAvgMetrics 中的全部字段,将所有 @Column 注解标注的字段封装成 DataColumn 对象记录到 persistentFields 集合中。

  • 根据 sourceName 字段的值从 generator-scope-meta.yml 文件中查找该 source 默认新增的字段,如下图所示,InstanceJvmOldGcTimeMetrics 需要新增 entityId、serviceId 两个字段,这也与我们之前的分析相同。这些新增字段会记录到 fieldsFromSource 集合中。

到此为止,instance_jvm_old_gc_time 这条 OAL 语句对应的 AnalysisResult 对象填充完毕。在第 11 步 exitAggregationStatement() 方法的最后,会将该 AnalysisResult 对象记录到 OALScripts.metricsStmts 集合中,作为后续 FreeMarker 填充模板的数据。

MetricsImplementor 模板

在完成 official_analysis.oal 文件中全部 OAL 语句的处理之后,会将 OALScripts 对象传入到 FileGenerator 中完成 Java 代码生成。在 FileGenerator 的构造方法中会初始化 Configuration 对象,与前面介绍的 FreeMarker 示例相同。

在 FileGenerator.generate() 方法中会遍历全部 AnalysisResult 对象,为每个 AnalysisResult 对象生成相应的 Metrics 类以及 Dispatcher 类。创建 Metrics 类时使用的是 MetricsImplementor.ftl 模板文件,相关代码如下:

java
void generateMetricsImplementor(AnalysisResult result,Writer output) {
   configuration.getTemplate("MetricsImplementor.ftl")
        .process(result, output);
}

在 MetricsImplementor.ftl 这个模板文件中,我们重点关注一下字段生成的逻辑以及 id() 方法的逻辑,具体如下所示:

dart
<!-- 直接获取 AnalysisResult中相应的字段值,生成的@Stream注解-->
@Stream(name = "${tableName}", 
        scopeId = ${sourceScopeId}, 
        builder = ${metricsName}Metrics.Builder.class, 
        processor = MetricsStreamProcessor.class)
<!-- 填充类名以及父类名称 -->        
public class ${metricsName}Metrics extends ${metricsClassName} 
    implements WithMetadata {
<!-- 遍历 AnalysisResult中的 fieldsFromSource集合,生成相应的字段 -->
<#list fieldsFromSource as sourceField>
    <!-- 设置 @Column注解的名称 -->
    @Setter @Getter @Column(columnName = "${sourceField.columnName}") 
    <!-- 根据配置是否添加 @IDColumn注解 -->
    <#if sourceField.isID()>@IDColumn</#if>   
    private ${sourceField.typeName} ${sourceField.fieldName};
</#list>

    @Override public String id() {
        String splitJointId = String.valueOf(getTimeBucket());
<!-- 遍历 AnalysisResult中的 fieldsFromSource集合 -->
<#list fieldsFromSource as sourceField>
    <#if sourceField.isID()> <!-- 根据ID配置决定是否参与构造Document Id-->
        <#if sourceField.getTypeName() == "java.lang.String">
            splitJointId += Const.ID_SPLIT + ${sourceField.fieldName};
        <#else>
            splitJointId += Const.ID_SPLIT +
                  String.valueOf(${sourceField.fieldName});
        </#if>
    </#if>
</#list>
        return splitJointId;
    }
    <!-- 省略后续其他方法 -->
}

Metrics 类其他方法的生成方式与 id() 方法类似,只是使用的 AnalysisResult 字段不同。你可以将 MetricsImplementor.ftl 模板与 InstanceJvmOldGcTimeMetrics.java 进行比较,更便于理解。

DispatcherTemplate 模板

在前文介绍 Dispatcher 的时候提到,不同 Dispatcher 实现会对关联的 Source 进行分析并转换成 Metrics 传入到 MetricsStreamProcessor 进行后续的流处理。例如,ServiceInstanceJVMGCDispatcher 会将一个 ServiceInstanceJVMGC 对象转换成下图展示的四个 Metrics 对象:

相应的,FileGenerator 生成 Dispatcher 实现类的代码之前,会将由同一个 Source 衍生出来的 Metrics 封装到一个 DispatcherContext 对象, DispatcherContext 的核心字段如下:

java
private String source; // Source 名称
private String packageName; // Dispatcher所在包名
// 该 Source所有衍生 Metrics对应的 AnalysisResult对象集合
private List<AnalysisResult> metrics = new ArrayList<>();

生成 Dispatcher 实现类使用的是 DispatcherTemplate.ftl 模板文件,填充的数据来自 DispatcherContext,入口是 FileGenerator.generateDispatcher() 方法:

java
void generateDispatcher(AnalysisResult result, Writer output) {
    String scopeName = result.getSourceName(); 
    // 根据 Source名称查找相应的 DispatcherContext
    DispatcherContext context =  
         allDispatcherContext.getAllContext().get(scopeName);
    // 生成 Dispatcher实现类的代码并写入到指定文件中
    configuration.getTemplate("DispatcherTemplate.ftl")
           .process(context, output);
}

接下来看 DispatcherTemplate.ftl 的实现,它会遍历 DispatcherContext.metrics 集合为每个 Metrics 生成相应的 do*() 方法,核心实现如下:

java
<#list metrics as metrics> <!-- 遍历 DispatcherContext.metrics 集合 -->
    <!-- 填充 do*()方法签名 -->
    <!-- 示例中对应 doInstanceJvmOldGcTime(ServiceInstanceJVMGC)方法 -->
    private void do${metrics.metricsName}(${source} source) {
        <!-- 创建相应的Metrics实例 -->
        ${metrics.metricsName}Metrics metrics = 
              new ${metrics.metricsName}Metrics();
    <#if metrics.filterExpressions??>
        <!--根据 OAL语句中 filter表达式生成对source过滤的代码(略) -->
    </#if>
        <!-- 下面开始填充 Metrics对象 -->
        metrics.setTimeBucket(source.getTimeBucket());
    <#list metrics.fieldsFromSource as field>
        metrics.${field.fieldSetter}(source.${field.fieldGetter}());
    </#list>
        <!-- 根据 AnalysisResult.entryMethod 生成调用入口方法的代码 -->
        <!-- doInstanceJvmOldGcTime() 方法中调用的是 combine() 方法 -->
        metrics.${metrics.entryMethod.methodName}(
          <!-- 生成入口方法的参数 -->
           <#list metrics.entryMethod.argsExpressions as arg>
             ${arg}<#if arg_has_next>, </#if></#list>
         );
        MetricsStreamProcessor.getInstance().in(metrics);
    }
</#list>

为了更好地理解 FreeMarker 填充数据的逻辑,你可以将 DispatcherTemplate.ftl 模板生成 do*() 方法的逻辑与生成后的 ServiceInstanceJVMGCDispatcher.doInstanceJvmOldGcTime() 方法进行比较。

内置 oal 引擎

从 6.3 版本的开始,SkyWalking 将 OAL 引擎内置到 OAP Server 中,在 OAP Server 启动时会动态生成 Metrics 类实现以及相应 Dispatcher 实现,我们可以在 CoreModuleProvider.prepare() 方法中看到下面这段代码(6.3 版本之后的代码):

java
oalEngine = OALEngineLoader.get();
oalEngine.setStreamListener(streamAnnotationListener);
oalEngine.setDispatcherListener(receiver.getDispatcherManager());
oalEngine.start(getClass().getClassLoader());

在 oalEngine.start() 方法中会解析 official_analysis.oal 文件得到 OALScripts 对象,然后使用 Javassist 和 FreeMarker 生成的 Metrics 和 Dispatcher 实现类,最后直接通过传入的 ClassLoader 加载到 JVM。

6.3 版本中生成代码的核心实现与 6.2 版本中生成代码的核心实现基本类似,只有下面的微小区别:

  • 6.3 版本之后的 OAL 语法略有改动,但改动很小,并不影响理解。

  • 6.3 版本之后在运行时生成代码,而 6.2 版本是在编译期生成。

  • 6.3 版本之后生成代码时使用了 Javassist 和 FreeMarker,6.2 版本只使用了 FreeMarker。

  • 6.3 版本之后生成的代码默认不会保存到磁盘中,我们可以在环境变量中设置 SW_OAL_ENGINE_DEBUG=Y 参数保存运行时生成的 Java 文件。如果你感兴趣可以对比 6.2 和 6.3 生成的 Java 代码,会发现两者区别不大。