This project is a minimal ja-netfilter plugin that bootstraps Lenni0451/ClassTransform during ja-netfilter plugin initialization.
Use the Gradle wrapper from the project root:
.\gradlew.bat clean shadowJarThe deployable artifact is generated at:
build/libs/ja-netfilter-classtransform-plugin-0.1.0-all.jar
The project resolves ja-netfilter 2025.3.0 from JitPack and ClassTransform from Maven Central. It also includes ClassTransform's additionalclassprovider and mixinstranslator submodules at runtime for richer class provider options and Mixin-annotation-based transformer translation. Eclipse ECJ is carried only in a nested isolated runtime jar for runtime .java transformer source compilation. Any local *.jar files placed directly under libs/ are added generically to the implementation classpath.
ClassTransform's own annotation API, net/lenni0451/classtransform/annotations/**, is intentionally kept in the shadow jar so runtime transformer discovery and externally compiled transformers can use the same ClassTransform annotation types. net.lenni0451.classtransform:mixinsdummy remains compile-only because Mixin annotation stubs are only needed to compile Mixin-style transformer declarations.
Annotation-only dependencies are excluded from the shadow jar when they only provide type/nullability metadata: com.google.errorprone:error_prone_annotations, com.google.j2objc:j2objc-annotations, org.jetbrains:annotations, and org.jspecify:jspecify. Google annotation packages that live inside bundled Guava classes, such as com/google/common/annotations/**, are also excluded. These annotations are kept out of the plugin jar because they are not required at runtime.
The deployable shadow jar embeds ECJ at META-INF/janet/ecj/ecj-runtime.jar. The main jar root intentionally excludes org/eclipse/jdt/** and settingdust/janet/classtransform/compile/isolated/** so ja-netfilter bootstrap loading does not expose ECJ as a root plugin dependency.
Author project-local Java transformer sources under:
src/transformers/java
The dedicated transformers source set is a development authoring convenience. It compiles against the plugin's main classes and dependency classpath, including ClassTransform APIs and compile-only annotations already used by the plugin. ./gradlew.bat build also copies these Java sources to:
build/libs/transformers
For example, src/transformers/java/example/MyTransformer.java is emitted as build/libs/transformers/example/MyTransformer.java. This directory is source-distribution and authoring output only, contains .java files instead of compiled .class files, and is not included in the plugin shadow jar. For deployment, copy the resulting Java source tree, or an equivalent transformer source tree, to the target ja-netfilter config directory's transformers/ child as described below. Precompiled .class transformer trees remain supported as a fallback when placed under the same runtime directory.
build.gradle.kts writes the ja-netfilter manifest attribute into both jar and shadowJar outputs:
JANF-Plugin-Entry: settingdust.janet.classtransform.ClassTransformPlugin
ja-netfilter loads this entry class and calls PluginEntry.init(Environment, PluginConfig). The entry gets Instrumentation from Environment, scans the conventional config-folder transformer source directory for supported metadata, creates the deferred runtime transformer, and hooks JVM instrumentation. Matching source transformers are compiled and registered later when their target class is being transformed with its actual ClassLoader.
PluginEntry.getTransformers() intentionally returns an empty list. This plugin lets the deferred ClassTransform runtime own bytecode dispatch instead of mixing ClassTransform with ja-netfilter MyTransformer dispatch.
Copy the *-all.jar shadow jar to the ja-netfilter plugin directory used by the target application:
plugins/for a shared ja-netfilter plugin directory.plugins-<app>/for an application-specific plugin directory.
Restart the target JVM after copying the jar so ja-netfilter can discover the manifest and initialize the plugin.
Normal use requires no transformer directory config. On plugin initialization, the runtime resolves this conventional deployed transformer directory:
<config-dir>/transformers
<config-dir> is the parent directory of the ja-netfilter plugin config file represented by PluginConfig.getFile(). The directory is not resolved from the JVM working directory and does not point at Gradle build/libs unless you explicitly copy files there. The default compiled output directory for deferred runtime compilation is:
<config-dir>/transformers/.compiled
The expected Java-source authoring/deployment workflow is:
- Write transformer Java sources under
src/transformers/java. - Run
./gradlew.bat buildfrom the project root. - Copy the Java source tree from
build/libs/transformers, or another equivalent transformer source tree, to<config-dir>/transformersin the target ja-netfilter deployment. - Deploy the plugin shadow jar and restart the target JVM.
At startup, Java sources under <config-dir>/transformers are scanned recursively for supported string target metadata. They are not compiled immediately. The plugin installs one global ClassTransform-backed runtime transformer, then compiles and registers only the matching source files when the JVM is transforming the named target class with its actual ClassLoader.
Runtime source changes are snapshot-based. The startup scan creates the initial metadata snapshot, and the runtime has an explicit re-scan path for replacing that snapshot. The MVP does not include a file watcher or hot reload loop, so deployed source changes normally require restart or an explicit re-scan integration before later target hits see the new metadata.
Supported metadata is intentionally string-based for the deferred runtime MVP, for example:
@CTransformer(name = "example.Target")and supported Mixin-style string metadata such as:
@Mixin(targets = "example.Target")Class-literal target metadata is diagnostic-only for deferred runtime compilation because target classes may not be visible before their target ClassLoader is known. Sources that only use unsupported metadata produce startup diagnostics and are not registered for deferred compilation.
Deferred runtime compilation uses Eclipse ECJ through the nested isolated runtime jar. On first matching target hit, the runtime extracts META-INF/janet/ecj/ecj-runtime.jar to a content-addressed cache beside the configured compile output directory:
<compile-output-parent>/.janet-ecj-runtime-cache/<plugin-version>/<sha256>/ecj-runtime.jar
A dedicated child-first URLClassLoader loads ECJ and settingdust.janet.classtransform.compile.isolated.IsolatedEcjCompilerFacade from that jar. The main runtime invokes the facade reflectively with JDK-only map/list/byte-array payloads, keeping org.eclipse.jdt types out of the bootstrap-loaded plugin path. The isolated loader keeps DefaultProblemFactory and org/eclipse/jdt/internal/compiler/problem/messages.properties co-visible so ECJ diagnostics do not depend on the target thread context class loader.
Class lookup still uses the target class loader followed by the plugin/compiler loader. The target JVM does not need the java.compiler module, javax.tools.ToolProvider, or a target JDK compiler. Dependencies referenced by transformer sources may be visible only from the current target loader; those target-loader-only dependencies are resolved when that target class is hit.
Compiled source output is written under <config-dir>/transformers/.compiled by default. If transformerCompileOutputDir is configured, it is resolved relative to <config-dir> and used instead. When transformerJavaSources is configured, its comma-, semicolon-, or line-separated .java paths are resolved relative to <config-dir> and scanned instead of recursively scanning the default source root. The compile output directory must not contain any configured transformer Java source, which prevents generated .class output from being reused as a source input boundary.
The runtime keeps separate practical keys for dispatch, compile caching, and registration:
- Dispatch is by normalized target class name plus target loader identity against the current metadata snapshot.
- Compile caching uses the source fingerprint, target loader identity, compiler visibility domain, and metadata fingerprint.
- Registration uses the source fingerprint, target class, target loader identity, and generated transformer binary namespace.
Compiled runtime transformer classes are given deterministic generated binary names that include the loader/cache namespace. This lets one global TransformerManager register same-source or same-original-name transformers from different loader domains without replacing each other, while repeated or concurrent hits for the same source/target/loader register idempotently. Multi-target transformers are discovered from supported metadata and are compiled lazily when one of their indexed targets is hit.
Precompiled top-level .class transformers found under the compiled output directory or <config-dir>/transformers are also registered in the global ClassTransform manager; inner-class files containing $ are skipped because their owning top-level transformer is registered instead. Package wildcards such as pkg.* or pkg.** are not used. If <config-dir>/transformers is absent and no explicit transformerJavaSources are configured, startup continues with no discovered source transformers.
Invalid Java sources or unresolved dependencies produce ECJ diagnostics in the deferred compile failure. Metadata problems are logged as ja-netfilter DebugInfo warnings during startup. By default, a failed deferred compile skips only that transformer and continues with other matching transformers. Strict/debug fail-fast behavior is not exposed as an MVP user switch yet. Failed transform application is logged as an error for the target class. Routine compile flow does not emit ECJ ResourceBundle probe diagnostics or near-target transform probes as normal behavior.
Unit tests create temporary config directories and assert that runtime paths are derived from the temporary config file parent. They also cover string metadata scanning, startup scan without eager compilation, explicit re-scan semantics, target-loader-aware deferred compilation, generated binary-name isolation, idempotent registration, target-loader-only dependency execution, skip-and-continue compile failure, diagnostics, source-hash caching, compile output guards, multi-target behavior, precompiled class fallback, isolated ECJ loader/resource visibility, and no transform probe noise. Archive guards run through verifyShadowJarEcjIsolation to require the nested ECJ runtime jar, reject root org/eclipse/jdt/** and isolated implementation entries, and reject main/root constant-pool ECJ references. For manual validation, build the project, copy the Java source tree from build/libs/transformers into the target ja-netfilter config directory's transformers/ child, start the target JVM, and verify your transformer callbacks run when the named target class is loaded or retransformed.
If validating protected or proprietary transformer files, check only path/status and observable behavior. Do not read, diff, quote, or hash protected transformer source content as part of documentation or test evidence.
All discovered or configured transformer code is trusted executable code loaded into the target JVM without sandboxing, and its ClassTransform callbacks can affect application bytecode.
- Transformer order matters: ja-netfilter and ClassTransform both install
ClassFileTransformerinstances, so validate actual ordering in the target application. - Already-loaded classes may not be transformed unless the target class supports retransformation and your integration explicitly triggers it.
- Retransform behavior can differ by JVM, target class, and agent setup; test with the real application startup path.
- Dependency conflicts are possible in instrumented JVMs. This plugin keeps ClassTransform packages unrelocated for external transformer compatibility and excludes ASM from the shadow jar because the target ja-netfilter runtime is expected to provide ASM. ECJ is not packaged at the main shadow jar root; it lives in the nested isolated runtime jar so ECJ classes and resources are loaded together by the dedicated compiler loader.
- Google, JetBrains, and JSpecify annotation-only packages are not shadowed. ClassTransform's own annotations are packaged and must remain available in the plugin jar.