Skip to content

Commit 54c6166

Browse files
authored
fix(common): strip leading whitespace in ClassPreLoader (#2560)
* fix(common): strip leading whitespace in ClassPreLoader ClassPreLoader used line.stripTrailing() when parsing classesloaded.txt, which keeps leading whitespace. Modules that ship the file with a leading space on every line (for example powertools-tracing) passed names such as " java.lang.Object" to Class.forName, which throws ClassNotFoundException, so SnapStart class priming loaded 0 classes. Use line.strip() so both leading and trailing whitespace are removed. Add a regression test that references a class with a leading space. Closes #2559 * fix(common): reject malformed classesloaded.txt entries and fix tracing test isolation Harden ClassPreLoader to only pass well-formed binary class names to Class.forName. Some modules ship classesloaded.txt with malformed entries such as runtime-synthetic lambda classes and lines that glue a class name to a file path, none of which are loadable. Add a debug log reporting how many classes were preloaded. Fixing the leading-whitespace parsing means the well-formed classes in powertools-tracing now actually load during priming. That exposed a pre-existing test isolation defect: PowerTracerToolEnabledForResponseWithCustomMapper set the static TracingUtils.objectMapper from a static initializer, which leaked into later tests in the same JVM fork and changed how errors were serialized. Set the mapper from the constructor instead and reset it in the aspect test teardown. Relates to #2559 * fix(common): simplify class name pattern to avoid backtracking Replace the nested-quantifier regex with a single linear character class. SonarCloud flagged the previous pattern for possible catastrophic backtracking on large inputs. The character class rejects the same malformed entries (paths, URL-encoded spaces, synthetic lambda names) without the backtracking risk. * refactor(common): drop class name pattern, rely on Class.forName Remove the binary class name pattern matching. Class.forName already rejects malformed entries by throwing ClassNotFoundException, which is caught and ignored, so the extra pattern check only duplicated that rejection and added complexity. The tracing test failures were caused by test ordering (a leaked static ObjectMapper), not by malformed entries, and that fix is retained.
1 parent 4f869f5 commit 54c6166

5 files changed

Lines changed: 50 additions & 9 deletions

File tree

powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/ClassPreLoader.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,17 @@
2222
import java.net.URLConnection;
2323
import java.util.Enumeration;
2424

25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
2528
/**
2629
* Used to preload classes to support automatic priming for SnapStart
2730
*/
2831
public final class ClassPreLoader {
2932
public static final String CLASSES_FILE = "classesloaded.txt";
3033

34+
private static final Logger LOG = LoggerFactory.getLogger(ClassPreLoader.class);
35+
3136
private ClassPreLoader() {
3237
// Hide default constructor
3338
}
@@ -57,6 +62,7 @@ public static void preloadClasses() {
5762
* @param is
5863
*/
5964
private static void preloadClassesFromStream(InputStream is) {
65+
int loaded = 0;
6066
try (is;
6167
InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
6268
BufferedReader reader = new BufferedReader(isr)) {
@@ -66,26 +72,30 @@ private static void preloadClassesFromStream(InputStream is) {
6672
if (idx != -1) {
6773
line = line.substring(0, idx);
6874
}
69-
final String className = line.stripTrailing();
70-
if (!className.isBlank()) {
71-
loadClassIfFound(className);
75+
final String className = line.strip();
76+
if (!className.isBlank() && loadClassIfFound(className)) {
77+
loaded++;
7278
}
7379
}
7480
} catch (Exception ignored) {
7581
// No action is required if preloading fails for any reason
7682
}
83+
LOG.debug("SnapStart priming: preloaded {} class(es) from {}", loaded, CLASSES_FILE);
7784
}
7885

7986
/**
8087
* Initializes the class with given name if found, ignores otherwise
8188
*
82-
* @param className
89+
* @param className the binary name of the class to load
90+
* @return true if the class was found and loaded, false otherwise
8391
*/
84-
private static void loadClassIfFound(String className) {
92+
private static boolean loadClassIfFound(String className) {
8593
try {
8694
Class.forName(className, true, ClassPreLoader.class.getClassLoader());
95+
return true;
8796
} catch (ClassNotFoundException e) {
8897
// No action is required if the class with given name cannot be found
98+
return false;
8999
}
90100
}
91101
}

powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/ClassPreLoaderTest.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class ClassPreLoaderTest {
88

99
// Making this volatile so the Thread Context doesn't need any special handling
1010
static volatile boolean dummyClassLoaded = false;
11+
static volatile boolean leadingSpaceDummyClassLoaded = false;
1112

1213
/**
1314
* Dummy class to be loaded by ClassPreLoader in test.
@@ -20,15 +21,35 @@ static class DummyClass {
2021
dummyClassLoaded = true;
2122
}
2223
}
24+
25+
/**
26+
* Dummy class whose name is referenced with a leading space in
27+
* <i>powertools-common/src/test/resources/classesloaded.txt</i>.
28+
* Some modules ship a classesloaded.txt with a leading space on every line, so the loader must
29+
* strip leading whitespace before calling Class.forName.
30+
*/
31+
static class LeadingSpaceDummyClass {
32+
static {
33+
leadingSpaceDummyClassLoaded = true;
34+
}
35+
}
36+
2337
@Test
2438
void preloadClasses_shouldIgnoreInvalidClassesAndLoadValidClasses() {
2539

2640
dummyClassLoaded = false;
27-
// powertools-common/src/test/resources/classesloaded.txt has a class that does not exist
41+
leadingSpaceDummyClassLoaded = false;
42+
// A class only runs its static initializer the first time it is loaded, so this test calls
43+
// preloadClasses once and asserts on all classes listed in classesloaded.txt together.
44+
// powertools-common/src/test/resources/classesloaded.txt has a class that does not exist.
2845
// Verify that the missing class did not throw any exception
2946
assertDoesNotThrow(ClassPreLoader::preloadClasses);
3047

31-
// When the classloaded.txt is a mixed bag of valid and invalid classes, Valid class must load
48+
// When the classloaded.txt is a mixed bag of valid and invalid classes, valid classes must load
3249
assertTrue(dummyClassLoaded, "DummyClass should be loaded");
50+
// classesloaded.txt references LeadingSpaceDummyClass with a leading space. The loader must
51+
// strip it before Class.forName, otherwise the class is never loaded.
52+
assertTrue(leadingSpaceDummyClassLoaded,
53+
"LeadingSpaceDummyClass should be loaded despite the leading space");
3354
}
3455
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
software.amazon.lambda.powertools.common.internal.NonExistingClass
2-
software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$DummyClass
2+
software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$DummyClass
3+
software.amazon.lambda.powertools.common.internal.ClassPreLoaderTest$LeadingSpaceDummyClass

powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponseWithCustomMapper.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@
2828
import software.amazon.lambda.powertools.tracing.TracingUtils;
2929

3030
public class PowerTracerToolEnabledForResponseWithCustomMapper implements RequestHandler<Object, Object> {
31-
static {
31+
public PowerTracerToolEnabledForResponseWithCustomMapper() {
32+
// Set the custom mapper in the constructor rather than a static initializer. A static
33+
// initializer only runs the first time this class is loaded, which makes the resulting
34+
// global state depend on when the class happens to be loaded (for example by SnapStart
35+
// class priming). Setting it in the constructor makes each instantiation deterministic.
3236
ObjectMapper objectMapper = new ObjectMapper();
3337
SimpleModule simpleModule = new SimpleModule();
3438
simpleModule.addSerializer(ChildClass.class, new ChildSerializer());

powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledExplicitlyForResponseAndError;
4242
import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForError;
4343
import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForResponse;
44+
import software.amazon.lambda.powertools.tracing.TracingUtils;
4445
import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForResponseWithCustomMapper;
4546
import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForStream;
4647
import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForStreamWithNoMetaData;
@@ -70,6 +71,10 @@ void setUp() throws IllegalAccessException {
7071
@AfterEach
7172
void tearDown() {
7273
AWSXRay.endSegment();
74+
// Some tests (e.g. the custom mapper handler) set the static TracingUtils.objectMapper.
75+
// Reset it so it does not leak into later tests running in the same JVM fork and change how
76+
// responses and errors are serialized.
77+
TracingUtils.defaultObjectMapper(null);
7378
}
7479

7580
@Test

0 commit comments

Comments
 (0)