You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/02/18 14:14:24 UTC
(skywalking-graalvm-distro) 01/04: Add OAL immigration: same-FQCN replacement classes + annotation manifests
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
commit 9a4c809699d35130f33ea0714cab253c601cf8ad
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Wed Feb 18 22:03:57 2026 +0800
Add OAL immigration: same-FQCN replacement classes + annotation manifests
Replace 3 upstream classes via Maven classpath precedence to eliminate
runtime Javassist code generation and Guava ClassPath scanning:
- OALEngineLoaderService: loads pre-compiled OAL classes from build-time
manifests instead of running the OAL engine at startup
- AnnotationScan: reads annotation manifests from META-INF/annotation-scan/
instead of Guava ClassPath.from() scanning
- SourceReceiverImpl: reads dispatcher/decorator manifests instead of
Guava classpath scanning
OALClassExporter enhanced to scan 6 annotation/interface types at build
time (ScopeDeclaration, Stream, Disable, MultipleDisable, SourceDispatcher,
ISourceDecorator) and write manifests.
Verification: OALClassExporterTest (3 tests) + PrecompiledRegistrationTest
(12 tests) covering manifest-vs-classpath comparison, class loading, scope
registration, and source→dispatcher→metrics chain consistency.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../server/buildtools/oal/OALClassExporter.java | 87 ++++-
.../buildtools/oal/OALClassExporterTest.java | 26 ++
oap-graalvm-server/pom.xml | 15 +
.../oap/server/core/annotation/AnnotationScan.java | 125 +++++++
.../server/core/oal/rt/OALEngineLoaderService.java | 129 +++++++
.../oap/server/core/source/SourceReceiverImpl.java | 112 ++++++
.../graalvm/PrecompiledRegistrationTest.java | 392 +++++++++++++++++++++
7 files changed, 881 insertions(+), 5 deletions(-)
diff --git a/build-tools/oal-exporter/src/main/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporter.java b/build-tools/oal-exporter/src/main/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporter.java
index ded57e7..614763d 100644
--- a/build-tools/oal-exporter/src/main/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporter.java
+++ b/build-tools/oal-exporter/src/main/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporter.java
@@ -17,8 +17,12 @@
package org.apache.skywalking.oap.server.buildtools.oal;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.reflect.ClassPath;
import java.io.IOException;
+import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -31,12 +35,17 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.aop.server.receiver.mesh.MeshOALDefine;
import org.apache.skywalking.oal.v2.OALEngineV2;
import org.apache.skywalking.oal.v2.generator.OALClassGeneratorV2;
+import org.apache.skywalking.oap.server.core.analysis.Disable;
import org.apache.skywalking.oap.server.core.analysis.DisableRegister;
+import org.apache.skywalking.oap.server.core.analysis.ISourceDecorator;
+import org.apache.skywalking.oap.server.core.analysis.MultipleDisable;
+import org.apache.skywalking.oap.server.core.analysis.SourceDispatcher;
import org.apache.skywalking.oap.server.core.annotation.AnnotationScan;
import org.apache.skywalking.oap.server.core.oal.rt.CoreOALDefine;
import org.apache.skywalking.oap.server.core.oal.rt.DisableOALDefine;
import org.apache.skywalking.oap.server.core.oal.rt.OALDefine;
import org.apache.skywalking.oap.server.core.source.DefaultScopeDefine;
+import org.apache.skywalking.oap.server.core.source.ScopeDeclaration;
import org.apache.skywalking.oap.server.core.storage.StorageBuilderFactory;
import org.apache.skywalking.oap.server.fetcher.cilium.CiliumOALDefine;
import org.apache.skywalking.oap.server.receiver.browser.provider.BrowserOALDefine;
@@ -47,13 +56,11 @@ import org.apache.skywalking.oap.server.receiver.jvm.provider.JVMOALDefine;
/**
* Build-time tool that runs the OAL engine for all 9 OALDefine configurations,
- * exports generated .class files (metrics, builders, dispatchers), and writes
- * manifest files listing all generated class names.
+ * exports generated .class files and manifest files, and scans the classpath for
+ * hardcoded annotated classes and interface implementations used at runtime.
*
* OAL script files are loaded from the skywalking submodule directly via
* additionalClasspathElements in the exec-maven-plugin configuration.
- *
- * Uses the existing SW_OAL_ENGINE_DEBUG export mechanism in OALClassGeneratorV2.
*/
@Slf4j
public class OALClassExporter {
@@ -129,8 +136,31 @@ public class OALClassExporter {
writeManifest(metaInf.resolve("oal-dispatcher-classes.txt"), dispatcherClasses);
writeManifest(metaInf.resolve("oal-disabled-sources.txt"), disabledSources);
- log.info("OAL Class Exporter: done - {} metrics, {} dispatchers, {} disabled sources",
+ log.info("OAL Class Exporter: {} metrics, {} dispatchers, {} disabled sources",
metricsClasses.size(), dispatcherClasses.size(), disabledSources.size());
+
+ // ---- Annotation & interface scanning for hardcoded classes ----
+ Path annotationScanDir = metaInf.resolve("annotation-scan");
+ Files.createDirectories(annotationScanDir);
+
+ ImmutableSet<ClassPath.ClassInfo> allClasses = ClassPath
+ .from(OALClassExporter.class.getClassLoader())
+ .getTopLevelClassesRecursive("org.apache.skywalking");
+
+ writeManifest(annotationScanDir.resolve("ScopeDeclaration.txt"),
+ scanAnnotation(allClasses, ScopeDeclaration.class));
+ writeManifest(annotationScanDir.resolve("Stream.txt"),
+ scanAnnotation(allClasses, org.apache.skywalking.oap.server.core.analysis.Stream.class));
+ writeManifest(annotationScanDir.resolve("Disable.txt"),
+ scanAnnotation(allClasses, Disable.class));
+ writeManifest(annotationScanDir.resolve("MultipleDisable.txt"),
+ scanAnnotation(allClasses, MultipleDisable.class));
+ writeManifest(annotationScanDir.resolve("SourceDispatcher.txt"),
+ scanInterface(allClasses, SourceDispatcher.class));
+ writeManifest(annotationScanDir.resolve("ISourceDecorator.txt"),
+ scanInterface(allClasses, ISourceDecorator.class));
+
+ log.info("OAL Class Exporter: done");
}
/**
@@ -195,6 +225,53 @@ public class OALClassExporter {
return result;
}
+ /**
+ * Scan for classes annotated with the given annotation type.
+ */
+ private static List<String> scanAnnotation(
+ ImmutableSet<ClassPath.ClassInfo> allClasses,
+ Class<? extends Annotation> annotationType) {
+
+ List<String> result = new ArrayList<>();
+ for (ClassPath.ClassInfo classInfo : allClasses) {
+ try {
+ Class<?> aClass = classInfo.load();
+ if (aClass.isAnnotationPresent(annotationType)) {
+ result.add(aClass.getName());
+ }
+ } catch (NoClassDefFoundError | Exception ignored) {
+ // Some classes may fail to load due to missing optional dependencies
+ }
+ }
+ Collections.sort(result);
+ log.info("Scanned @{}: {} classes", annotationType.getSimpleName(), result.size());
+ return result;
+ }
+
+ /**
+ * Scan for concrete classes implementing the given interface.
+ */
+ private static List<String> scanInterface(
+ ImmutableSet<ClassPath.ClassInfo> allClasses, Class<?> interfaceType) {
+
+ List<String> result = new ArrayList<>();
+ for (ClassPath.ClassInfo classInfo : allClasses) {
+ try {
+ Class<?> aClass = classInfo.load();
+ if (!aClass.isInterface()
+ && !Modifier.isAbstract(aClass.getModifiers())
+ && interfaceType.isAssignableFrom(aClass)) {
+ result.add(aClass.getName());
+ }
+ } catch (NoClassDefFoundError | Exception ignored) {
+ // Some classes may fail to load due to missing optional dependencies
+ }
+ }
+ Collections.sort(result);
+ log.info("Scanned {}: {} classes", interfaceType.getSimpleName(), result.size());
+ return result;
+ }
+
private static void writeManifest(Path path, List<String> lines) throws IOException {
Files.write(path, lines, StandardCharsets.UTF_8);
}
diff --git a/build-tools/oal-exporter/src/test/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporterTest.java b/build-tools/oal-exporter/src/test/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporterTest.java
index 27a1866..8384d51 100644
--- a/build-tools/oal-exporter/src/test/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporterTest.java
+++ b/build-tools/oal-exporter/src/test/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporterTest.java
@@ -126,5 +126,31 @@ class OALClassExporterTest {
// Verify disabled sources manifest exists (may be empty when all disable() calls are commented out)
Path disabledManifest = tempDir.resolve("META-INF/oal-disabled-sources.txt");
assertTrue(Files.exists(disabledManifest), "disabled sources manifest should exist");
+
+ // Verify annotation scan manifests
+ Path annotationScanDir = tempDir.resolve("META-INF/annotation-scan");
+ assertTrue(Files.isDirectory(annotationScanDir), "annotation-scan directory should exist");
+
+ assertManifestNotEmpty(annotationScanDir, "ScopeDeclaration.txt");
+ assertManifestNotEmpty(annotationScanDir, "Stream.txt");
+ assertManifestNotEmpty(annotationScanDir, "SourceDispatcher.txt");
+ assertManifestNotEmpty(annotationScanDir, "ISourceDecorator.txt");
+
+ // Disable/MultipleDisable manifests exist but may be empty (no classes use these annotations currently)
+ assertTrue(
+ Files.exists(annotationScanDir.resolve("Disable.txt")),
+ "Disable manifest should exist"
+ );
+ assertTrue(
+ Files.exists(annotationScanDir.resolve("MultipleDisable.txt")),
+ "MultipleDisable manifest should exist"
+ );
+ }
+
+ private void assertManifestNotEmpty(Path dir, String fileName) throws Exception {
+ Path manifest = dir.resolve(fileName);
+ assertTrue(Files.exists(manifest), fileName + " should exist");
+ List<String> lines = Files.readAllLines(manifest);
+ assertFalse(lines.isEmpty(), fileName + " should not be empty");
}
}
diff --git a/oap-graalvm-server/pom.xml b/oap-graalvm-server/pom.xml
index dfc7881..37dec83 100644
--- a/oap-graalvm-server/pom.xml
+++ b/oap-graalvm-server/pom.xml
@@ -241,10 +241,25 @@
<artifactId>server-starter</artifactId>
</dependency>
+ <!-- Pre-compiled OAL classes + annotation manifests from build-time export -->
+ <dependency>
+ <groupId>org.apache.skywalking</groupId>
+ <artifactId>oal-exporter</artifactId>
+ <version>${project.version}</version>
+ <classifier>generated</classifier>
+ </dependency>
+
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
+
+ <!-- Test -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
\ No newline at end of file
diff --git a/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/annotation/AnnotationScan.java b/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/annotation/AnnotationScan.java
new file mode 100644
index 0000000..9ec96fa
--- /dev/null
+++ b/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/annotation/AnnotationScan.java
@@ -0,0 +1,125 @@
+/*
+ * 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.annotation;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.annotation.Annotation;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.server.core.storage.StorageException;
+
+/**
+ * GraalVM replacement for upstream AnnotationScan.
+ * Same FQCN — shadows the upstream class via Maven classpath precedence.
+ *
+ * Instead of Guava ClassPath scanning, reads pre-built manifest files
+ * produced by OALClassExporter at build time.
+ * Manifest path: META-INF/annotation-scan/{AnnotationSimpleName}.txt
+ */
+@Slf4j
+public class AnnotationScan {
+
+ private static final String MANIFEST_PREFIX = "META-INF/annotation-scan/";
+
+ private final List<AnnotationListenerCache> listeners;
+
+ public AnnotationScan() {
+ this.listeners = new LinkedList<>();
+ }
+
+ public void registerListener(AnnotationListener listener) {
+ listeners.add(new AnnotationListenerCache(listener));
+ }
+
+ public void scan() throws IOException, StorageException {
+ for (AnnotationListenerCache listener : listeners) {
+ String annotationName = listener.annotation().getSimpleName();
+ String manifestPath = MANIFEST_PREFIX + annotationName + ".txt";
+
+ List<String> classNames = readManifest(manifestPath);
+ for (String className : classNames) {
+ try {
+ Class<?> aClass = Class.forName(className);
+ if (aClass.isAnnotationPresent(listener.annotation())) {
+ listener.addMatch(aClass);
+ }
+ } catch (ClassNotFoundException e) {
+ log.warn("Class not found from manifest {}: {}", manifestPath, className);
+ }
+ }
+ }
+
+ for (AnnotationListenerCache listener : listeners) {
+ listener.complete();
+ }
+ }
+
+ private static List<String> readManifest(String resourcePath) throws IOException {
+ List<String> lines = new ArrayList<>();
+ ClassLoader cl = AnnotationScan.class.getClassLoader();
+ try (InputStream is = cl.getResourceAsStream(resourcePath)) {
+ if (is == null) {
+ log.warn("Annotation manifest not found: {}", resourcePath);
+ return lines;
+ }
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (!line.isEmpty()) {
+ lines.add(line);
+ }
+ }
+ }
+ }
+ return lines;
+ }
+
+ private static class AnnotationListenerCache {
+ private final AnnotationListener listener;
+ private final List<Class<?>> matchedClass;
+
+ private AnnotationListenerCache(AnnotationListener listener) {
+ this.listener = listener;
+ this.matchedClass = new LinkedList<>();
+ }
+
+ private Class<? extends Annotation> annotation() {
+ return this.listener.annotation();
+ }
+
+ private void addMatch(Class<?> aClass) {
+ matchedClass.add(aClass);
+ }
+
+ private void complete() throws StorageException {
+ matchedClass.sort(Comparator.comparing(Class::getName));
+ for (Class<?> aClass : matchedClass) {
+ listener.notify(aClass);
+ }
+ }
+ }
+}
diff --git a/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/oal/rt/OALEngineLoaderService.java b/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/oal/rt/OALEngineLoaderService.java
new file mode 100644
index 0000000..fa40c7b
--- /dev/null
+++ b/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/oal/rt/OALEngineLoaderService.java
@@ -0,0 +1,129 @@
+/*
+ * 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.oal.rt;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.server.core.CoreModule;
+import org.apache.skywalking.oap.server.core.analysis.DisableRegister;
+import org.apache.skywalking.oap.server.core.analysis.StreamAnnotationListener;
+import org.apache.skywalking.oap.server.core.source.SourceReceiver;
+import org.apache.skywalking.oap.server.library.module.ModuleManager;
+import org.apache.skywalking.oap.server.library.module.ModuleStartException;
+import org.apache.skywalking.oap.server.library.module.Service;
+
+/**
+ * GraalVM replacement for upstream OALEngineLoaderService.
+ * Same FQCN — shadows the upstream class via Maven classpath precedence.
+ *
+ * Instead of running the OAL engine (Javassist code generation) at runtime,
+ * loads pre-compiled classes from build-time manifests produced by OALClassExporter.
+ */
+@Slf4j
+public class OALEngineLoaderService implements Service {
+
+ private static final String METRICS_MANIFEST = "META-INF/oal-metrics-classes.txt";
+ private static final String DISPATCHER_MANIFEST = "META-INF/oal-dispatcher-classes.txt";
+ private static final String DISABLED_MANIFEST = "META-INF/oal-disabled-sources.txt";
+
+ private final Set<OALDefine> oalDefineSet = new HashSet<>();
+ private final ModuleManager moduleManager;
+ private boolean loaded;
+
+ public OALEngineLoaderService(ModuleManager moduleManager) {
+ this.moduleManager = moduleManager;
+ }
+
+ public void load(OALDefine define) throws ModuleStartException {
+ if (oalDefineSet.contains(define)) {
+ return;
+ }
+
+ if (!loaded) {
+ loadAllPrecompiledClasses();
+ loaded = true;
+ }
+
+ oalDefineSet.add(define);
+ }
+
+ private void loadAllPrecompiledClasses() throws ModuleStartException {
+ try {
+ // 1. Register disabled sources before processing metrics
+ List<String> disabledSources = readManifest(DISABLED_MANIFEST);
+ for (String name : disabledSources) {
+ DisableRegister.INSTANCE.add(name);
+ }
+ log.info("Loaded {} disabled sources from manifest", disabledSources.size());
+
+ // 2. Load and register pre-compiled metrics classes
+ StreamAnnotationListener streamListener = new StreamAnnotationListener(moduleManager);
+ List<String> metricsClassNames = readManifest(METRICS_MANIFEST);
+ for (String className : metricsClassNames) {
+ Class<?> metricsClass = Class.forName(className);
+ streamListener.notify(metricsClass);
+ }
+ log.info("Registered {} pre-compiled OAL metrics classes", metricsClassNames.size());
+
+ // 3. Load and register pre-compiled dispatcher classes
+ var dispatcherListener = moduleManager.find(CoreModule.NAME)
+ .provider()
+ .getService(SourceReceiver.class)
+ .getDispatcherDetectorListener();
+ List<String> dispatcherClassNames = readManifest(DISPATCHER_MANIFEST);
+ for (String className : dispatcherClassNames) {
+ Class<?> dispatcherClass = Class.forName(className);
+ dispatcherListener.addIfAsSourceDispatcher(dispatcherClass);
+ }
+ log.info("Registered {} pre-compiled OAL dispatcher classes", dispatcherClassNames.size());
+
+ } catch (Exception e) {
+ throw new ModuleStartException("Failed to load pre-compiled OAL classes", e);
+ }
+ }
+
+ private static List<String> readManifest(String resourcePath) throws IOException {
+ List<String> lines = new ArrayList<>();
+ ClassLoader cl = OALEngineLoaderService.class.getClassLoader();
+ try (InputStream is = cl.getResourceAsStream(resourcePath)) {
+ if (is == null) {
+ log.warn("Manifest not found: {}", resourcePath);
+ return lines;
+ }
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (!line.isEmpty()) {
+ lines.add(line);
+ }
+ }
+ }
+ }
+ return lines;
+ }
+}
diff --git a/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/source/SourceReceiverImpl.java b/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/source/SourceReceiverImpl.java
new file mode 100644
index 0000000..08c7b02
--- /dev/null
+++ b/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/source/SourceReceiverImpl.java
@@ -0,0 +1,112 @@
+/*
+ * 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.source;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.server.core.analysis.DispatcherDetectorListener;
+import org.apache.skywalking.oap.server.core.analysis.DispatcherManager;
+import org.apache.skywalking.oap.server.core.analysis.SourceDecoratorManager;
+
+/**
+ * GraalVM replacement for upstream SourceReceiverImpl.
+ * Same FQCN — shadows the upstream class via Maven classpath precedence.
+ *
+ * Instead of Guava ClassPath scanning, reads pre-built manifest files
+ * produced by OALClassExporter at build time.
+ */
+@Slf4j
+public class SourceReceiverImpl implements SourceReceiver {
+
+ private static final String DISPATCHER_MANIFEST = "META-INF/annotation-scan/SourceDispatcher.txt";
+ private static final String DECORATOR_MANIFEST = "META-INF/annotation-scan/ISourceDecorator.txt";
+
+ @Getter
+ private final DispatcherManager dispatcherManager;
+
+ @Getter
+ private final SourceDecoratorManager sourceDecoratorManager;
+
+ public SourceReceiverImpl() {
+ this.dispatcherManager = new DispatcherManager();
+ this.sourceDecoratorManager = new SourceDecoratorManager();
+ }
+
+ @Override
+ public void receive(ISource source) {
+ dispatcherManager.forward(source);
+ }
+
+ @Override
+ public DispatcherDetectorListener getDispatcherDetectorListener() {
+ return getDispatcherManager();
+ }
+
+ public void scan() throws IOException, InstantiationException, IllegalAccessException {
+ List<String> dispatcherNames = readManifest(DISPATCHER_MANIFEST);
+ for (String className : dispatcherNames) {
+ try {
+ Class<?> aClass = Class.forName(className);
+ dispatcherManager.addIfAsSourceDispatcher(aClass);
+ } catch (ClassNotFoundException e) {
+ log.warn("Dispatcher class not found: {}", className);
+ }
+ }
+ log.info("Registered {} source dispatchers from manifest", dispatcherNames.size());
+
+ List<String> decoratorNames = readManifest(DECORATOR_MANIFEST);
+ for (String className : decoratorNames) {
+ try {
+ Class<?> aClass = Class.forName(className);
+ sourceDecoratorManager.addIfAsSourceDecorator(aClass);
+ } catch (ClassNotFoundException e) {
+ log.warn("Decorator class not found: {}", className);
+ }
+ }
+ log.info("Registered {} source decorators from manifest", decoratorNames.size());
+ }
+
+ private static List<String> readManifest(String resourcePath) throws IOException {
+ List<String> lines = new ArrayList<>();
+ ClassLoader cl = SourceReceiverImpl.class.getClassLoader();
+ try (InputStream is = cl.getResourceAsStream(resourcePath)) {
+ if (is == null) {
+ log.warn("Manifest not found: {}", resourcePath);
+ return lines;
+ }
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (!line.isEmpty()) {
+ lines.add(line);
+ }
+ }
+ }
+ }
+ return lines;
+ }
+}
diff --git a/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/PrecompiledRegistrationTest.java b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/PrecompiledRegistrationTest.java
new file mode 100644
index 0000000..eafa1c0
--- /dev/null
+++ b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/PrecompiledRegistrationTest.java
@@ -0,0 +1,392 @@
+/*
+ * 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.graalvm;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.reflect.ClassPath;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import org.apache.skywalking.oap.server.core.analysis.ISourceDecorator;
+import org.apache.skywalking.oap.server.core.analysis.SourceDispatcher;
+import org.apache.skywalking.oap.server.core.analysis.Stream;
+import org.apache.skywalking.oap.server.core.annotation.AnnotationScan;
+import org.apache.skywalking.oap.server.core.source.DefaultScopeDefine;
+import org.apache.skywalking.oap.server.core.source.ISource;
+import org.apache.skywalking.oap.server.core.source.ScopeDeclaration;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Verifies that pre-compiled manifests produced by OALClassExporter are complete
+ * and consistent. Checks:
+ * 1. Manifest contents match live Guava ClassPath scan (for hardcoded classes)
+ * 2. All OAL-generated classes are loadable and properly annotated
+ * 3. DefaultScopeDefine initializes correctly from manifests
+ * 4. Source -> Dispatcher -> Metrics chain is consistent
+ */
+class PrecompiledRegistrationTest {
+
+ private static final String OAL_RT_PACKAGE =
+ "org.apache.skywalking.oap.server.core.source.oal.rt.";
+
+ @BeforeAll
+ static void initScopeDefine() throws Exception {
+ DefaultScopeDefine.reset();
+ AnnotationScan scopeScan = new AnnotationScan();
+ scopeScan.registerListener(new DefaultScopeDefine.Listener());
+ scopeScan.scan();
+ }
+
+ @AfterAll
+ static void cleanUp() {
+ DefaultScopeDefine.reset();
+ }
+
+ // =========================================================================
+ // 1. Manifest vs Guava ClassPath comparison (hardcoded classes only)
+ // =========================================================================
+
+ @Test
+ void scopeDeclarationManifestMatchesClasspath() throws Exception {
+ Set<String> manifest = readManifestAsSet("META-INF/annotation-scan/ScopeDeclaration.txt");
+ Set<String> scanned = guavaScanAnnotation(ScopeDeclaration.class);
+ assertFalse(manifest.isEmpty(), "ScopeDeclaration manifest is empty");
+ assertEquals(scanned, manifest,
+ "ScopeDeclaration manifest does not match classpath scan");
+ }
+
+ @Test
+ void hardcodedStreamManifestMatchesClasspath() throws Exception {
+ Set<String> manifest = readManifestAsSet("META-INF/annotation-scan/Stream.txt");
+ Set<String> scanned = guavaScanAnnotation(Stream.class);
+ // Filter out OAL-generated classes from Guava scan
+ scanned.removeIf(name -> name.startsWith(OAL_RT_PACKAGE));
+ assertFalse(manifest.isEmpty(), "Stream manifest is empty");
+ assertEquals(scanned, manifest,
+ "Hardcoded @Stream manifest does not match classpath scan");
+ }
+
+ @Test
+ void sourceDispatcherManifestMatchesClasspath() throws Exception {
+ Set<String> manifest = readManifestAsSet("META-INF/annotation-scan/SourceDispatcher.txt");
+ Set<String> scanned = guavaScanInterface(SourceDispatcher.class);
+ // Filter out OAL-generated dispatchers from Guava scan
+ scanned.removeIf(name -> name.startsWith(OAL_RT_PACKAGE));
+ assertFalse(manifest.isEmpty(), "SourceDispatcher manifest is empty");
+ assertEquals(scanned, manifest,
+ "SourceDispatcher manifest does not match classpath scan");
+ }
+
+ @Test
+ void sourceDecoratorManifestMatchesClasspath() throws Exception {
+ Set<String> manifest = readManifestAsSet("META-INF/annotation-scan/ISourceDecorator.txt");
+ Set<String> scanned = guavaScanInterface(ISourceDecorator.class);
+ assertFalse(manifest.isEmpty(), "ISourceDecorator manifest is empty");
+ assertEquals(scanned, manifest,
+ "ISourceDecorator manifest does not match classpath scan");
+ }
+
+ // =========================================================================
+ // 2. OAL-generated classes: loadable and properly annotated
+ // =========================================================================
+
+ @Test
+ void allOalMetricsClassesLoadAndHaveStreamAnnotation() throws Exception {
+ List<String> classes = readManifest("META-INF/oal-metrics-classes.txt");
+ assertFalse(classes.isEmpty(), "OAL metrics manifest is empty");
+ for (String className : classes) {
+ Class<?> clazz = Class.forName(className);
+ assertTrue(clazz.isAnnotationPresent(Stream.class),
+ "OAL metrics class missing @Stream: " + className);
+ }
+ }
+
+ @Test
+ void allOalDispatcherClassesLoadAndImplementInterface() throws Exception {
+ List<String> classes = readManifest("META-INF/oal-dispatcher-classes.txt");
+ assertFalse(classes.isEmpty(), "OAL dispatcher manifest is empty");
+ for (String className : classes) {
+ Class<?> clazz = Class.forName(className);
+ assertTrue(SourceDispatcher.class.isAssignableFrom(clazz),
+ "OAL dispatcher doesn't implement SourceDispatcher: " + className);
+ }
+ }
+
+ // =========================================================================
+ // 3. Scope registration verification
+ // =========================================================================
+
+ @Test
+ void allDeclaredScopesRegisteredInDefaultScopeDefine() throws Exception {
+ List<String> classes = readManifest("META-INF/annotation-scan/ScopeDeclaration.txt");
+ for (String className : classes) {
+ Class<?> clazz = Class.forName(className);
+ ScopeDeclaration decl = clazz.getAnnotation(ScopeDeclaration.class);
+ assertNotNull(decl, "Missing @ScopeDeclaration on " + className);
+
+ String registeredName = DefaultScopeDefine.nameOf(decl.id());
+ assertEquals(decl.name(), registeredName,
+ "Scope name mismatch for id=" + decl.id() + " on " + className);
+ }
+ }
+
+ @Test
+ void wellKnownScopesAreRegistered() {
+ // Verify essential scopes exist after manifest-based initialization
+ assertNotNull(DefaultScopeDefine.nameOf(DefaultScopeDefine.SERVICE));
+ assertNotNull(DefaultScopeDefine.nameOf(DefaultScopeDefine.SERVICE_INSTANCE));
+ assertNotNull(DefaultScopeDefine.nameOf(DefaultScopeDefine.ENDPOINT));
+ }
+
+ // =========================================================================
+ // 4. Source -> Dispatcher -> Metrics chain
+ // =========================================================================
+
+ @Test
+ void allStreamAnnotationsReferenceValidScopes() throws Exception {
+ // Check hardcoded @Stream classes
+ List<String> hardcoded = readManifest("META-INF/annotation-scan/Stream.txt");
+ for (String className : hardcoded) {
+ Class<?> clazz = Class.forName(className);
+ Stream stream = clazz.getAnnotation(Stream.class);
+ assertNotNull(stream, "Missing @Stream on " + className);
+ assertScopeExists(stream.scopeId(), className);
+ }
+
+ // Check OAL-generated @Stream classes
+ List<String> oalMetrics = readManifest("META-INF/oal-metrics-classes.txt");
+ for (String className : oalMetrics) {
+ Class<?> clazz = Class.forName(className);
+ Stream stream = clazz.getAnnotation(Stream.class);
+ assertNotNull(stream, "Missing @Stream on OAL class " + className);
+ assertScopeExists(stream.scopeId(), className);
+ }
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void hardcodedDispatchersHaveDeclaredSourceScopes() throws Exception {
+ List<String> dispatchers = readManifest("META-INF/annotation-scan/SourceDispatcher.txt");
+ for (String className : dispatchers) {
+ Class<?> clazz = Class.forName(className);
+ Class<?> sourceType = extractSourceType(clazz);
+ if (sourceType == null) {
+ fail("Cannot extract source type from " + className);
+ }
+ ISource source = (ISource) sourceType.newInstance();
+ int scopeId = source.scope();
+ assertScopeExists(scopeId,
+ className + " (source=" + sourceType.getSimpleName() + ")");
+ }
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void oalDispatchersHaveDeclaredSourceScopes() throws Exception {
+ List<String> dispatchers = readManifest("META-INF/oal-dispatcher-classes.txt");
+ for (String className : dispatchers) {
+ Class<?> clazz = Class.forName(className);
+ Class<?> sourceType = extractSourceType(clazz);
+ if (sourceType == null) {
+ fail("Cannot extract source type from " + className);
+ }
+ ISource source = (ISource) sourceType.newInstance();
+ int scopeId = source.scope();
+ assertScopeExists(scopeId,
+ className + " (source=" + sourceType.getSimpleName() + ")");
+ }
+ }
+
+ @Test
+ void knownScopesHaveDispatchersAndStreamMetrics() throws Exception {
+ // Build scope -> dispatchers map from both hardcoded and OAL manifests
+ Map<Integer, List<String>> scopeDispatchers = buildScopeDispatcherMap();
+
+ // Build scope -> @Stream metrics map from both hardcoded and OAL manifests
+ Map<Integer, List<String>> scopeStreams = buildScopeStreamMap();
+
+ // Verify key scopes have both dispatchers and stream metrics
+ int[] keyScopeIds = {
+ DefaultScopeDefine.SERVICE,
+ DefaultScopeDefine.ENDPOINT,
+ DefaultScopeDefine.SERVICE_INSTANCE,
+ DefaultScopeDefine.SERVICE_RELATION,
+ };
+
+ for (int scopeId : keyScopeIds) {
+ String scopeName = DefaultScopeDefine.nameOf(scopeId);
+
+ assertTrue(
+ scopeDispatchers.containsKey(scopeId)
+ && !scopeDispatchers.get(scopeId).isEmpty(),
+ "Scope " + scopeName + " (id=" + scopeId + ") has no dispatchers"
+ );
+
+ assertTrue(
+ scopeStreams.containsKey(scopeId)
+ && !scopeStreams.get(scopeId).isEmpty(),
+ "Scope " + scopeName + " (id=" + scopeId + ") has no @Stream metrics"
+ );
+ }
+ }
+
+ // =========================================================================
+ // Helpers
+ // =========================================================================
+
+ private static void assertScopeExists(int scopeId, String context) {
+ try {
+ DefaultScopeDefine.nameOf(scopeId);
+ } catch (Exception e) {
+ fail("Invalid scope id=" + scopeId + " referenced by " + context);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private Map<Integer, List<String>> buildScopeDispatcherMap() throws Exception {
+ Map<Integer, List<String>> map = new HashMap<>();
+
+ List<String> allDispatchers = new ArrayList<>();
+ allDispatchers.addAll(readManifest("META-INF/annotation-scan/SourceDispatcher.txt"));
+ allDispatchers.addAll(readManifest("META-INF/oal-dispatcher-classes.txt"));
+
+ for (String className : allDispatchers) {
+ Class<?> clazz = Class.forName(className);
+ Class<?> sourceType = extractSourceType(clazz);
+ if (sourceType != null) {
+ ISource source = (ISource) sourceType.newInstance();
+ map.computeIfAbsent(source.scope(), k -> new ArrayList<>()).add(className);
+ }
+ }
+ return map;
+ }
+
+ private Map<Integer, List<String>> buildScopeStreamMap() throws Exception {
+ Map<Integer, List<String>> map = new HashMap<>();
+
+ List<String> allStreams = new ArrayList<>();
+ allStreams.addAll(readManifest("META-INF/annotation-scan/Stream.txt"));
+ allStreams.addAll(readManifest("META-INF/oal-metrics-classes.txt"));
+
+ for (String className : allStreams) {
+ Class<?> clazz = Class.forName(className);
+ Stream stream = clazz.getAnnotation(Stream.class);
+ if (stream != null) {
+ map.computeIfAbsent(stream.scopeId(), k -> new ArrayList<>()).add(className);
+ }
+ }
+ return map;
+ }
+
+ private static Class<?> extractSourceType(Class<?> dispatcherClass) {
+ for (Type genericInterface : dispatcherClass.getGenericInterfaces()) {
+ if (genericInterface instanceof ParameterizedType) {
+ ParameterizedType parameterized = (ParameterizedType) genericInterface;
+ if (parameterized.getRawType().getTypeName().equals(
+ SourceDispatcher.class.getName())) {
+ Type[] args = parameterized.getActualTypeArguments();
+ if (args.length == 1 && args[0] instanceof Class<?>) {
+ return (Class<?>) args[0];
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private static Set<String> guavaScanAnnotation(
+ Class<? extends java.lang.annotation.Annotation> annotationType) throws IOException {
+ ClassPath classpath = ClassPath.from(
+ PrecompiledRegistrationTest.class.getClassLoader());
+ ImmutableSet<ClassPath.ClassInfo> classes =
+ classpath.getTopLevelClassesRecursive("org.apache.skywalking");
+ Set<String> result = new TreeSet<>();
+ for (ClassPath.ClassInfo info : classes) {
+ try {
+ Class<?> clazz = info.load();
+ if (clazz.isAnnotationPresent(annotationType)) {
+ result.add(clazz.getName());
+ }
+ } catch (NoClassDefFoundError | Exception ignored) {
+ }
+ }
+ return result;
+ }
+
+ private static Set<String> guavaScanInterface(Class<?> interfaceType) throws IOException {
+ ClassPath classpath = ClassPath.from(
+ PrecompiledRegistrationTest.class.getClassLoader());
+ ImmutableSet<ClassPath.ClassInfo> classes =
+ classpath.getTopLevelClassesRecursive("org.apache.skywalking");
+ Set<String> result = new TreeSet<>();
+ for (ClassPath.ClassInfo info : classes) {
+ try {
+ Class<?> clazz = info.load();
+ if (!clazz.isInterface()
+ && !Modifier.isAbstract(clazz.getModifiers())
+ && interfaceType.isAssignableFrom(clazz)) {
+ result.add(clazz.getName());
+ }
+ } catch (NoClassDefFoundError | Exception ignored) {
+ }
+ }
+ return result;
+ }
+
+ private static Set<String> readManifestAsSet(String resourcePath) throws IOException {
+ return new TreeSet<>(readManifest(resourcePath));
+ }
+
+ private static List<String> readManifest(String resourcePath) throws IOException {
+ List<String> lines = new ArrayList<>();
+ ClassLoader cl = PrecompiledRegistrationTest.class.getClassLoader();
+ try (InputStream is = cl.getResourceAsStream(resourcePath)) {
+ assertNotNull(is, "Manifest not found: " + resourcePath);
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (!line.isEmpty()) {
+ lines.add(line);
+ }
+ }
+ }
+ }
+ return lines;
+ }
+}