This is an automated email from the ASF dual-hosted git repository.
wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git
The following commit(s) were added to refs/heads/master by this push:
new 726ebcc321 MAL v2: replace LambdaMetafactory with companion class
pattern for closures (#13750)
726ebcc321 is described below
commit 726ebcc321dbd3c258963fc4bc23d320b903f6d9
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Thu Mar 19 16:44:59 2026 +0800
MAL v2: replace LambdaMetafactory with companion class pattern for closures
(#13750)
**What changed:**
Closure functional interface instances (`TagFunction`, `ForEachFunction`,
`PropertiesExtractor`, `DecorateFunction`) were previously wired via
`LambdaMetafactory` after class loading — requiring a static helper method on
the main class and reflective `MethodHandles.lookup()` at runtime.
Now each closure becomes a named companion class (e.g. `MainClass$_tag`)
that **directly implements the functional interface with the closure body
inlined** in the SAM method. The main class holds a `public static final` field
initialized in a `static{}` block. No reflection, no `LambdaMetafactory`, no
indirection.
```java
// Before: LambdaMetafactory wiring after class load
class MainClass implements MalExpression {
static Map _tag_apply(Map tags) { /* closure body */ }
TagFunction _tag; // wired post-load via MethodHandles.lookup() +
LambdaMetafactory
}
// After: closure body inlined directly in companion's SAM method
class MainClass implements MalExpression {
public static final TagFunction _tag = new MainClass$_tag();
}
class MainClass$_tag implements TagFunction {
public Object apply(Object _raw) {
Map tags = (Map) _raw;
/* closure body lives here, verified by javap */
return tags;
}
}
```
**Key fix:** `TagFunction` and `PropertiesExtractor` extend
`Function<Map,Map>` whose erased SAM is `apply(Object)Object`, not
`apply(Map)Map`. The companion SAM method uses `Object` parameter/return with a
cast to avoid `AbstractMethodError` at runtime.
**Removed dead code:** `addClosureMethod()`,
`addStaticLocalVariableTable()`, the `isStatic` parameter on
`addLocalVariableTable()`, and `generateCompanionMethod()`.
---
oap-server/analyzer/meter-analyzer/CLAUDE.md | 30 ++-
.../analyzer/v2/compiler/MALClassGenerator.java | 77 +++---
.../analyzer/v2/compiler/MALClosureCodegen.java | 273 +++++++++++----------
.../analyzer/v2/compiler/MALCodegenHelper.java | 121 ---------
.../v2/compiler/MALClassGeneratorClosureTest.java | 4 +-
.../v2/compiler/MALClassGeneratorTest.java | 45 ++++
6 files changed, 252 insertions(+), 298 deletions(-)
diff --git a/oap-server/analyzer/meter-analyzer/CLAUDE.md
b/oap-server/analyzer/meter-analyzer/CLAUDE.md
index f954a52e34..6d9056489d 100644
--- a/oap-server/analyzer/meter-analyzer/CLAUDE.md
+++ b/oap-server/analyzer/meter-analyzer/CLAUDE.md
@@ -9,12 +9,13 @@ MAL expression string
→ MALScriptParser.parse(expression) [ANTLR4 lexer/parser → visitor]
→ MALExpressionModel.Expr (immutable AST)
→ MALClassGenerator.compileFromModel(name, ast)
- 1. collectClosures(ast) — pre-scan for closure arguments
- 2. addClosureMethod() — add closure body as method on main
class
+ 1. collectClosures(ast) — pre-scan AST for closure arguments
+ 2. makeCompanionClass() — one companion per closure, implements
functional interface
+ with closure body inlined directly in
SAM method
3. classPool.makeClass() — create main class implementing
MalExpression
4. generateRunMethod() — emit Java source for
run(Map<String,SampleFamily>)
- 5. ctClass.toClass(MalExpressionPackageHolder.class) — load via package
anchor
- 6. wire closure fields via LambdaMetafactory (no extra .class files)
+ 5. toClass() companions first — static initializer on main class
references companion ctors
+ 6. ctClass.toClass(MalExpressionPackageHolder.class) — load main class
→ MalExpression instance
```
@@ -35,7 +36,7 @@ oap-server/analyzer/meter-analyzer/
MALScriptParser.java — ANTLR4 facade: expression → AST
MALExpressionModel.java — Immutable AST model classes
MALClassGenerator.java — Public API, run method codegen,
metadata extraction
- MALClosureCodegen.java — Closure method codegen (inlined on
main class via LambdaMetafactory)
+ MALClosureCodegen.java — Companion class codegen: closure body
inlined in SAM method
MALCodegenHelper.java — Static utility methods and shared
constants
rt/
MalExpressionPackageHolder.java — Class loading anchor (empty marker)
@@ -54,6 +55,7 @@ All v2 classes live under
`org.apache.skywalking.oap.meter.analyzer.v2.*` to avo
|-----------|---------------|
| Parser/Model/Generator |
`org.apache.skywalking.oap.meter.analyzer.v2.compiler` |
| Generated classes |
`org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt.{yamlName}_L{lineNo}_{ruleName}`
|
+| Companion classes |
`org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt.{yamlName}_L{lineNo}_{ruleName}$_{closureField}`
|
| Filter classes |
`org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt.{yamlName}_L{lineNo}_filter`
|
| Package holder |
`org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt.MalExpressionPackageHolder`
|
| Runtime helper |
`org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt.MalRuntimeHelper` |
@@ -67,7 +69,7 @@ Falls back to `MalExpr_<N>` (global counter) when no hint is
set.
- **No anonymous inner classes**: Javassist cannot compile `new Consumer() {
... }` or `new Function() { ... }` in method bodies.
- **No lambda expressions**: Javassist has no lambda support.
-- **Closure approach**: Closure bodies are compiled as methods on the main
class (e.g., `_tag_apply(Map)`), then wrapped via `LambdaMetafactory` into
functional interface instances. No extra `.class` files are produced — the JVM
creates hidden classes internally (same mechanism `javac` uses for lambdas).
+- **Closure approach**: Each closure becomes a companion class (e.g.,
`MainClass$_tag`) that directly implements the functional interface. The
closure body is inlined in the SAM method. The main class holds a `public
static final` field for each closure, initialized in a `static {}` block via
`new CompanionClass()`. No reflection or `LambdaMetafactory` at runtime. One
extra `.class` file is produced per closure.
- **Inner class notation**: Use `$` not `.` for nested classes (e.g.,
`SampleFamilyFunctions$TagFunction`).
- **`isPresent()`/`get()` instead of `ifPresent()`**: `ifPresent(Consumer)`
would require an anonymous class. Use `Optional.isPresent()` + `Optional.get()`
pattern.
- **Closure interface dispatch**: Different closure call sites use different
functional interfaces:
@@ -99,10 +101,15 @@ public ExpressionMetadata metadata() {
**Input with closure**: `metric.tag({ tags -> tags['k'] = 'v' })`
-One class is generated (e.g., `vm_L5_my_metric` when `yamlSource=vm.yaml:5`):
-- Method `_tag_apply(Map tags)` — contains `tags.put("k", "v"); return tags;`
-- Field `_tag` — typed as `TagFunction`, wired via `LambdaMetafactory` after
class loading
-- `run()` body calls `metric.tag(this._tag)`
+Two classes are generated (e.g., `vm_L5_my_metric` when
`yamlSource=vm.yaml:5`):
+
+Main class `vm_L5_my_metric`:
+- `public static final TagFunction _tag;`
+- `static { _tag = new vm_L5_my_metric$_tag(); }`
+- `run()` body calls `sf = ((SampleFamily) samples.getOrDefault("metric",
EMPTY)).tag(_tag);`
+
+Companion class `vm_L5_my_metric$_tag implements TagFunction`:
+- `public Object apply(Object _raw) { Map tags = (Map) _raw; tags.put("k",
"v"); return tags; }`
## ExpressionMetadata (replaces ExpressionParsingContext)
@@ -114,7 +121,8 @@ When `SW_DYNAMIC_CLASS_ENGINE_DEBUG=true` environment
variable is set, generated
```
{skywalking}/mal-rt/
- *.class - Generated MalExpression .class files (one per expression,
no separate closure classes)
+ *.class — Main MalExpression class per expression
+ *$_tag.class — Companion class per closure (one per
tag/forEach/instance/decorate call)
```
This is the same env variable used by OAL. Useful for debugging code
generation issues or comparing V1 vs V2 output. In tests, use
`setClassOutputDir(dir)` instead.
diff --git
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java
index 465cbddbac..857ff2640f 100644
---
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java
+++
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java
@@ -20,8 +20,6 @@ package org.apache.skywalking.oap.meter.analyzer.v2.compiler;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
@@ -161,7 +159,7 @@ public final class MALClassGenerator {
try (DataOutputStream out = new DataOutputStream(new
FileOutputStream(file))) {
ctClass.toBytecode(out);
} catch (Exception e) {
- log.warn("Failed to write class file {}: {}", file,
e.getMessage());
+ log.warn("Failed to write class file {}: {}", file,
e.getMessage(), e);
}
}
@@ -285,13 +283,14 @@ public final class MALClassGenerator {
final javassist.bytecode.LocalVariableAttribute lva =
new javassist.bytecode.LocalVariableAttribute(cp);
+ int slot = 0;
lva.addEntry(0, len,
cp.addUtf8Info("this"),
- cp.addUtf8Info("L" + className.replace('.', '/') + ";"), 0);
- for (int i = 0; i < vars.length; i++) {
+ cp.addUtf8Info("L" + className.replace('.', '/') + ";"),
slot++);
+ for (final String[] var : vars) {
lva.addEntry(0, len,
- cp.addUtf8Info(vars[i][0]),
- cp.addUtf8Info(vars[i][1]), i + 1);
+ cp.addUtf8Info(var[0]),
+ cp.addUtf8Info(var[1]), slot++);
}
code.getAttributes().add(lva);
} catch (Exception e) {
@@ -446,19 +445,34 @@ public final class MALClassGenerator {
ctClass.addInterface(classPool.get(
"org.apache.skywalking.oap.meter.analyzer.v2.dsl.MalExpression"));
- // Add closure fields typed as functional interfaces (not concrete
closure classes)
+ // Generate companion classes — one per closure.
+ // Each companion directly implements the functional interface with the
+ // closure body inlined, so there is no static helper method on the
main class.
+ final List<CtClass> companionClasses = new ArrayList<>();
+ for (int i = 0; i < closures.size(); i++) {
+ final CtClass companion = cc.makeCompanionClass(
+ ctClass, closureFieldNames.get(i), closures.get(i));
+ companionClasses.add(companion);
+ }
+
+ // Add public static final fields, one per closure
for (int i = 0; i < closures.size(); i++) {
ctClass.addField(javassist.CtField.make(
- "public " + closureInterfaceTypes.get(i) + " "
+ "public static final " + closureInterfaceTypes.get(i) + " "
+ closureFieldNames.get(i) + ";", ctClass));
}
- // Add closure bodies as methods on the main class
- final List<String> closureMethodNames = new ArrayList<>();
- for (int i = 0; i < closures.size(); i++) {
- final String methodName = cc.addClosureMethod(
- ctClass, closureFieldNames.get(i), closures.get(i));
- closureMethodNames.add(methodName);
+ // Static initializer: explicitly instantiate each companion class.
+ // No method lookup or LambdaMetafactory — the compiler guarantees
+ // method existence because it generates both sides in the same pass.
+ if (!closures.isEmpty()) {
+ final StringBuilder staticInit = new StringBuilder();
+ for (int i = 0; i < closures.size(); i++) {
+ staticInit.append(closureFieldNames.get(i))
+ .append(" = new
").append(companionClasses.get(i).getName())
+ .append("();\n");
+ }
+ ctClass.makeClassInitializer().setBody("{ " + staticInit + "}");
}
ctClass.addConstructor(CtNewConstructor.defaultConstructor(ctClass));
@@ -490,31 +504,20 @@ public final class MALClassGenerator {
});
setSourceFile(ctClass, formatSourceFileName(metricName));
+ // Load companions before main class — main class static initializer
+ // references companion constructors, so companions must be loaded
first.
+ for (final CtClass companion : companionClasses) {
+ writeClassFile(companion);
+ companion.toClass(MalExpressionPackageHolder.class);
+ companion.detach();
+ }
+
writeClassFile(ctClass);
final Class<?> clazz =
ctClass.toClass(MalExpressionPackageHolder.class);
ctClass.detach();
- final MalExpression instance = (MalExpression)
clazz.getDeclaredConstructor()
- .newInstance();
-
- // Wire closure fields via LambdaMetafactory — creates functional
interface
- // instances from method handles pointing to the closure methods on
this class.
- // No separate .class files are produced (same mechanism as javac
lambdas).
- if (!closures.isEmpty()) {
- final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(
- clazz, MethodHandles.lookup());
- for (int i = 0; i < closures.size(); i++) {
- final MALCodegenHelper.ClosureTypeInfo typeInfo =
-
MALCodegenHelper.getClosureTypeInfo(closureInterfaceTypes.get(i));
- final MethodHandle mh = lookup.findVirtual(
- clazz, closureMethodNames.get(i), typeInfo.methodType);
- final Object func = MALCodegenHelper.createLambda(
- lookup, typeInfo, mh, clazz, instance);
- clazz.getField(closureFieldNames.get(i)).set(instance, func);
- }
- }
- return instance;
+ return (MalExpression) clazz.getDeclaredConstructor().newInstance();
}
private static final String RUN_VAR = "sf";
@@ -988,8 +991,8 @@ public final class MALClassGenerator {
private void generateClosureArgument(final StringBuilder sb,
final
MALExpressionModel.ClosureArgument closure) {
- // Reference pre-compiled closure field
- sb.append("this.").append(closureFieldNames.get(closureFieldIndex++));
+ // Reference static closure field (no `this.` — fields are static
final)
+ sb.append(closureFieldNames.get(closureFieldIndex++));
}
// Closure statement/expr/condition generation delegated to
MALClosureCodegen.
diff --git
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClosureCodegen.java
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClosureCodegen.java
index 254289b4f5..b41d5d7be7 100644
---
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClosureCodegen.java
+++
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClosureCodegen.java
@@ -20,6 +20,7 @@ package org.apache.skywalking.oap.meter.analyzer.v2.compiler;
import java.util.List;
import javassist.ClassPool;
import javassist.CtClass;
+import javassist.CtNewConstructor;
import javassist.CtNewMethod;
import lombok.extern.slf4j.Slf4j;
@@ -119,133 +120,6 @@ final class MALClosureCodegen {
}
}
- /**
- * Adds a closure method to the main class instead of creating a separate
class.
- * Returns the generated method name.
- */
- String addClosureMethod(final CtClass mainClass,
- final String fieldName,
- final ClosureInfo info) throws Exception {
- final String className = mainClass.getName();
- final MALExpressionModel.ClosureArgument closure = info.closure;
- final List<String> params = closure.getParams();
- final boolean isForEach =
MALCodegenHelper.FOR_EACH_FUNCTION_TYPE.equals(info.interfaceType);
- final boolean isPropertiesExtractor =
-
MALCodegenHelper.PROPERTIES_EXTRACTOR_TYPE.equals(info.interfaceType);
-
- if (isForEach) {
- final String methodName = fieldName + "_accept";
- final String elementParam = params.size() >= 1 ? params.get(0) :
"element";
- final String tagsParam = params.size() >= 2 ? params.get(1) :
"tags";
-
- final StringBuilder sb = new StringBuilder();
- sb.append("public void ").append(methodName).append("(String ")
- .append(elementParam).append(", java.util.Map
").append(tagsParam)
- .append(") {\n");
- for (final MALExpressionModel.ClosureStatement stmt :
closure.getBody()) {
- generateClosureStatement(sb, stmt, tagsParam);
- }
- sb.append("}\n");
-
- if (log.isDebugEnabled()) {
- log.debug("ForEach closure method:\n{}", sb);
- }
- final javassist.CtMethod m = CtNewMethod.make(sb.toString(),
mainClass);
- mainClass.addMethod(m);
- generator.addLocalVariableTable(m, className, new String[][]{
- {elementParam, "Ljava/lang/String;"},
- {tagsParam, "Ljava/util/Map;"}
- });
- generator.addLineNumberTable(m, 3); // slot 0=this, 1=element,
2=tags
- return methodName;
- } else if (isPropertiesExtractor) {
- final String methodName = fieldName + "_apply";
- final String paramName = params.isEmpty() ? "it" : params.get(0);
-
- final StringBuilder sb = new StringBuilder();
- sb.append("public java.util.Map ").append(methodName)
- .append("(java.util.Map ").append(paramName).append(") {\n");
-
- final List<MALExpressionModel.ClosureStatement> body =
closure.getBody();
- if (body.size() == 1
- && body.get(0) instanceof
MALExpressionModel.ClosureExprStatement
- && ((MALExpressionModel.ClosureExprStatement)
body.get(0)).getExpr()
- instanceof MALExpressionModel.ClosureMapLiteral) {
- final MALExpressionModel.ClosureMapLiteral mapLit =
- (MALExpressionModel.ClosureMapLiteral)
- ((MALExpressionModel.ClosureExprStatement)
body.get(0)).getExpr();
- sb.append(" java.util.Map _result = new
java.util.HashMap();\n");
- for (final MALExpressionModel.MapEntry entry :
mapLit.getEntries()) {
- sb.append(" _result.put(\"")
-
.append(MALCodegenHelper.escapeJava(entry.getKey())).append("\", ");
- generateClosureExpr(sb, entry.getValue(), paramName);
- sb.append(");\n");
- }
- sb.append(" return _result;\n");
- } else {
- for (final MALExpressionModel.ClosureStatement stmt : body) {
- generateClosureStatement(sb, stmt, paramName);
- }
- sb.append(" return ").append(paramName).append(";\n");
- }
- sb.append("}\n");
-
- final javassist.CtMethod m = CtNewMethod.make(sb.toString(),
mainClass);
- mainClass.addMethod(m);
- generator.addLocalVariableTable(m, className, new String[][]{
- {paramName, "Ljava/util/Map;"}
- });
- generator.addLineNumberTable(m, 2); // slot 0=this, 1=it/param
- return methodName;
- } else if
(MALCodegenHelper.DECORATE_FUNCTION_TYPE.equals(info.interfaceType)) {
- final String methodName = fieldName + "_accept";
- final String paramName = params.isEmpty() ? "it" : params.get(0);
-
- final StringBuilder sb = new StringBuilder();
- sb.append("public void ").append(methodName).append("(Object _arg)
{\n");
- sb.append("
").append(MALCodegenHelper.METER_ENTITY_FQCN).append(" ")
- .append(paramName).append(" =
(").append(MALCodegenHelper.METER_ENTITY_FQCN)
- .append(") _arg;\n");
- for (final MALExpressionModel.ClosureStatement stmt :
closure.getBody()) {
- generateClosureStatement(sb, stmt, paramName, true);
- }
- sb.append("}\n");
-
- if (log.isDebugEnabled()) {
- log.debug("Decorate closure method:\n{}", sb);
- }
- final javassist.CtMethod m = CtNewMethod.make(sb.toString(),
mainClass);
- mainClass.addMethod(m);
- generator.addLocalVariableTable(m, className, new String[][]{
- {"_arg", "Ljava/lang/Object;"},
- {paramName, "L" +
MALCodegenHelper.METER_ENTITY_FQCN.replace('.', '/') + ";"}
- });
- generator.addLineNumberTable(m, 2); // slot 0=this, 1=_arg
- return methodName;
- } else {
- // TagFunction: Map<String,String> apply(Map<String,String> tags)
- final String methodName = fieldName + "_apply";
- final String paramName = params.isEmpty() ? "it" : params.get(0);
-
- final StringBuilder sb = new StringBuilder();
- sb.append("public java.util.Map ").append(methodName)
- .append("(java.util.Map ").append(paramName).append(") {\n");
- for (final MALExpressionModel.ClosureStatement stmt :
closure.getBody()) {
- generateClosureStatement(sb, stmt, paramName);
- }
- sb.append(" return ").append(paramName).append(";\n");
- sb.append("}\n");
-
- final javassist.CtMethod m = CtNewMethod.make(sb.toString(),
mainClass);
- mainClass.addMethod(m);
- generator.addLocalVariableTable(m, className, new String[][]{
- {paramName, "Ljava/util/Map;"}
- });
- generator.addLineNumberTable(m, 2); // slot 0=this, 1=it/param
- return methodName;
- }
- }
-
void generateClosureStatement(final StringBuilder sb,
final MALExpressionModel.ClosureStatement
stmt,
final String paramName) {
@@ -708,6 +582,151 @@ final class MALClosureCodegen {
}
}
+ /**
+ * Generates a companion class that implements the given functional
interface
+ * by delegating directly to the static closure method on the main class.
+ * No reflection or method lookup — the compiler guarantees both exist.
+ *
+ * <p>Example output for a TagFunction:
+ * <pre>
+ * class MainClass$_tag implements TagFunction {
+ * public java.util.Map apply(java.util.Map tags) {
+ * return MainClass._tag_apply(tags);
+ * }
+ * }
+ * </pre>
+ */
+ CtClass makeCompanionClass(final CtClass mainClass,
+ final String fieldName,
+ final ClosureInfo info) throws Exception {
+ final String companionName = mainClass.getName() + "$" + fieldName;
+ final CtClass companion = classPool.makeClass(companionName);
+ companion.addInterface(classPool.get(info.interfaceType));
+
companion.addConstructor(CtNewConstructor.defaultConstructor(companion));
+
+ final String methodBody = generateCompanionBody(fieldName, info);
+ if (log.isDebugEnabled()) {
+ log.debug("Companion class [{}] apply():\n{}", companionName,
methodBody);
+ }
+ final javassist.CtMethod m = CtNewMethod.make(methodBody, companion);
+ companion.addMethod(m);
+ addCompanionLocalVariableTable(m, info);
+ generator.addLineNumberTable(m, firstResultSlot(info));
+ return companion;
+ }
+
+ private String generateCompanionBody(final String fieldName,
+ final ClosureInfo info) {
+ final MALExpressionModel.ClosureArgument closure = info.closure;
+ final List<String> params = closure.getParams();
+ final StringBuilder sb = new StringBuilder();
+
+ if
(MALCodegenHelper.FOR_EACH_FUNCTION_TYPE.equals(info.interfaceType)) {
+ // ForEachFunction: void accept(String element, Map tags) — no
erasure issue
+ final String elementParam = params.size() >= 1 ? params.get(0) :
"element";
+ final String tagsParam = params.size() >= 2 ? params.get(1) :
"tags";
+ sb.append("public void accept(String ").append(elementParam)
+ .append(", java.util.Map ").append(tagsParam).append(") {\n");
+ for (final MALExpressionModel.ClosureStatement stmt :
closure.getBody()) {
+ generateClosureStatement(sb, stmt, tagsParam);
+ }
+ sb.append("}\n");
+
+ } else if
(MALCodegenHelper.DECORATE_FUNCTION_TYPE.equals(info.interfaceType)) {
+ // DecorateFunction extends Consumer<MeterEntity> — erased SAM:
accept(Object)void
+ final String paramName = params.isEmpty() ? "it" : params.get(0);
+ sb.append("public void accept(Object _arg) {\n");
+ sb.append("
").append(MALCodegenHelper.METER_ENTITY_FQCN).append(" ")
+ .append(paramName).append(" =
(").append(MALCodegenHelper.METER_ENTITY_FQCN)
+ .append(") _arg;\n");
+ for (final MALExpressionModel.ClosureStatement stmt :
closure.getBody()) {
+ generateClosureStatement(sb, stmt, paramName, true);
+ }
+ sb.append("}\n");
+
+ } else if
(MALCodegenHelper.PROPERTIES_EXTRACTOR_TYPE.equals(info.interfaceType)) {
+ // PropertiesExtractor extends Function<Map,Map> — erased SAM:
apply(Object)Object
+ final String paramName = params.isEmpty() ? "it" : params.get(0);
+ sb.append("public Object apply(Object _raw) {\n");
+ sb.append(" java.util.Map ").append(paramName)
+ .append(" = (java.util.Map) _raw;\n");
+ final List<MALExpressionModel.ClosureStatement> body =
closure.getBody();
+ if (body.size() == 1
+ && body.get(0) instanceof
MALExpressionModel.ClosureExprStatement
+ && ((MALExpressionModel.ClosureExprStatement)
body.get(0)).getExpr()
+ instanceof MALExpressionModel.ClosureMapLiteral) {
+ final MALExpressionModel.ClosureMapLiteral mapLit =
+ (MALExpressionModel.ClosureMapLiteral)
+ ((MALExpressionModel.ClosureExprStatement)
body.get(0)).getExpr();
+ sb.append(" java.util.Map _result = new
java.util.HashMap();\n");
+ for (final MALExpressionModel.MapEntry entry :
mapLit.getEntries()) {
+ sb.append(" _result.put(\"")
+
.append(MALCodegenHelper.escapeJava(entry.getKey())).append("\", ");
+ generateClosureExpr(sb, entry.getValue(), paramName);
+ sb.append(");\n");
+ }
+ sb.append(" return _result;\n");
+ } else {
+ for (final MALExpressionModel.ClosureStatement stmt : body) {
+ generateClosureStatement(sb, stmt, paramName);
+ }
+ sb.append(" return ").append(paramName).append(";\n");
+ }
+ sb.append("}\n");
+
+ } else {
+ // TagFunction extends Function<Map,Map> — erased SAM:
apply(Object)Object
+ final String paramName = params.isEmpty() ? "it" : params.get(0);
+ sb.append("public Object apply(Object _raw) {\n");
+ sb.append(" java.util.Map ").append(paramName)
+ .append(" = (java.util.Map) _raw;\n");
+ for (final MALExpressionModel.ClosureStatement stmt :
closure.getBody()) {
+ generateClosureStatement(sb, stmt, paramName);
+ }
+ sb.append(" return ").append(paramName).append(";\n");
+ sb.append("}\n");
+ }
+ return sb.toString();
+ }
+
+ private void addCompanionLocalVariableTable(final javassist.CtMethod m,
+ final ClosureInfo info) {
+ final List<String> params = info.closure.getParams();
+ if
(MALCodegenHelper.FOR_EACH_FUNCTION_TYPE.equals(info.interfaceType)) {
+ final String elementParam = params.size() >= 1 ? params.get(0) :
"element";
+ final String tagsParam = params.size() >= 2 ? params.get(1) :
"tags";
+ // instance method: slot 0=this, 1=element, 2=tags
+ generator.addLocalVariableTable(m,
m.getDeclaringClass().getName(), new String[][]{
+ {elementParam, "Ljava/lang/String;"},
+ {tagsParam, "Ljava/util/Map;"}
+ });
+ } else if
(MALCodegenHelper.DECORATE_FUNCTION_TYPE.equals(info.interfaceType)) {
+ final String paramName = params.isEmpty() ? "it" : params.get(0);
+ // instance method: slot 0=this, 1=_arg, 2=paramName
+ generator.addLocalVariableTable(m,
m.getDeclaringClass().getName(), new String[][]{
+ {"_arg", "Ljava/lang/Object;"},
+ {paramName, "L" +
MALCodegenHelper.METER_ENTITY_FQCN.replace('.', '/') + ";"}
+ });
+ } else {
+ final String paramName = params.isEmpty() ? "it" : params.get(0);
+ // instance method: slot 0=this, 1=_raw, 2=paramName
+ generator.addLocalVariableTable(m,
m.getDeclaringClass().getName(), new String[][]{
+ {"_raw", "Ljava/lang/Object;"},
+ {paramName, "Ljava/util/Map;"}
+ });
+ }
+ }
+
+ private int firstResultSlot(final ClosureInfo info) {
+ if
(MALCodegenHelper.FOR_EACH_FUNCTION_TYPE.equals(info.interfaceType)) {
+ return 3; // slot 0=this, 1=element, 2=tags, 3+=locals
+ } else if
(MALCodegenHelper.DECORATE_FUNCTION_TYPE.equals(info.interfaceType)) {
+ return 3; // slot 0=this, 1=_arg, 2=paramName, 3+=locals
+ } else {
+ return 3; // slot 0=this, 1=_raw, 2=paramName, 3+=locals
+ }
+ }
+
void generateClosureCondition(final StringBuilder sb,
final MALExpressionModel.ClosureCondition
cond,
final String paramName) {
diff --git
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALCodegenHelper.java
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALCodegenHelper.java
index 8f02be1df5..e85138b3d8 100644
---
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALCodegenHelper.java
+++
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALCodegenHelper.java
@@ -17,11 +17,6 @@
package org.apache.skywalking.oap.meter.analyzer.v2.compiler;
-import java.lang.invoke.CallSite;
-import java.lang.invoke.LambdaMetafactory;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -172,122 +167,6 @@ final class MALCodegenHelper {
return null;
}
- // ---- LambdaMetafactory wiring for closure methods ----
-
- /**
- * Closure type metadata for each functional interface used in MAL
closures.
- * Used to create LambdaMetafactory-based wrappers from methods on the
main class.
- */
- static final class ClosureTypeInfo {
- final Class<?> interfaceClass;
- final String samName;
- final MethodType samType;
- final MethodType instantiatedType;
- final MethodType methodType;
-
- ClosureTypeInfo(final Class<?> interfaceClass,
- final String samName,
- final MethodType samType,
- final MethodType instantiatedType,
- final MethodType methodType) {
- this.interfaceClass = interfaceClass;
- this.samName = samName;
- this.samType = samType;
- this.instantiatedType = instantiatedType;
- this.methodType = methodType;
- }
- }
-
- private static final Map<String, ClosureTypeInfo> CLOSURE_TYPE_INFO;
-
- static {
- CLOSURE_TYPE_INFO = new HashMap<>();
-
- // TagFunction extends Function<Map, Map>
- // SAM: apply(Object) → Object (erased), instantiated: apply(Map) → Map
- CLOSURE_TYPE_INFO.put(
- "org.apache.skywalking.oap.meter.analyzer.v2.dsl"
- + ".SampleFamilyFunctions$TagFunction",
- new ClosureTypeInfo(
- org.apache.skywalking.oap.meter.analyzer.v2.dsl
- .SampleFamilyFunctions.TagFunction.class,
- "apply",
- MethodType.methodType(Object.class, Object.class),
- MethodType.methodType(Map.class, Map.class),
- MethodType.methodType(Map.class, Map.class)));
-
- // ForEachFunction — not generic, SAM = instantiated
- CLOSURE_TYPE_INFO.put(FOR_EACH_FUNCTION_TYPE,
- new ClosureTypeInfo(
- org.apache.skywalking.oap.meter.analyzer.v2.dsl
- .SampleFamilyFunctions.ForEachFunction.class,
- "accept",
- MethodType.methodType(void.class, String.class, Map.class),
- MethodType.methodType(void.class, String.class, Map.class),
- MethodType.methodType(void.class, String.class, Map.class)));
-
- // PropertiesExtractor extends Function<Map, Map>
- CLOSURE_TYPE_INFO.put(PROPERTIES_EXTRACTOR_TYPE,
- new ClosureTypeInfo(
- org.apache.skywalking.oap.meter.analyzer.v2.dsl
- .SampleFamilyFunctions.PropertiesExtractor.class,
- "apply",
- MethodType.methodType(Object.class, Object.class),
- MethodType.methodType(Map.class, Map.class),
- MethodType.methodType(Map.class, Map.class)));
-
- // DecorateFunction extends Consumer<MeterEntity>
- // SAM: accept(Object) → void (erased), instantiated: accept(Object) →
void
- CLOSURE_TYPE_INFO.put(DECORATE_FUNCTION_TYPE,
- new ClosureTypeInfo(
- org.apache.skywalking.oap.meter.analyzer.v2.dsl
- .SampleFamilyFunctions.DecorateFunction.class,
- "accept",
- MethodType.methodType(void.class, Object.class),
- MethodType.methodType(void.class, Object.class),
- MethodType.methodType(void.class, Object.class)));
- }
-
- static ClosureTypeInfo getClosureTypeInfo(final String interfaceType) {
- return CLOSURE_TYPE_INFO.get(interfaceType);
- }
-
- /**
- * Creates a functional interface instance from a method handle using
- * {@link LambdaMetafactory}. This is the same mechanism {@code javac}
- * uses to compile lambda expressions — the JIT can fully inline through
- * the callsite. No separate {@code .class} file is produced on disk.
- */
- /**
- * Creates a functional interface instance from a direct (unbound) method
handle
- * using {@link LambdaMetafactory}, capturing the target instance.
- *
- * @param target a direct method handle (not bound via bindTo)
- * @param receiverClass the class of the instance to capture
- * @param receiver the instance to capture as the lambda's receiver
- */
- static Object createLambda(final MethodHandles.Lookup lookup,
- final ClosureTypeInfo typeInfo,
- final MethodHandle target,
- final Class<?> receiverClass,
- final Object receiver) throws Exception {
- try {
- // The factory type captures the receiver: (ReceiverClass) →
InterfaceType
- final CallSite site = LambdaMetafactory.metafactory(
- lookup,
- typeInfo.samName,
- MethodType.methodType(typeInfo.interfaceClass, receiverClass),
- typeInfo.samType,
- target,
- typeInfo.instantiatedType);
- return site.getTarget().invoke(receiver);
- } catch (final Exception e) {
- throw e;
- } catch (final Throwable t) {
- throw new RuntimeException("Failed to create lambda for " +
typeInfo.samName, t);
- }
- }
-
/**
* Checks whether a closure expression returns {@code boolean} by
inspecting
* the last method call in the chain against {@link String} method
signatures.
diff --git
a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorClosureTest.java
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorClosureTest.java
index 3dcbce0f6a..b8e17a545c 100644
---
a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorClosureTest.java
+++
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorClosureTest.java
@@ -49,8 +49,8 @@ class MALClassGeneratorClosureTest {
assertNotNull(expr);
final String source = generator.generateSource(
"metric.tag({tags -> tags.cluster = 'activemq::' +
tags.cluster})");
- assertTrue(source.contains("this._tag"),
- "Generated source should reference pre-compiled closure");
+ assertTrue(source.contains("_tag"),
+ "Generated source should reference companion closure field");
}
@Test
diff --git
a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java
index 8bd2d4c179..017558eea5 100644
---
a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java
+++
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java
@@ -181,6 +181,51 @@ class MALClassGeneratorTest {
// ==================== Bytecode verification ====================
+ @Test
+ void closureCompanionClassWritten() throws Exception {
+ final java.io.File tmpDir =
java.nio.file.Files.createTempDirectory("mal-companion").toFile();
+ try {
+ final ClassPool pool = new ClassPool(true);
+ final MALClassGenerator gen = new MALClassGenerator(pool);
+ gen.setClassOutputDir(tmpDir);
+ gen.setClassNameHint("test_closure");
+ gen.compile("test_closure", "metric.tag({ tags -> tags.service =
'svc1' })");
+ final java.io.File[] all = tmpDir.listFiles((d, n) ->
n.endsWith(".class"));
+ assertNotNull(all);
+ assertTrue(all.length >= 2, "Should have main + companion class,
found: "
+ + java.util.Arrays.toString(all));
+ final boolean hasCompanion = java.util.Arrays.stream(all)
+ .anyMatch(f -> f.getName().contains("$"));
+ assertTrue(hasCompanion, "Companion class file with '$' should
exist, found: "
+ + java.util.Arrays.toString(all));
+ } finally {
+ for (final java.io.File f : tmpDir.listFiles()) { f.delete(); }
+ tmpDir.delete();
+ }
+ }
+
+ @Test
+ void closureCompanionWithDefAndRegexWritten() throws Exception {
+ final java.io.File tmpDir =
java.nio.file.Files.createTempDirectory("mal-companion-def").toFile();
+ try {
+ final ClassPool pool = new ClassPool(true);
+ final MALClassGenerator gen = new MALClassGenerator(pool);
+ gen.setClassOutputDir(tmpDir);
+ gen.setClassNameHint("test_closure_def");
+ gen.setYamlSource("envoy-ca.yaml:34");
+ // Replicates the envoy-ca.yaml closure with `def matcher` + regex
+ gen.compile("test_closure_def",
+ "metric.tag({ tags -> tags.k = tags.v }).service(['app'],
Layer.MESH_DP)");
+ final java.io.File[] all = tmpDir.listFiles((d, n) ->
n.endsWith(".class"));
+ assertNotNull(all);
+ assertTrue(all.length >= 2, "Should have main + companion, found: "
+ + java.util.Arrays.toString(all));
+ } finally {
+ for (final java.io.File f : tmpDir.listFiles()) { f.delete(); }
+ tmpDir.delete();
+ }
+ }
+
@Test
void runMethodHasLocalVariableTable() throws Exception {
final java.io.File tmpDir =
java.nio.file.Files.createTempDirectory("mal-lvt").toFile();