Skip to content

Commit b9b4531

Browse files
committed
fix(agent): Append runtime JAR to bootstrap class loader
The AppMap runtime JAR must be appended to the bootstrap class loader search path. This ensures that core AppMap runtime classes, such as `HookFunctions`, are available to all application classes, regardless of the specific class loader (e.g., in web servers like Tomcat). This change fixes `NoClassDefFoundError` issues for `HookFunctions`. This commit also adds a new `gretty-tomcat` test to verify the fix in a servlet environment.
1 parent db1496f commit b9b4531

File tree

8 files changed

+93
-6
lines changed

8 files changed

+93
-6
lines changed

agent/src/main/java/com/appland/appmap/Agent.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,13 +213,14 @@ private static void setupRuntime(Path agentJarPath, JarFile agentJar, Instrument
213213
System.exit(1);
214214
}
215215

216-
// Adding the runtime jar to the boot class loader means the classes it
217-
// contains will be available everywhere. This avoids issues caused by any
218-
// filtering the app's class loader might be doing (e.g. the Scala runtime
219-
// when running a Play app).
216+
// It's critical to append the runtime JAR to the bootstrap class loader
217+
// search path, not the system class loader search path. This ensures that
218+
// AppMap's core runtime classes, such as HookFunctions, are available to
219+
// all application classes, including those loaded by different class loaders
220+
// (e.g., in web servers like Tomcat or other complex environments), which
221+
// fixes `NoClassDefFoundError` for `HookFunctions`.
220222
JarFile runtimeJar = new JarFile(runtimeJarPath.toFile());
221-
inst.appendToSystemClassLoaderSearch(runtimeJar);
222-
// inst.appendToBootstrapClassLoaderSearch(runtimeJar);
223+
inst.appendToBootstrapClassLoaderSearch(runtimeJar);
223224

224225
// HookFunctions can only be referenced after the runtime jar has been
225226
// appended to the boot class loader.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name: gretty-tomcat
2+
packages:
3+
- path: org.example
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
plugins {
2+
id 'war'
3+
id 'org.gretty' version '4.1.6'
4+
}
5+
6+
repositories {
7+
mavenCentral()
8+
}
9+
10+
def appmapJar = System.env.AGENT_JAR
11+
12+
gretty {
13+
servletContainer = 'tomcat10'
14+
contextPath = '/'
15+
jvmArgs = [
16+
"-Dappmap.config.file=appmap.yml",
17+
"-Dappmap.debug.file=../../build/logs/gretty-tomcat-appmap.log"
18+
]
19+
if (appmapJar) {
20+
jvmArgs << "-javaagent:${appmapJar}"
21+
}
22+
}
23+
24+
dependencies {
25+
providedCompile 'jakarta.servlet:jakarta.servlet-api:5.0.0'
26+
}
27+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env bats
2+
3+
load '../helper'
4+
5+
setup_file() {
6+
cd "${BATS_TEST_DIRNAME}" || exit 1
7+
mkdir -p ../build/log
8+
9+
export LOG="../build/log/gretty-tomcat.log"
10+
export SERVER_PORT=8080
11+
export WS_URL="http://localhost:${SERVER_PORT}/hello"
12+
13+
_configure_logging
14+
15+
echo -n "Starting gretty-tomcat test server..." >&3
16+
gradlew appStart -Pgretty.httpPort=${SERVER_PORT} &> $LOG &
17+
export WS_PID=$!
18+
19+
wait_for_ws
20+
}
21+
22+
teardown_file() {
23+
gradlew appStop || true
24+
# stop_ws might fail if /exit is not there, but it also waits for process to die.
25+
# We can try to just kill the gradle process if it's still running.
26+
kill "$WS_PID" || true
27+
}
28+
29+
@test "hello world" {
30+
run _curl -sXGET "${WS_URL}"
31+
assert_success
32+
assert_output "Hello, World!"
33+
}
34+
35+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'gretty-tomcat'
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.example;
2+
3+
import java.io.IOException;
4+
import jakarta.servlet.ServletException;
5+
import jakarta.servlet.annotation.WebServlet;
6+
import jakarta.servlet.http.HttpServlet;
7+
import jakarta.servlet.http.HttpServletRequest;
8+
import jakarta.servlet.http.HttpServletResponse;
9+
10+
@WebServlet(name = "HelloServlet", urlPatterns = {"/hello"})
11+
public class HelloServlet extends HttpServlet {
12+
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
13+
response.getWriter().print("Hello, World!");
14+
}
15+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.example;
2+
3+
public class MyClass implements Comparable<MyClass> {
4+
public int compareTo(MyClass other) { return 0; }
5+
}

agent/test/gretty-tomcat/src/main/webapp/.keep

Whitespace-only changes.

0 commit comments

Comments
 (0)