Caldum (Latin: "hot") is a microframework for writing function hooks targeting the Java Virtual Machine (JVM). It provides additional scaffolding on top of the venerable Byte Buddy, and provides several features that enable faster and easier development of Byte Buddy-based Java agents.
Caldum's main feature is its hot-reloading capabilities that enable hooks to be updated at runtime. It additionally provides annotation-based dependency injection similar to JAX-RS, enabling simpler development workflows that do not require application restarts. Lastly, Caldum provides an interface to extend function hooks themselves with dynamic instrumentation.
Caldum (and VulcanLoader [see below]) support Java 6 and above (most recently tested against Java 21) and have been tested on both the HotSpot and OpenJ9 JVMs.
The following will build everything in the current environment:
./build.sh
This is more or less equvalent to:
cd caldum && ./gradlew; cd ..
cd vulcanloader && ./gradlew; cd ..
cd embeddedagentplugin && ./gradlew; cd ..
But if you want a saner/cleaner build, the following will build everything in a Docker container:
sudo ./build-docker.sh
The following will run all of the "static" tests:
sudo ./tests/test.sh
Howver, individual tests can be performed under various configurations:
sudo ./test.sh 'java89' -- 'premain' ---- eclipse-temurin:21-jdk ibm-semeru-runtimes:open-21-jdk
sudo ./test.sh 'java67' -- 'agentmain' ---- openjdk:6-jdk
There is a more "dynamic" test in the works in tests/hotreload
, but it's a bit more involved
to run at the moment and has not been dockerized yet:
cd tests/hotreload/testapp
JAVA_HOME=path/to/testingjdk ./test.sh 5
VulcanLoader (VL) is a thin-wrapper that builds Caldum as a Java agent and provides a minimal interface to set up, reconfigure, and unload Caldum-compatible Java agents. Agent state is maintained based on the JAR filename, but not the entire file path; attempting to load a new agent JAR with same filename as an existing agent JAR will result in the original being unloaded prior to the new one being loaded.
VulcanLoader is intended to be rebuilt for each target host when targeting
JRE versions below 9. When doing so, the respective platform's tools.jar
(from its JDK, not JRE) must be copied to the vulcanloader/tools/
directory
prior to building.
cd vulcanloader && ./gradlew
-javaagent:path/to/vl.jar=path/to/config
-javaagent:path/to/vl.jar=path/to/agent.jar
java -jar path/to/vl.jar <pid> -c path/to/config
java -jar path/to/vl.jar <pid> <agent.jar...> -- <agent opts>
path/to/agent1.jar: <agent1 opts>
path/to/agent2.jar: <agent2 opts>
...
- Unload one agent:
java -jar path/to/vl.jar <pid> -u path/to/agent.jar
java -jar path/to/vl.jar <pid> path/to/agent.jar -- unload
- Unload all agents:
java -jar path/to/vl.jar <pid> -u
Caldum and VulcanLoader are designed to directly expose a single bundled version of Byte Buddy directly to compatible Java agents, such that they do not need to bundle the entire Byte buddy library and may build to relatively small JAR files. The ClassLoader used to load agent JARs will first check itself when attempting to load classes. This enables developers to embed generally whatever libraries they may need. However, care should be taken to ensure that dependencies intended for use within hook code are injected so that they are accessible from the ClassLoader of the instrumented class.
While it is not strictly necessary to include the normal JAR manifest
attributes used for Java agents (all loaded JARs inherit Caldum/VL's agent
capabilities), Caldum/VL will respect any Premain-Class
and Agent-Class
attributes and invoke the associated entry points prior to scanning for
and applying annotated function hooks. This enables developers to provide
additional static configuration to annotated classes and support legacy
instrumentation code. Caldum/VL will additionally invoke any
public static void unload()
method within the entry class when the
agent is unloaded (e.g. manually or when reloading).
Caldum and VulcanLoader currently attempt to maintain support for the same versions of Java supported by Byte Buddy (currently Java 6 through 9+). Due to this, the codebases of Caldum and VulcanLoader themselves are built as Java 6 (using OpenJDK 11); however, Caldum-compatible agents may target newer versions of Java supported by the JVM being instrumented.
This annotation identifies a Caldum hook class to be automatically applied on load.
Options:
- The
wrappers
value may be used to supply a list of hook wrapping classes that will be applied in order to instrument the hook code. - The
redefinition
value may be used to configure anAgentBuilder.RedefinitionStrategy
(e.g.DISABLED
(new loads only),REDEFINITION
, orRETRANSFORMATION
).
Hook classes are structured as follows:
@Hook
class ExampleHook {
static class SomeSettings {
... // type/method matchers, local dependency injection providers
}
... // static fields (copied or dependency injected)
@Advice.OnMethodEnter
static void enter() {
...
}
@Advice.OnMethodExit
static void exit() {
...
}
}
This annotation identifies that a Caldum hook class is to be preprocessed using
the DynVarsAgent
/DynamicFields
classes. They rewrite classes to proxy their
static and non-static fields (and associated field accesses) into fixed Map objects
that it adds to the classes. This makes it possible to make a wider range of hot-reload
modifications to already loaded hooks beyond what is traditionally supported by
mainstream JVMs (e.g. not adding/removing/reordering fields/methods, and not editing
method signatures). Generally speaking, this annotation should be used primarily
during the development of Caldum-based agents/hooks as its modifications can make
analyzing one's own hooks at runtime a bit annoying.
This annotation is used to configure Byte Buddy to write events and exceptions
of an @Hook
-annotated class to the standard output of the process being
hooked. This is implemented using a
new AgentBuilder.Listener.StreamWriting(System.out)
.
This annotation identifies a global dependency injection provider.
Dependency injection in Caldum is designed to support local, in-file
configuration, with opt-in support for global defaults without violating the
principle of least surprise. By default, locally configured @DI.Provide
-d
values will be applied to any static fields within the @Hook
class that have
a matching name without validating type compatibility.
This annotation may be applied to a static field or static method within a
global @DI.Provider
class or a "settings" class nested within an @Hook
class.
If applied to a field without an overriding name
value, the name of the field
will be used to match against injection targets.
If applied to a static method returning a Map<String,Object>
, the method will
be invoked to provide a dependency injection mapping of field names to values.
This annotation may be applied to a static field within a @Hook
class to
opt-in to dependency injection from a matching @DI.Provider
class-supplied
@DI.Provide
-d field. However, locally-declared @DI.Provide
-d fields will
take precedence.
This annotation may be applied to a static field within a @Hook
class to
inject the ClassLoader
of the @Hook
class (i.e. the ClassLoader
of the
Caldum-compatible agent JAR).
Each of these annotations may be applied to a static Byte Buddy
ElementMatcher
field or a static method returning an ElementMatcher
.
The ElementMatcher
will be used to configure net.bytebuddy.agent.builder.AgentBuilder::ignore
.
net.bytebuddy.agent.builder.AgentBuilder::ignore
.
The ElementMatcher
will be used to configure the type matcher of
net.bytebuddy.agent.builder.AgentBuilder::type
.
The ElementMatcher
will be used to configure the ClassLoader
matcher of
net.bytebuddy.agent.builder.AgentBuilder::type
.
The ElementMatcher
will be used to configure the module matcher of
net.bytebuddy.agent.builder.AgentBuilder::type
.
The ElementMatcher
will be used to configure
net.bytebuddy.agent.builder.AgentBuilder::type(AgentBuilder.RawMatcher matcher)
.
The ElementMatcher
will be used to configure
net.bytebuddy.asm.Advice::on(ElementMatcher<? super MethodDescription> matcher)
.
Caldum supports the instrumentation of @Hook
classes through the use of its
wrappers
value. Classes specified in this manner must contain static nested
classes annotated with at least one of the following annotations.
This class will be applied as Byte Buddy Advice
to the
@Advice.OnMethodEnter
-annotated method of the @Hook
class. It should
specify at least one @Advice.OnMethodEnter
-/@Advice.OnMethodExit
-annotated
static method. Care should be taken when specifying @Advice
dependency
injected method arguments; these will be applied to the @Hook
-annotated class
itself and not the classes it instruments.
This class will be applied as Byte Buddy Advice
to the
@Advice.OnMethodExit
-annotated method of the @Hook
class. It should
specify at least one @Advice.OnMethodEnter
-/@Advice.OnMethodExit
-annotated
static method. Care should be taken when specifying @Advice
dependency
injected method arguments; these will be applied to the @Hook
-annotated class
itself and not the classes it instruments.
NoRecursion
: This wrapper ensures that a@Hook
-annotated class configured with it will not invoke the injected hook code when invoked from the call-stack of aNoRecursion
-wrapped@Hook
-annotated class. This is useful for performing invocations within hook code that may result in infinite recursion otherwise.
Caldum (and VulcanLoader) are licensed under the Apache License, Version 2.0. Exceptions to this (i.e. for certain vendored source files) are explicitly noted in the relevant source files.