Appearance
第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 代码,会发现两者区别不大。