This is an automated email from the ASF dual-hosted git repository. wusheng pushed a commit to branch feature/mal-closure-companion-class in repository https://gitbox.apache.org/repos/asf/skywalking.git
commit 6bb6236fef96fd2f8224e8a114e842af5754c7f3 Author: Wu Sheng <[email protected]> AuthorDate: Thu Mar 19 15:15:57 2026 +0800 MAL v2: replace LambdaMetafactory with companion class pattern for closures 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 method lookup at runtime. Replace with a companion class per closure (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 via new CompanionClass() — no reflection, no LambdaMetafactory, no indirection. 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. Remove addClosureMethod(), addStaticLocalVariableTable(), the isStatic parameter on addLocalVariableTable(), and generateCompanionMethod() — all replaced by generateCompanionBody() which inlines the closure logic directly into the companion's SAM method. --- 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/MALClassGeneratorTest.java | 45 ++++ 5 files changed, 250 insertions(+), 296 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/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();
