This is an automated email from the ASF dual-hosted git repository.
wusheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-graalvm-distro.git
The following commit(s) were added to refs/heads/main by this push:
new f5d9748 Replace HierarchyDefinitionService GroovyShell with
Java-backed closures
f5d9748 is described below
commit f5d97485b1e5da5a575ef75c79dbb5a10c1c0c2e
Author: Wu Sheng <[email protected]>
AuthorDate: Fri Feb 20 16:54:54 2026 +0800
Replace HierarchyDefinitionService GroovyShell with Java-backed closures
HierarchyDefinitionService.MatchingRule used GroovyShell.evaluate() to
compile 4 closure expressions from hierarchy-definition.yml at runtime.
This blocks GraalVM native image where dynamic Groovy compilation is
unavailable.
Replace with same-FQCN class in server-core-for-graalvm that provides
pre-built Closure<Boolean> implementations in Java, keyed by rule name.
Unknown rule names fail fast at startup. HierarchyService (the caller)
is unchanged — MatchingRule.getClosure() still returns Closure<Boolean>.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
DISTRO-POLICY.md | 7 +-
.../server-core-for-graalvm/pom.xml | 2 +
.../core/config/HierarchyDefinitionService.java | 193 +++++++++++++++++++++
3 files changed, 199 insertions(+), 3 deletions(-)
diff --git a/DISTRO-POLICY.md b/DISTRO-POLICY.md
index 000cd9f..4e53997 100644
--- a/DISTRO-POLICY.md
+++ b/DISTRO-POLICY.md
@@ -133,13 +133,13 @@ Each upstream JAR that has replacement classes gets a
corresponding `*-for-graal
`oap-graalvm-server` depends on `*-for-graalvm` JARs instead of originals.
Original upstream JARs are forced to `provided` scope via
`<dependencyManagement>` to prevent transitive leakage.
-### 18 Same-FQCN Replacement Classes Across 12 Modules
+### 19 Same-FQCN Replacement Classes Across 12 Modules
**Non-trivial replacements (load pre-compiled assets from manifests):**
| Module | Replacement Classes | Purpose |
|---|---|---|
-| `server-core-for-graalvm` | `OALEngineLoaderService`, `AnnotationScan`,
`SourceReceiverImpl`, `MeterSystem`, `CoreModuleConfig` | Load from manifests
instead of Javassist/ClassPath; config with @Setter |
+| `server-core-for-graalvm` | `OALEngineLoaderService`, `AnnotationScan`,
`SourceReceiverImpl`, `MeterSystem`, `CoreModuleConfig`,
`HierarchyDefinitionService` | Load from manifests instead of
Javassist/ClassPath; config with @Setter; Java-backed closures instead of
GroovyShell |
| `library-util-for-graalvm` | `YamlConfigLoaderUtils` | Set config fields via
setter instead of reflection |
| `meter-analyzer-for-graalvm` | `DSL`, `FilterExpression` | Load pre-compiled
MAL Groovy scripts from manifest |
| `log-analyzer-for-graalvm` | `DSL`, `LogAnalyzerModuleConfig` | Load
pre-compiled LAL scripts; config with @Setter |
@@ -295,7 +295,8 @@ and ~1254 Groovy scripts stored in JARs.
- [ ] `resource-config.json` for runtime config files loaded via
`ResourceUtils.read()`
- [ ] Configure gRPC/Netty/Protobuf for native image
- [ ] GraalVM Feature class for SkyWalking-specific registrations
-- [ ] Resolve remaining code-level blockers: OTEL SPI, Envoy SPI,
HierarchyDefinitionService Groovy
+- [x] Resolve HierarchyDefinitionService Groovy blocker (same-FQCN replacement
with Java-backed closures)
+- [ ] Verify OTEL and Envoy ServiceLoader SPI work in native image (GraalVM
supports META-INF/services natively)
- [ ] Get OAP server booting as native image with BanyanDB
### Phase 4: Harden & Test
diff --git a/oap-libs-for-graalvm/server-core-for-graalvm/pom.xml
b/oap-libs-for-graalvm/server-core-for-graalvm/pom.xml
index ba898f4..deba20d 100644
--- a/oap-libs-for-graalvm/server-core-for-graalvm/pom.xml
+++ b/oap-libs-for-graalvm/server-core-for-graalvm/pom.xml
@@ -64,6 +64,8 @@
<exclude>org/apache/skywalking/oap/server/core/source/SourceReceiverImpl.class</exclude>
<exclude>org/apache/skywalking/oap/server/core/analysis/meter/MeterSystem.class</exclude>
<exclude>org/apache/skywalking/oap/server/core/analysis/meter/MeterSystem$*.class</exclude>
+
<exclude>org/apache/skywalking/oap/server/core/config/HierarchyDefinitionService.class</exclude>
+
<exclude>org/apache/skywalking/oap/server/core/config/HierarchyDefinitionService$*.class</exclude>
</excludes>
</filter>
</filters>
diff --git
a/oap-libs-for-graalvm/server-core-for-graalvm/src/main/java/org/apache/skywalking/oap/server/core/config/HierarchyDefinitionService.java
b/oap-libs-for-graalvm/server-core-for-graalvm/src/main/java/org/apache/skywalking/oap/server/core/config/HierarchyDefinitionService.java
new file mode 100644
index 0000000..a946708
--- /dev/null
+++
b/oap-libs-for-graalvm/server-core-for-graalvm/src/main/java/org/apache/skywalking/oap/server/core/config/HierarchyDefinitionService.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.core.config;
+
+import groovy.lang.Closure;
+import java.io.FileNotFoundException;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.server.core.CoreModuleConfig;
+import org.apache.skywalking.oap.server.core.UnexpectedException;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.query.type.Service;
+import org.apache.skywalking.oap.server.library.util.ResourceUtils;
+import org.yaml.snakeyaml.Yaml;
+
+import static java.util.stream.Collectors.toMap;
+
+/**
+ * Same-FQCN replacement of upstream HierarchyDefinitionService.
+ *
+ * <p>Replaces {@code GroovyShell.evaluate()} in {@code MatchingRule} with
+ * pre-built Java-backed {@link Closure} objects. Eliminates runtime Groovy
+ * compilation, which is not available in GraalVM native image.
+ *
+ * <p>The 4 matching rules from {@code hierarchy-definition.yml} are
implemented
+ * as anonymous {@code Closure<Boolean>} subclasses in {@link #RULE_REGISTRY}.
+ * Unknown rule names fail fast at startup.
+ */
+@Slf4j
+public class HierarchyDefinitionService implements
org.apache.skywalking.oap.server.library.module.Service {
+
+ /**
+ * Pre-built matching rule closures keyed by rule name from
hierarchy-definition.yml.
+ * Each closure takes two Service arguments (upper, lower) and returns
Boolean.
+ */
+ private static final Map<String, Closure<Boolean>> RULE_REGISTRY;
+
+ static {
+ RULE_REGISTRY = new HashMap<>();
+
+ // name: "{ (u, l) -> u.name == l.name }"
+ RULE_REGISTRY.put("name", new Closure<Boolean>(null) {
+ public Boolean doCall(final Service u, final Service l) {
+ return Objects.equals(u.getName(), l.getName());
+ }
+ });
+
+ // short-name: "{ (u, l) -> u.shortName == l.shortName }"
+ RULE_REGISTRY.put("short-name", new Closure<Boolean>(null) {
+ public Boolean doCall(final Service u, final Service l) {
+ return Objects.equals(u.getShortName(), l.getShortName());
+ }
+ });
+
+ // lower-short-name-remove-ns:
+ // "{ (u, l) -> { if(l.shortName.lastIndexOf('.') > 0)
+ // return u.shortName == l.shortName.substring(0,
l.shortName.lastIndexOf('.'));
+ // return false; } }"
+ RULE_REGISTRY.put("lower-short-name-remove-ns", new
Closure<Boolean>(null) {
+ public Boolean doCall(final Service u, final Service l) {
+ int dot = l.getShortName().lastIndexOf('.');
+ if (dot > 0) {
+ return Objects.equals(
+ u.getShortName(),
+ l.getShortName().substring(0, dot));
+ }
+ return false;
+ }
+ });
+
+ // lower-short-name-with-fqdn:
+ // "{ (u, l) -> { if(u.shortName.lastIndexOf(':') > 0)
+ // return u.shortName.substring(0, u.shortName.lastIndexOf(':'))
+ // == l.shortName.concat('.svc.cluster.local');
+ // return false; } }"
+ RULE_REGISTRY.put("lower-short-name-with-fqdn", new
Closure<Boolean>(null) {
+ public Boolean doCall(final Service u, final Service l) {
+ int colon = u.getShortName().lastIndexOf(':');
+ if (colon > 0) {
+ return Objects.equals(
+ u.getShortName().substring(0, colon),
+ l.getShortName() + ".svc.cluster.local");
+ }
+ return false;
+ }
+ });
+ }
+
+ @Getter
+ private final Map<String, Map<String, MatchingRule>> hierarchyDefinition;
+ @Getter
+ private Map<String, Integer> layerLevels;
+ private Map<String, MatchingRule> matchingRules;
+
+ public HierarchyDefinitionService(CoreModuleConfig moduleConfig) {
+ this.hierarchyDefinition = new HashMap<>();
+ this.layerLevels = new HashMap<>();
+ if (moduleConfig.isEnableHierarchy()) {
+ this.init();
+ this.checkLayers();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void init() {
+ try {
+ Reader applicationReader =
ResourceUtils.read("hierarchy-definition.yml");
+ Yaml yaml = new Yaml();
+ Map<String, Map> config = yaml.loadAs(applicationReader,
Map.class);
+ Map<String, Map<String, String>> hierarchy = (Map<String,
Map<String, String>>) config.get("hierarchy");
+ Map<String, String> matchingRules = (Map<String, String>)
config.get("auto-matching-rules");
+ this.layerLevels = (Map<String, Integer>)
config.get("layer-levels");
+ this.matchingRules = matchingRules.entrySet().stream().map(entry
-> {
+ MatchingRule matchingRule = new MatchingRule(entry.getKey(),
entry.getValue());
+ return Map.entry(entry.getKey(), matchingRule);
+ }).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
+ hierarchy.forEach((layer, lowerLayers) -> {
+ Map<String, MatchingRule> rules = new HashMap<>();
+ lowerLayers.forEach((lowerLayer, ruleName) -> {
+ rules.put(lowerLayer, this.matchingRules.get(ruleName));
+ });
+ this.hierarchyDefinition.put(layer, rules);
+ });
+ } catch (FileNotFoundException e) {
+ throw new UnexpectedException("hierarchy-definition.yml not
found.", e);
+ }
+ }
+
+ private void checkLayers() {
+ this.layerLevels.keySet().forEach(layer -> {
+ if (Layer.nameOf(layer).equals(Layer.UNDEFINED)) {
+ throw new IllegalArgumentException(
+ "hierarchy-definition.yml " + layer + " is not a valid
layer name.");
+ }
+ });
+ this.hierarchyDefinition.forEach((layer, lowerLayers) -> {
+ Integer layerLevel = this.layerLevels.get(layer);
+ if (this.layerLevels.get(layer) == null) {
+ throw new IllegalArgumentException(
+ "hierarchy-definition.yml layer-levels: " + layer + " is
not defined");
+ }
+
+ for (String lowerLayer : lowerLayers.keySet()) {
+ Integer lowerLayerLevel = this.layerLevels.get(lowerLayer);
+ if (lowerLayerLevel == null) {
+ throw new IllegalArgumentException(
+ "hierarchy-definition.yml layer-levels: " +
lowerLayer + " is not defined.");
+ }
+ if (layerLevel <= lowerLayerLevel) {
+ throw new IllegalArgumentException(
+ "hierarchy-definition.yml hierarchy: " + layer + "
layer-level should be greater than " + lowerLayer + " layer-level.");
+ }
+ }
+ });
+ }
+
+ @Getter
+ public static class MatchingRule {
+ private final String name;
+ private final String expression;
+ private final Closure<Boolean> closure;
+
+ public MatchingRule(final String name, final String expression) {
+ this.name = name;
+ this.expression = expression;
+ this.closure = RULE_REGISTRY.get(name);
+ if (this.closure == null) {
+ throw new IllegalArgumentException(
+ "Unknown hierarchy matching rule: " + name
+ + ". Known rules: " + RULE_REGISTRY.keySet());
+ }
+ }
+ }
+}