Skip to content

Commit 198994f

Browse files
dougqhdevflow.devflow-routing-intake
andauthored
Build DatadogClassLoader resource URL from agent jar URL, not OS path (#11480)
Build DatadogClassLoader resource URL from agent jar URL, not OS path `agentResourcePrefix` was constructed via `"jar:file:" + JarFile.getName() + "!/"`. `JarFile.getName()` returns the OS-native path. On Windows that is `C:\Datadog\dd-java-agent.jar`, producing the malformed URL `jar:file:C:\Datadog\dd-java-agent.jar!/...` — the backslashes are not valid in a URL path and the embedded `file:C:\...` is missing the leading slash before the drive letter. `new URL(...)` is permissive enough to accept the string, but `URLConnection.openStream()` then fails, so `findResource()` returns URLs that cannot be read. This breaks helper-class injection on Spring Boot 1.3.5 fat jars (and WARs) on Windows: byte-buddy's `ClassFileLocator.ForClassLoader` surfaces the failure as `IllegalStateException: Could not locate class file for <helper>` and `HelperInjector.getHelperMap` propagates it. Without the Servlet3 and Tomcat helpers no entry spans are created, so no traces reach the Agent. Use the well-formed `agentJarURL` (already built via `File.toURI().toURL()` upstream) instead. The migrated test exercises the regression cross-platform by placing the test jar in a directory whose name contains a space — `URL.toString()` percent-encodes it, `JarFile.getName()` does not — which is the same class of malformation Windows triggers. Migrate `DatadogClassLoaderTest.groovy` to Java JUnit 5 per project convention (uses reflection for the protected `getClassLoadingLock`). Fixes #6398 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent 9735506 commit 198994f

3 files changed

Lines changed: 176 additions & 108 deletions

File tree

‎dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/DatadogClassLoader.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ public DatadogClassLoader(final URL agentJarURL, final ClassLoader parent) throw
4141

4242
agentJarFile = new JarFile(new File(agentJarURL.toURI()), false);
4343
agentCodeSource = new CodeSource(agentJarURL, (Certificate[]) null);
44-
agentResourcePrefix = "jar:file:" + agentJarFile.getName() + "!/";
44+
// Build the resource prefix from the agent jar URL rather than JarFile.getName().
45+
// JarFile.getName() returns the OS-native path, which on Windows looks like
46+
// "C:\Datadog\dd-java-agent.jar" — producing a malformed URL ("jar:file:C:\..."), so
47+
// findResource() returns URLs that openStream() cannot read. See APMS-19624 / #6398.
48+
agentResourcePrefix = "jar:" + agentJarURL + "!/";
4549
agentJarIndex = AgentJarIndex.readIndex(agentJarFile);
4650
}
4751

‎dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/DatadogClassLoaderTest.groovy‎

Lines changed: 0 additions & 107 deletions
This file was deleted.
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package datadog.trace.bootstrap;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.io.File;
8+
import java.lang.reflect.Method;
9+
import java.net.URL;
10+
import java.nio.file.Files;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import java.util.concurrent.ExecutorService;
14+
import java.util.concurrent.Executors;
15+
import java.util.concurrent.Future;
16+
import java.util.concurrent.Phaser;
17+
import org.junit.jupiter.api.Test;
18+
import org.junit.jupiter.api.Timeout;
19+
20+
class DatadogClassLoaderTest {
21+
private static final URL testJarLocation =
22+
toUrl(new File("src/test/resources/classloader-test-jar/testjar-jdk8"));
23+
private static final URL nestedTestJarLocation =
24+
toUrl(new File("src/test/resources/classloader-test-jar/jar-with-nested-classes-jdk8"));
25+
26+
private static URL toUrl(File file) {
27+
try {
28+
return file.toURI().toURL();
29+
} catch (Exception e) {
30+
throw new RuntimeException(e);
31+
}
32+
}
33+
34+
// ClassLoader.getClassLoadingLock is protected; reach it via reflection, matching what the
35+
// original Groovy test did implicitly.
36+
private static Object classLoadingLock(ClassLoader cl, String name) throws Exception {
37+
Method m = ClassLoader.class.getDeclaredMethod("getClassLoadingLock", String.class);
38+
m.setAccessible(true);
39+
return m.invoke(cl, name);
40+
}
41+
42+
@Test
43+
@Timeout(60)
44+
void agentClassloaderDoesNotLockClassloadingAroundInstance() throws Exception {
45+
// setup
46+
String className1 = "some/class/Name1";
47+
String className2 = "some/class/Name2";
48+
DatadogClassLoader ddLoader = new DatadogClassLoader();
49+
Object lock1 = classLoadingLock(ddLoader, className1);
50+
Object lock2 = classLoadingLock(ddLoader, className2);
51+
Phaser threadHoldLockPhase = new Phaser(2);
52+
Phaser acquireLockFromMainThreadPhase = new Phaser(2);
53+
54+
// when
55+
Thread thread1 =
56+
new Thread() {
57+
@Override
58+
public void run() {
59+
synchronized (lock1) {
60+
threadHoldLockPhase.arrive();
61+
acquireLockFromMainThreadPhase.arriveAndAwaitAdvance();
62+
}
63+
}
64+
};
65+
thread1.start();
66+
67+
Thread thread2 =
68+
new Thread() {
69+
@Override
70+
public void run() {
71+
threadHoldLockPhase.arriveAndAwaitAdvance();
72+
synchronized (lock2) {
73+
acquireLockFromMainThreadPhase.arrive();
74+
}
75+
}
76+
};
77+
thread2.start();
78+
thread1.join();
79+
thread2.join();
80+
81+
// then — reaching this point means no deadlock occurred
82+
}
83+
84+
@Test
85+
void agentClassloaderSuccessfullyLoadsClassesConcurrently() throws Exception {
86+
// given
87+
DatadogClassLoader ddLoader = new DatadogClassLoader(testJarLocation, null);
88+
89+
// when
90+
ExecutorService executorService = Executors.newCachedThreadPool();
91+
List<Future<Void>> futures = new ArrayList<>();
92+
for (int i = 0; i < 100; i++) {
93+
futures.add(
94+
executorService.submit(
95+
() -> {
96+
ddLoader.loadClass("a.A");
97+
return null;
98+
}));
99+
}
100+
for (Future<Void> future : futures) {
101+
try {
102+
future.get();
103+
} catch (Exception ex) {
104+
if (ex.getCause() instanceof Throwable) {
105+
throw (Exception) ex.getCause();
106+
}
107+
throw ex;
108+
}
109+
}
110+
111+
// then — no exception thrown
112+
}
113+
114+
@Test
115+
void loadNestedClassesAndCallGetEnclosingClass() throws Exception {
116+
// given
117+
DatadogClassLoader ddLoader = new DatadogClassLoader(nestedTestJarLocation, null);
118+
119+
// when
120+
Class<?> klass = ddLoader.loadClass("p.EnclosingClass$StaticInnerClass");
121+
122+
// then
123+
assertEquals("StaticInnerClass", klass.getSimpleName());
124+
125+
// when
126+
Class<?> enclosing = klass.getEnclosingClass();
127+
128+
// then
129+
assertEquals("EnclosingClass", enclosing.getSimpleName());
130+
}
131+
132+
/**
133+
* Regression test for APMS-19624 / DataDog/dd-trace-java#6398. The resource URL must be built
134+
* from the agent jar URL (a properly-formed {@code file:} URL), not from {@code
135+
* JarFile.getName()} (an OS-native path). On Windows the OS-native form is {@code
136+
* C:\Datadog\dd-java-agent.jar}, which produces the malformed URL {@code
137+
* jar:file:C:\Datadog\dd-java-agent.jar!/...} and breaks helper-class injection on Spring Boot
138+
* 1.3.5 {@code LaunchedURLClassLoader}.
139+
*
140+
* <p>We exercise the divergence cross-platform by copying the test jar into a directory whose
141+
* name contains a space: {@code URL.toString()} percent-encodes it, {@code JarFile.getName()}
142+
* does not.
143+
*/
144+
@Test
145+
void findResourceUsesAgentJarUrlAsPrefix(@org.junit.jupiter.api.io.TempDir File tempDir)
146+
throws Exception {
147+
File spacedDir = new File(tempDir, "dir with spaces");
148+
assertTrue(spacedDir.mkdirs());
149+
File spacedJar = new File(spacedDir, "testjar-jdk8");
150+
Files.copy(
151+
new File("src/test/resources/classloader-test-jar/testjar-jdk8").toPath(),
152+
spacedJar.toPath());
153+
154+
URL spacedJarUrl = spacedJar.toURI().toURL();
155+
DatadogClassLoader ddLoader = new DatadogClassLoader(spacedJarUrl, null);
156+
157+
URL resource = ddLoader.findResource("a/A.class");
158+
assertNotNull(resource, "findResource should locate a/A.class in the test jar");
159+
160+
String expectedPrefix = "jar:" + spacedJarUrl + "!/";
161+
assertTrue(
162+
resource.toString().startsWith(expectedPrefix),
163+
() ->
164+
"resource URL ("
165+
+ resource
166+
+ ") should start with the agent jar URL prefix ("
167+
+ expectedPrefix
168+
+ ") — pre-fix code derives the prefix from JarFile.getName(),"
169+
+ " which leaves the space unencoded and on Windows produces a malformed URL.");
170+
}
171+
}

0 commit comments

Comments
 (0)