diff --git a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java index ca599429e..77318722c 100644 --- a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java @@ -22,12 +22,17 @@ import java.net.URLConnection; import java.util.Enumeration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Used to preload classes to support automatic priming for SnapStart */ public final class ClassPreLoader { public static final String CLASSES_FILE = "classesloaded.txt"; + private static final Logger LOG = LoggerFactory.getLogger(ClassPreLoader.class); + private ClassPreLoader() { // Hide default constructor } @@ -57,6 +62,7 @@ public static void preloadClasses() { * @param is */ private static void preloadClassesFromStream(InputStream is) { + int loaded = 0; try (is; InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(isr)) { @@ -66,26 +72,30 @@ private static void preloadClassesFromStream(InputStream is) { if (idx != -1) { line = line.substring(0, idx); } - final String className = line.stripTrailing(); - if (!className.isBlank()) { - loadClassIfFound(className); + final String className = line.strip(); + if (!className.isBlank() && loadClassIfFound(className)) { + loaded++; } } } catch (Exception ignored) { // No action is required if preloading fails for any reason } + LOG.debug("SnapStart priming: preloaded {} class(es) from {}", loaded, CLASSES_FILE); } /** * Initializes the class with given name if found, ignores otherwise * - * @param className + * @param className the binary name of the class to load + * @return true if the class was found and loaded, false otherwise */ - private static void loadClassIfFound(String className) { + private static boolean loadClassIfFound(String className) { try { Class.forName(className, true, ClassPreLoader.class.getClassLoader()); + return true; } catch (ClassNotFoundException e) { // No action is required if the class with given name cannot be found + return false; } } } \ No newline at end of file diff --git a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java index 03991688e..fb1359434 100644 --- a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java +++ b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java @@ -8,6 +8,7 @@ class ClassPreLoaderTest { // Making this volatile so the Thread Context doesn't need any special handling static volatile boolean dummyClassLoaded = false; + static volatile boolean leadingSpaceDummyClassLoaded = false; /** * Dummy class to be loaded by ClassPreLoader in test. @@ -20,15 +21,35 @@ static class DummyClass { dummyClassLoaded = true; } } + + /** + * Dummy class whose name is referenced with a leading space in + * powertools-common/src/test/resources/classesloaded.txt. + * Some modules ship a classesloaded.txt with a leading space on every line, so the loader must + * strip leading whitespace before calling Class.forName. + */ + static class LeadingSpaceDummyClass { + static { + leadingSpaceDummyClassLoaded = true; + } + } + @Test void preloadClasses_shouldIgnoreInvalidClassesAndLoadValidClasses() { dummyClassLoaded = false; - // powertools-common/src/test/resources/classesloaded.txt has a class that does not exist + leadingSpaceDummyClassLoaded = false; + // A class only runs its static initializer the first time it is loaded, so this test calls + // preloadClasses once and asserts on all classes listed in classesloaded.txt together. + // powertools-common/src/test/resources/classesloaded.txt has a class that does not exist. // Verify that the missing class did not throw any exception assertDoesNotThrow(ClassPreLoader::preloadClasses); - // When the classloaded.txt is a mixed bag of valid and invalid classes, Valid class must load + // When the classloaded.txt is a mixed bag of valid and invalid classes, valid classes must load assertTrue(dummyClassLoaded, "DummyClass should be loaded"); + // classesloaded.txt references LeadingSpaceDummyClass with a leading space. The loader must + // strip it before Class.forName, otherwise the class is never loaded. + assertTrue(leadingSpaceDummyClassLoaded, + "LeadingSpaceDummyClass should be loaded despite the leading space"); } } \ No newline at end of file diff --git a/powertools-common/src/test/resources/classesloaded.txt b/powertools-common/src/test/resources/classesloaded.txt index 498ffdf69..a6cda309e 100644 --- a/powertools-common/src/test/resources/classesloaded.txt +++ b/powertools-common/src/test/resources/classesloaded.txt @@ -1,2 +1,3 @@ software.amazon.lambda.powertools.common.internal.NonExistingClass -software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$DummyClass \ No newline at end of file +software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$DummyClass + software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$LeadingSpaceDummyClass diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponseWithCustomMapper.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponseWithCustomMapper.java index a18b1580d..3627ab355 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponseWithCustomMapper.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponseWithCustomMapper.java @@ -28,7 +28,11 @@ import software.amazon.lambda.powertools.tracing.TracingUtils; public class PowerTracerToolEnabledForResponseWithCustomMapper implements RequestHandler { - static { + public PowerTracerToolEnabledForResponseWithCustomMapper() { + // Set the custom mapper in the constructor rather than a static initializer. A static + // initializer only runs the first time this class is loaded, which makes the resulting + // global state depend on when the class happens to be loaded (for example by SnapStart + // class priming). Setting it in the constructor makes each instantiation deterministic. ObjectMapper objectMapper = new ObjectMapper(); SimpleModule simpleModule = new SimpleModule(); simpleModule.addSerializer(ChildClass.class, new ChildSerializer()); diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java index b22f7a9af..bf0e9aa90 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java @@ -41,6 +41,7 @@ import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledExplicitlyForResponseAndError; import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForError; import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForResponse; +import software.amazon.lambda.powertools.tracing.TracingUtils; import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForResponseWithCustomMapper; import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForStream; import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForStreamWithNoMetaData; @@ -70,6 +71,10 @@ void setUp() throws IllegalAccessException { @AfterEach void tearDown() { AWSXRay.endSegment(); + // Some tests (e.g. the custom mapper handler) set the static TracingUtils.objectMapper. + // Reset it so it does not leak into later tests running in the same JVM fork and change how + // responses and errors are serialized. + TracingUtils.defaultObjectMapper(null); } @Test