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/04/26 02:10:37 UTC
(skywalking) branch feature/runtime-rule-hot-update updated: runtime-rule: BytecodeClassDefiner contract — bypass Javassist's deprecated reflection-based class definition for the per-file RuleClassLoader path
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch feature/runtime-rule-hot-update
in repository https://gitbox.apache.org/repos/asf/skywalking.git
The following commit(s) were added to refs/heads/feature/runtime-rule-hot-update by this push:
new 0d95d6c82f runtime-rule: BytecodeClassDefiner contract — bypass Javassist's deprecated reflection-based class definition for the per-file RuleClassLoader path
0d95d6c82f is described below
commit 0d95d6c82f4291099ebe25b5c3bebf3f5c7b9081
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Sun Apr 26 10:10:31 2026 +0800
runtime-rule: BytecodeClassDefiner contract — bypass Javassist's deprecated reflection-based class definition for the per-file RuleClassLoader path
JDK 17+ blocks Javassist's CtClass.toClass(loader, ProtectionDomain), which
reflects into java.lang.ClassLoader.defineClass via setAccessible — the
strong-encapsulation rule fires with InaccessibleObjectException. The static
MAL/LAL boot path is unaffected because it uses the neighbor-class form
(toClass(MalExpressionPackageHolder.class) / equivalent), which Javassist
routes through MethodHandles.privateLookupIn — no reflection. The runtime-
rule per-file RuleClassLoader has no such neighbor at the first toClass
call, so it falls into the deprecated 2-arg path and fails.
Adds a small core contract — BytecodeClassDefiner — that loaders can implement
to publish their defineClass(String, byte[]) as a public method. RuleClassLoader
now implements it (its private defineRuleClass becomes a public override of
the contract method), and three generators check the loader's interface
before falling back:
- MeterSystem.createInternal (Metrics subclass for runtime-rule metrics)
- MALClassGenerator.defineClass (MalExpression + closure companion classes)
- LALClassGenerator.defineClass (LalExpression + sub-method classes)
The interface lives in server-core under .classloader/ rather than under
.analysis/meter/ so MAL and LAL can both reference it without dragging the
meter package into log-analyzer's import graph. Production callers that go
through the static neighbor-class path are unchanged; legacy 2-arg toClass
remains as a back-compat fallback for any non-BytecodeClassDefiner loader,
but no shipped loader uses it today.
Verified locally — runtime-rule /addOrUpdate against a fresh OAP boot
returns 200 structural_applied (was 500 apply_failed before this commit).
---
.../analyzer/v2/compiler/LALClassGenerator.java | 19 +++-
.../analyzer/v2/compiler/MALClassGenerator.java | 25 ++++-
.../server/core/analysis/meter/MeterSystem.java | 119 +++++++++++++--------
.../core/classloader/BytecodeClassDefiner.java | 58 ++++++++++
.../runtimerule/classloader/RuleClassLoader.java | 19 ++--
5 files changed, 184 insertions(+), 56 deletions(-)
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGenerator.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGenerator.java
index 3a8ffa3ee7..5cfb17ffe3 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGenerator.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGenerator.java
@@ -20,6 +20,7 @@ package org.apache.skywalking.oap.log.analyzer.v2.compiler;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -34,6 +35,7 @@ import javassist.CtNewConstructor;
import javassist.CtNewMethod;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.oap.log.analyzer.v2.compiler.rt.LalExpressionPackageHolder;
+import org.apache.skywalking.oap.server.core.classloader.BytecodeClassDefiner;
import org.apache.skywalking.oap.log.analyzer.v2.dsl.LalExpression;
import org.apache.skywalking.oap.server.core.WorkPath;
import org.apache.skywalking.oap.server.library.util.StringUtil;
@@ -540,12 +542,27 @@ public final class LALClassGenerator {
/**
* Loads a generated class through the configured {@link #targetClassLoader} when set
* (runtime-rule hot-update path: class lands in the per-file {@code RuleClassLoader}),
- * or falls back to the neighbor-class form when {@code targetClassLoader} is {@code null}
+ * or via the neighbor-class form when {@code targetClassLoader} is {@code null}
* (startup path: class lands in the OAP app loader alongside
* {@link LalExpressionPackageHolder}).
+ *
+ * <p>{@link BytecodeClassDefiner} loaders (the runtime-rule {@code RuleClassLoader})
+ * receive the {@code CtClass.toBytecode()} bytes via their public {@code defineClass}
+ * — bypasses Javassist's deprecated {@code toClass(loader, ProtectionDomain)} reflection
+ * path so we don't need {@code --add-opens java.base/java.lang} on the OAP container.
+ * Same shape as {@code MALClassGenerator}; both DSLs share the contract.
*/
private Class<?> defineClass(final CtClass ctClass) throws javassist.CannotCompileException {
if (targetClassLoader != null) {
+ if (targetClassLoader instanceof BytecodeClassDefiner) {
+ try {
+ return ((BytecodeClassDefiner) targetClassLoader)
+ .defineClass(ctClass.getName(), ctClass.toBytecode());
+ } catch (final IOException e) {
+ throw new javassist.CannotCompileException(
+ "failed to serialise " + ctClass.getName() + " bytes", e);
+ }
+ }
return ctClass.toClass(targetClassLoader, null);
}
return ctClass.toClass(LalExpressionPackageHolder.class);
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 d5fa037eb9..839733b1e3 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
@@ -18,6 +18,7 @@
package org.apache.skywalking.oap.meter.analyzer.v2.compiler;
import java.io.File;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javassist.ClassPool;
@@ -26,6 +27,7 @@ import javassist.CtNewConstructor;
import javassist.CtNewMethod;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt.MalExpressionPackageHolder;
+import org.apache.skywalking.oap.server.core.classloader.BytecodeClassDefiner;
import org.apache.skywalking.oap.meter.analyzer.v2.dsl.ExpressionMetadata;
import org.apache.skywalking.oap.meter.analyzer.v2.dsl.MalExpression;
import org.apache.skywalking.oap.meter.analyzer.v2.dsl.MalFilter;
@@ -191,13 +193,30 @@ public final class MALClassGenerator {
/**
* Loads a generated class through the configured {@link #targetClassLoader} when set
* (runtime-rule hot-update path: class lands in the per-file {@code RuleClassLoader}),
- * or falls back to the neighbor-class form when {@code targetClassLoader} is {@code null}
+ * or via the neighbor-class form when {@code targetClassLoader} is {@code null}
* (startup path: class lands in the OAP app loader alongside
- * {@link MalExpressionPackageHolder}). Both forms use the same underlying
- * {@code ClassLoader.defineClass} — only the target loader differs.
+ * {@link MalExpressionPackageHolder}).
+ *
+ * <p>When {@code targetClassLoader} implements
+ * {@link org.apache.skywalking.oap.server.core.classloader.BytecodeClassDefiner
+ * BytecodeClassDefiner} (the runtime-rule {@code RuleClassLoader} does), we hand
+ * the loader the {@code CtClass.toBytecode()} bytes and let it invoke its public
+ * {@code defineClass} directly — no Javassist {@code toClass(loader,
+ * ProtectionDomain)} reflection, no {@code --add-opens java.base/java.lang}
+ * requirement on JDK 17+. Otherwise we fall back to the legacy 2-arg toClass for
+ * back-compat, but no shipped loader uses that path today.
*/
private Class<?> defineClass(final CtClass ctClass) throws javassist.CannotCompileException {
if (targetClassLoader != null) {
+ if (targetClassLoader instanceof BytecodeClassDefiner) {
+ try {
+ return ((BytecodeClassDefiner) targetClassLoader)
+ .defineClass(ctClass.getName(), ctClass.toBytecode());
+ } catch (final IOException e) {
+ throw new javassist.CannotCompileException(
+ "failed to serialise " + ctClass.getName() + " bytes", e);
+ }
+ }
return ctClass.toClass(targetClassLoader, null);
}
return ctClass.toClass(MalExpressionPackageHolder.class);
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/MeterSystem.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/MeterSystem.java
index f8dd9722df..4fe03742be 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/MeterSystem.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/MeterSystem.java
@@ -39,6 +39,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.JavaVersion;
import org.apache.commons.lang3.SystemUtils;
import org.apache.skywalking.oap.server.core.UnexpectedException;
+import org.apache.skywalking.oap.server.core.classloader.BytecodeClassDefiner;
import org.apache.skywalking.oap.server.core.analysis.StreamDefinition;
import org.apache.skywalking.oap.server.core.analysis.TimeBucket;
import org.apache.skywalking.oap.server.core.analysis.meter.dynamic.MeterClassPackageHolder;
@@ -291,11 +292,26 @@ public class MeterSystem implements Service {
final Class targetClass;
try {
// Explicit targetClassLoader — the generated class goes directly into the
- // per-file RuleClassLoader, not the neighbor class's loader. Uses the legacy
- // 2-arg toClass signature which remains supported on Java 9+ (deprecated but
- // functional); the alternative Lookup-based path would require a neighbor class
- // loaded by targetClassLoader, which defeats the point.
- targetClass = metricsClass.toClass(targetClassLoader, null);
+ // per-file RuleClassLoader, not the neighbor class's loader. Two paths:
+ //
+ // - {@link BytecodeClassDefiner} loaders (the runtime-rule {@code
+ // RuleClassLoader} is the only known implementor today): hand the loader
+ // the raw bytecode via its public {@code defineClass(String, byte[])}.
+ // This sidesteps Javassist's deprecated 2-arg {@code toClass(loader,
+ // ProtectionDomain)} which reflects into {@code java.lang.ClassLoader.
+ // defineClass} and requires {@code --add-opens java.base/java.lang} on
+ // JDK 17+ — a JVM-flag tax we don't want to put on every operator.
+ //
+ // - Other loaders (legacy callers): keep the 2-arg toClass for back-compat.
+ // No new constraints on existing static rule paths; they don't use this
+ // overload anyway, so this branch is effectively dead today and exists as
+ // a safety net.
+ if (targetClassLoader instanceof BytecodeClassDefiner) {
+ targetClass = ((BytecodeClassDefiner) targetClassLoader)
+ .defineClass(METER_CLASS_PACKAGE + className, metricsClass.toBytecode());
+ } else {
+ targetClass = metricsClass.toClass(targetClassLoader, null);
+ }
AcceptableValue prototype = (AcceptableValue) targetClass.newInstance();
meterPrototypes.put(metricsName, new MeterDefinition(type, prototype, dataType, true));
MetricsStreamProcessor.getInstance().create(
@@ -314,9 +330,12 @@ public class MeterSystem implements Service {
if (opt.hasShapeMismatch()) {
meterPrototypes.remove(metricsName);
}
- } catch (CannotCompileException | IllegalAccessException | InstantiationException | StorageException e) {
+ } catch (CannotCompileException | IllegalAccessException | InstantiationException
+ | StorageException | IOException e) {
// Also roll back on exception paths — an unsuccessful create must not leave a
- // prototype stranded in the registry.
+ // prototype stranded in the registry. {@link IOException} surfaces from
+ // {@code metricsClass.toBytecode()} when serialising the generated bytes; treat
+ // it the same as a Javassist compile failure so the apply rolls back cleanly.
meterPrototypes.remove(metricsName);
throw new UnexpectedException(e.getMessage(), e);
}
@@ -404,41 +423,41 @@ public class MeterSystem implements Service {
/**
* Opt-aware {@code removeMetric}. Runtime-rule peer-side callers pass
* {@link StorageManipulationOpt#localCacheOnly()} so {@code ModelInstaller.dropTable} is
- * NOT invoked on the shared storage — the cluster main owns that side-effect. The local
- * state (meterPrototypes, L1/L2 workers, StorageModels registry entry, CtClass detach)
- * is torn down identically regardless of policy; only the backend-facing listener
- * chain respects it.
+ * NOT invoked on the shared storage — the cluster main owns that side-effect.
+ *
+ * <p>Order is backend-first / local-state-second so failure is retriable. The earlier
+ * version evicted {@code meterPrototypes} and called the cascade in parallel; if the
+ * cascade threw (BanyanDB {@code dropTable} failure), the local state was already torn
+ * down — there was nothing for the next {@code /inactivate} or reconciler tick to drive
+ * a backend retry against, and the operator could not recover the orphaned measure
+ * without an OAP restart. Now the cascade runs first; on success we drop the local
+ * caches; on failure we leave {@code meterPrototypes} populated and the CtClass attached
+ * so a retry hits the backend again.
*
* <p>Failure surface: under {@code fullInstall} the storage-model cascade failure is
- * propagated as a {@link RuntimeException}, because that is the caller's signal that the
- * backend drop (BanyanDB delete-measure, ES mapping close, ...) did not fully complete
- * and an operator-facing "inactivated" response would be incorrect. Under
- * {@code localCacheOnly} the cascade fires listener {@code whenRemoving} but the peer's
- * {@code ModelInstaller.dropTable} is suppressed anyway, so any listener-level throw is
- * logged and swallowed — the peer has no backend debt to surface. Streaming-chain drain
- * failures are always logged and swallowed: stale workers self-drain within one tick,
- * and carrying them into the response would be noise, not signal.
+ * propagated as a {@link RuntimeException}. The REST {@code /inactivate} path depends on
+ * this to surface 500 {@code teardown_deferred} when BanyanDB's delete-measure threw —
+ * without it the handler would return 200 inactivated despite the measure still being
+ * live. Under {@code localCacheOnly} the cascade fires {@code whenRemoving} but the
+ * peer's {@code ModelInstaller.dropTable} is suppressed by policy, so any throw is
+ * logged and swallowed — the peer has no backend debt. Streaming-chain drain failures
+ * are always logged and swallowed: stale workers self-drain within one tick.
*/
public synchronized boolean removeMetric(final String metricsName, final StorageManipulationOpt opt) {
- final MeterDefinition def = meterPrototypes.remove(metricsName);
+ final MeterDefinition def = meterPrototypes.get(metricsName);
if (def == null) {
return false;
}
final Class<?> prototypeClass = def.getMeterPrototype().getClass();
- // Drain + deregister worker chain and release the shared queue handlers.
- try {
- MetricsStreamProcessor.getInstance().removeMetric(manager, (Class) prototypeClass);
- } catch (final Throwable t) {
- log.error("Failed to remove streaming chain for metric {}; storage model cascade "
- + "will still run.", metricsName, t);
- }
-
- // Cascade storage-model removal (Hour / Day / Minute). ModelRegistry.remove fires
- // whenRemoving on every listener, so each backend's ModelInstaller.dropTable runs
- // in FULL_INSTALL mode — real delete for BanyanDB, no-op for JDBC / Elasticsearch,
- // skipped outright when the caller is a peer-side (LOCAL_CACHE_ONLY) apply.
- Throwable cascadeFailure = null;
+ // Cascade storage-model removal (Hour / Day / Minute) FIRST. ModelRegistry.remove
+ // fires whenRemoving on every listener, so each backend's ModelInstaller.dropTable
+ // runs — real delete for BanyanDB, no-op for JDBC / Elasticsearch, skipped outright
+ // when the caller is a peer-side (LOCAL_CACHE_ONLY) apply. If a listener throws,
+ // ModelRegistry.remove keeps the model in its registry so this retry path stays
+ // open: the caller (Reconciler unregisterBundle) preserves appliedMal[key] and the
+ // next tick (or operator retry) re-enters this method, finds meterPrototypes still
+ // populated, and re-fires the cascade.
try {
final ModelRegistry modelCreator = manager.find(CoreModule.NAME)
.provider()
@@ -446,7 +465,27 @@ public class MeterSystem implements Service {
modelCreator.remove(prototypeClass, opt);
} catch (final Throwable t) {
log.error("Failed to cascade storage-model removal for metric {}", metricsName, t);
- cascadeFailure = t;
+ if (opt.isFullInstall()) {
+ throw new RuntimeException(
+ "Storage-model cascade failed for metric " + metricsName
+ + "; backend drop did not complete. Local state preserved for retry.",
+ t);
+ }
+ // localCacheOnly: peer-side caller — backend drop is suppressed by policy
+ // anyway, so a listener throw here is the listener's local bookkeeping
+ // misbehaving, not real backend debt. Fall through to clear local state.
+ }
+
+ // Backend cascade succeeded (or local-cache-only, where it doesn't matter). Drop
+ // the prototype and drain the workers. Worker drain failure is non-fatal and
+ // logged: stale workers self-drain within one tick, and the prototype + Model are
+ // already gone so future samples can't reach them.
+ meterPrototypes.remove(metricsName);
+ try {
+ MetricsStreamProcessor.getInstance().removeMetric(manager, (Class) prototypeClass);
+ } catch (final Throwable t) {
+ log.error("Failed to remove streaming chain for metric {}; prototype + storage "
+ + "model already gone.", metricsName, t);
}
// Detach the CtClass from the default pool so a future shape-breaking re-create passes
@@ -466,18 +505,6 @@ public class MeterSystem implements Service {
metricsName);
}
}
- // On fullInstall, propagate a cascade failure after the local teardown has fully run
- // (meterPrototypes evicted, CtClass detached). The REST /inactivate path depends on
- // this to surface 500 teardown_deferred when BanyanDB's delete-measure threw — without
- // it the handler would return 200 inactivated despite the backend measure still being
- // live, misleading operators into thinking the rule is fully torn down.
- if (cascadeFailure != null && opt.isFullInstall()) {
- throw new RuntimeException(
- "Storage-model cascade failed for metric " + metricsName
- + "; backend drop may not have completed. Local state has been torn down; "
- + "retry the operator request to re-attempt backend teardown.",
- cascadeFailure);
- }
return true;
}
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/classloader/BytecodeClassDefiner.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/classloader/BytecodeClassDefiner.java
new file mode 100644
index 0000000000..618918f94f
--- /dev/null
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/classloader/BytecodeClassDefiner.java
@@ -0,0 +1,58 @@
+/*
+ * 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.classloader;
+
+/**
+ * Marker contract for class loaders that expose a public {@code defineClass} so generated
+ * bytecode can be injected without going through Javassist's deprecated
+ * {@code CtClass.toClass(ClassLoader, ProtectionDomain)} reflection path.
+ *
+ * <p>Background: on JDK 9+ the deprecated {@code toClass(ClassLoader, ProtectionDomain)}
+ * reflectively calls {@code java.lang.ClassLoader.defineClass} via {@code setAccessible},
+ * which the strong-encapsulation rule blocks at runtime with
+ * {@code InaccessibleObjectException} unless the operator explicitly opens
+ * {@code java.base/java.lang} via {@code --add-opens}. Static MAL/LAL boot is unaffected
+ * because {@code MeterClassPackageHolder}'s package access works through Javassist's
+ * neighbor-class API on the default loader. Runtime-rule's per-file loader has no such
+ * neighbor at the first {@code toClass} call; the only pre-loaded classes are inherited
+ * via parent delegation and so live in the parent's loader, not the rule loader.
+ *
+ * <p>This contract sidesteps the issue entirely: a class loader that implements it
+ * publishes a {@code defineClass} method as part of its API, and the runtime-rule
+ * generator path calls it directly with {@code CtClass.toBytecode()} bytes — no
+ * reflection, no deprecated overload, no {@code --add-opens} requirement on the OAP
+ * JVM. Production loaders (the static path) keep working through their existing
+ * {@code toClass(Class<?>)} neighbor-based path.
+ *
+ * <p>The interface is intentionally minimal — a single bytecode-defining method that
+ * mirrors what {@link ClassLoader#defineClass(String, byte[], int, int)} does. Lifecycle
+ * (parent delegation, URL search) stays the implementor's concern.
+ */
+public interface BytecodeClassDefiner {
+
+ /**
+ * Define {@code bytecode} as a {@link Class} in this loader's namespace.
+ *
+ * @param className fully-qualified binary name, must match the class's
+ * {@code this_class} attribute in the bytecode.
+ * @param bytecode a complete classfile (e.g., from {@code CtClass.toBytecode()}).
+ * @return the resolved {@link Class} object loaded by this defining loader.
+ */
+ Class<?> defineClass(String className, byte[] bytecode);
+}
diff --git a/oap-server/server-receiver-plugin/skywalking-runtime-rule-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/runtimerule/classloader/RuleClassLoader.java b/oap-server/server-receiver-plugin/skywalking-runtime-rule-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/runtimerule/classloader/RuleClassLoader.java
index 36b17873eb..3fda057cc5 100644
--- a/oap-server/server-receiver-plugin/skywalking-runtime-rule-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/runtimerule/classloader/RuleClassLoader.java
+++ b/oap-server/server-receiver-plugin/skywalking-runtime-rule-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/runtimerule/classloader/RuleClassLoader.java
@@ -21,6 +21,7 @@ package org.apache.skywalking.oap.server.receiver.runtimerule.classloader;
import java.net.URL;
import java.net.URLClassLoader;
import lombok.Getter;
+import org.apache.skywalking.oap.server.core.classloader.BytecodeClassDefiner;
/**
* Per-file classloader that isolates all classes generated for one runtime rule file.
@@ -43,7 +44,7 @@ import lombok.Getter;
* (parented to {@code ClassPool.getDefault()} with {@code LoaderClassPath(this)} appended)
* to inject generated bytecode back into this loader via {@code defineClass}.
*/
-public final class RuleClassLoader extends URLClassLoader {
+public final class RuleClassLoader extends URLClassLoader implements BytecodeClassDefiner {
/** Source rule identity — informational, used in log lines and graveyard metrics. */
@Getter
private final String catalog;
@@ -62,12 +63,18 @@ public final class RuleClassLoader extends URLClassLoader {
}
/**
- * Package-private escape hatch used by the generator-injection layer so Javassist-produced
- * bytecode can be defined directly in this loader without going through {@code toClass}'s
- * classpath resolution. Javassist's {@code toClass(Class<?>)} (Java 9+) path is preferred
- * at compile time; this method is here for reflective fallbacks and tests.
+ * Bytecode-injection entry point used by {@link
+ * org.apache.skywalking.oap.server.core.analysis.meter.MeterSystem MeterSystem}'s
+ * runtime-rule path and by Javassist's MAL / LAL generators after they call {@code
+ * CtClass.toBytecode()}. Goes straight through {@code URLClassLoader.defineClass} —
+ * no Javassist {@code toClass(loader, ProtectionDomain)} reflection, no JDK 17+
+ * {@code --add-opens java.base/java.lang} requirement on the OAP container.
+ *
+ * <p>Implements {@link BytecodeClassDefiner} so callers in {@code server-core} can
+ * type-check against the contract without taking a compile-time dep on this plugin.
*/
- Class<?> defineRuleClass(final String className, final byte[] bytecode) {
+ @Override
+ public Class<?> defineClass(final String className, final byte[] bytecode) {
return defineClass(className, bytecode, 0, bytecode.length);
}