Skip to content

Commit

Permalink
Support spring-boot-devtoools reloadable classlaoder
Browse files Browse the repository at this point in the history
  • Loading branch information
amarziali committed Jul 12, 2024
1 parent 63f16d7 commit 7e51450
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@
0 org.springframework.boot.context.embedded.EmbeddedWebApplicationContext
0 org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer$*
0 org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedWebappClassLoader
0 org.springframework.boot.devtools.restart.classloader.RestartClassLoader
0 org.springframework.boot.web.embedded.netty.NettyWebServer$*
0 org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader
0 org.springframework.boot.web.embedded.tomcat.TomcatWebServer$1
Expand Down
23 changes: 21 additions & 2 deletions dd-java-agent/instrumentation/spring-boot/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,23 @@ muzzle {
module = 'spring-boot'
versions = "[1.3.0.RELEASE,3)"
}
pass {
group = 'org.springframework.boot'
module = 'spring-boot-devtools'
versions = "[1.3.0.RELEASE,3)"
}
pass {
group = 'org.springframework.boot'
module = 'spring-boot'
versions = "[3,)"
javaVersion = "17"
}
pass {
group = 'org.springframework.boot'
module = 'spring-boot-devtools'
versions = "[3,)"
javaVersion = "17"
}
}

apply from: "$rootDir/gradle/java.gradle"
Expand All @@ -39,16 +50,24 @@ dependencies {
implementation project(':dd-java-agent:instrumentation:span-origin')

compileOnly group: 'org.springframework.boot', name: 'spring-boot', version: '1.3.0.RELEASE'
compileOnly group: 'org.springframework.boot', name: 'spring-boot-devtools', version: '1.3.0.RELEASE'
testImplementation group: 'org.springframework.boot', name: 'spring-boot', version: '1.3.0.RELEASE'
testImplementation group: 'org.springframework.boot', name: 'spring-boot-devtools', version: '1.3.0.RELEASE'
testImplementation project(':dd-java-agent:instrumentation:trace-annotation')
boot1LatestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot', version: '1.+'
boot1LatestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-devtools', version: '1.+'
boot1LatestDepForkedTestImplementation group: 'org.springframework.boot', name: 'spring-boot', version: '1.+'
boot2TestImplementation group: 'org.springframework.boot', name: 'spring-boot', version: '2.0.0.RELEASE'
boot2TestImplementation group: 'org.springframework.boot', name: 'spring-boot-devtools', version: '2.0.0.RELEASE'
boot2ForkedTestImplementation group: 'org.springframework.boot', name: 'spring-boot', version: '2.0.0.RELEASE'
boot2LatestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot', version: '1.+'
boot2LatestDepForkedTestImplementation group: 'org.springframework.boot', name: 'spring-boot', version: '1.+'
boot2LatestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot', version: '2.+'
boot2LatestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-devtools', version: '2.+'
boot2LatestDepForkedTestImplementation group: 'org.springframework.boot', name: 'spring-boot', version: '2.+'
boot3TestImplementation group: 'org.springframework.boot', name: 'spring-boot', version: '3.0.0'
boot3TestImplementation group: 'org.springframework.boot', name: 'spring-boot-devtools', version: '3.0.0'
boot3ForkedTestImplementation group: 'org.springframework.boot', name: 'spring-boot', version: '3.0.0'
latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot', version: '+'
latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-devtools', version: '+'
latestDepForkedTestImplementation group: 'org.springframework.boot', name: 'spring-boot', version: '+'
latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-logging', version: '+'
latestDepForkedTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-logging', version: '+'
Expand Down
73 changes: 47 additions & 26 deletions dd-java-agent/instrumentation/spring-boot/gradle.lockfile

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package datadog.trace.instrumentation.springboot;

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter.ExcludeType.RUNNABLE;
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;

import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.ExcludeFilterProvider;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.agent.tooling.InstrumenterModule;
import datadog.trace.agent.tooling.bytebuddy.memoize.Memoizer;
import datadog.trace.api.InstrumenterConfig;
import datadog.trace.bootstrap.InstrumentationContext;
import datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import org.springframework.boot.devtools.restart.classloader.RestartClassLoader;

@AutoService(InstrumenterModule.class)
public class RestartClassLoaderInstrumentation extends InstrumenterModule.Tracing
implements Instrumenter.ForSingleType, ExcludeFilterProvider {

public RestartClassLoaderInstrumentation() {
super("spring-boot-devtools", "spring-boot");
}

@Override
public boolean isEnabled() {
return super.isEnabled() && InstrumenterConfig.get().isResolverMemoizingEnabled();
}

@Override
public Map<String, String> contextStore() {
return Collections.singletonMap(
"org.springframework.boot.devtools.restart.classloader.RestartClassLoader",
"java.lang.Boolean");
}

@Override
public String instrumentedType() {
return "org.springframework.boot.devtools.restart.classloader.RestartClassLoader";
}

@Override
public void methodAdvice(MethodTransformer transformer) {
transformer.applyAdvice(isConstructor(), getClass().getName() + "$RecordNewClassloaderAdvice");
transformer.applyAdvice(
isMethod().and(named("findClass")), getClass().getName() + "$ResetMemoizerAdvice");
}

@Override
public Map<ExcludeFilter.ExcludeType, ? extends Collection<String>> excludedClasses() {
return Collections.singletonMap(
RUNNABLE,
Collections.singleton(
"org.springframework.boot.devtools.restart.Restarter$LeakSafeThread"));
}

public static class RecordNewClassloaderAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void after(@Advice.This RestartClassLoader self) {
InstrumentationContext.get(RestartClassLoader.class, Boolean.class).putIfAbsent(self, true);
}
}

public static class ResetMemoizerAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void before(@Advice.This RestartClassLoader self) {
if (Boolean.TRUE.equals(
InstrumentationContext.get(RestartClassLoader.class, Boolean.class).remove(self))) {
Memoizer.resetState();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.agent.test.utils.TraceUtils
import org.apache.commons.io.IOUtils
import org.springframework.asm.ClassReader
import org.springframework.asm.ClassVisitor
import org.springframework.asm.ClassWriter
import org.springframework.asm.MethodVisitor
import org.springframework.asm.Opcodes
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles
import org.springframework.boot.devtools.restart.classloader.RestartClassLoader

class RestartClassLoaderTest extends AgentTestRunner {
def 'should instrument reloaded classes'() {
given:
def repository = new ClassLoaderFiles()
def parent = new URLClassLoader(new URL[]{
GroovyObject.class.getProtectionDomain().getCodeSource().getLocation()
}, (ClassLoader)null)
def cl = new RestartClassLoader(parent,
new URL[] {
TestBean.class.getProtectionDomain().getCodeSource().getLocation()
},
repository)
when:
TestBean.test()
then:
assertTraces(0, {})
when:
def content = IOUtils.toByteArray(new URL(TracingBean.class.getProtectionDomain().getCodeSource().getLocation().toString() + "TracingBean.class"))
// We need to inject a different bytecode (the one of TracingBean) by emulating it was in TestBean like if TestBean has been recompiled on the fly and swapped.
// One option is to kind of cheat is to quickly manipulate the TracingBean bytecode to have a type name change
ClassReader reader = new ClassReader(content)
ClassWriter writer = new ClassWriter(0)
ClassVisitor transformer = new ClassVisitor(Opcodes.ASM5, writer) {
@Override
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, "TestBean", signature, superName, interfaces)
}

@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return super.visitMethod(access, name, desc, signature, exceptions)
}
}
reader.accept( transformer, 0)
repository.addFile("TestBean.class", new ClassLoaderFile(ClassLoaderFile.Kind.MODIFIED, writer.toByteArray()))
cl.loadClass(TestBean.class.getName()).getMethod("test").invoke(null)
then:
assertTraces(1, {
trace(1) {
TraceUtils.basicSpan(it, "trace.annotation","TestBean.test",null, null, ["component":"trace"] )
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public class TestBean {

public static void test() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import datadog.trace.api.Trace;

public class TracingBean {
@Trace
public static void test() {}
}

0 comments on commit 7e51450

Please sign in to comment.