Skip to content

Commit 3feaaa4

Browse files
committed
refactor(appsec): cache reflection and use try-with-resources in ParameterCollector
- Replace per-call getMethod() with volatile (Class, Method) entry cache so reflection resolves once per concrete Part class rather than per file per request. - Use try-with-resources for InputStream in readContent() instead of try/finally. - Add unit test covering the Tomcat 7 getFilename() fallback path.
1 parent d73b5da commit 3feaaa4

2 files changed

Lines changed: 72 additions & 13 deletions

File tree

dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/src/main/java/datadog/trace/instrumentation/tomcat7/ParameterCollector.java

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.io.InputStream;
44
import java.lang.reflect.Method;
55
import java.nio.charset.StandardCharsets;
6+
import java.util.AbstractMap;
67
import java.util.ArrayList;
78
import java.util.Collections;
89
import java.util.HashMap;
@@ -142,11 +143,23 @@ public List<String> getContents() {
142143
return contents != null ? contents : Collections.<String>emptyList();
143144
}
144145

146+
// Entry caches (class → method) stored as a single volatile write for safe publication.
147+
// Keyed by Part concrete class; re-resolved when the class changes (different Tomcat version).
148+
private static volatile Map.Entry<Class<?>, Method> cachedInputStreamEntry;
149+
private static volatile Map.Entry<Class<?>, Method> cachedFilenameEntry;
150+
145151
private static String readContent(Object part) {
146152
try {
147-
Method m = part.getClass().getMethod("getInputStream");
148-
InputStream is = (InputStream) m.invoke(part);
149-
try {
153+
Class<?> partClass = part.getClass();
154+
Map.Entry<Class<?>, Method> entry = cachedInputStreamEntry;
155+
Method m;
156+
if (entry == null || entry.getKey() != partClass) {
157+
m = partClass.getMethod("getInputStream");
158+
cachedInputStreamEntry = new AbstractMap.SimpleImmutableEntry<>(partClass, m);
159+
} else {
160+
m = entry.getValue();
161+
}
162+
try (InputStream is = (InputStream) m.invoke(part)) {
150163
byte[] buf = new byte[MAX_CONTENT_BYTES];
151164
int total = 0;
152165
int n;
@@ -155,28 +168,43 @@ private static String readContent(Object part) {
155168
total += n;
156169
}
157170
return new String(buf, 0, total, StandardCharsets.ISO_8859_1);
158-
} finally {
159-
is.close();
160171
}
161172
} catch (Exception ignored) {
162173
return "";
163174
}
164175
}
165176

166177
private static String getFilename(Object part) {
167-
// Try getSubmittedFileName() first — Servlet 3.1+ / Tomcat 8+ (both javax and jakarta)
168-
try {
169-
Method m = part.getClass().getMethod("getSubmittedFileName");
170-
return (String) m.invoke(part);
171-
} catch (Exception ignored) {
178+
Class<?> partClass = part.getClass();
179+
Map.Entry<Class<?>, Method> entry = cachedFilenameEntry;
180+
Method m;
181+
if (entry == null || entry.getKey() != partClass) {
182+
m = null;
183+
// Try getSubmittedFileName() first — Servlet 3.1+ / Tomcat 8+ (both javax and jakarta)
184+
try {
185+
m = partClass.getMethod("getSubmittedFileName");
186+
} catch (Exception ignored) {
187+
}
188+
if (m == null) {
189+
// Fall back to getFilename() — Tomcat 7 ApplicationPart specific
190+
try {
191+
m = partClass.getMethod("getFilename");
192+
} catch (Exception ignored) {
193+
}
194+
}
195+
// null value in the entry means "no filename method on this class"
196+
cachedFilenameEntry = new AbstractMap.SimpleImmutableEntry<>(partClass, m);
197+
} else {
198+
m = entry.getValue();
199+
}
200+
if (m == null) {
201+
return null;
172202
}
173-
// Fall back to getFilename() — Tomcat 7 ApplicationPart specific
174203
try {
175-
Method m = part.getClass().getMethod("getFilename");
176204
return (String) m.invoke(part);
177205
} catch (Exception ignored) {
206+
return null;
178207
}
179-
return null;
180208
}
181209
}
182210
}

dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/src/test/groovy/datadog/trace/instrumentation/tomcat7/ParameterCollectorImplTest.groovy

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,18 @@ class ParameterCollectorImplTest extends Specification {
131131
collector.getFilenames() == ['file.txt']
132132
}
133133

134+
void 'addPart falls back to getFilename() when getSubmittedFileName() is absent (Tomcat 7)'() {
135+
given:
136+
def collector = new ParameterCollector.ParameterCollectorImpl(true)
137+
138+
when:
139+
collector.addPart(new Tomcat7Part('tomcat7.txt', 'tomcat7 content'))
140+
141+
then:
142+
collector.getContents() == ['tomcat7 content']
143+
collector.getFilenames() == ['tomcat7.txt']
144+
}
145+
134146
void 'ParameterCollectorNoop getContents returns empty list'() {
135147
expect:
136148
ParameterCollector.ParameterCollectorNoop.INSTANCE.getContents().isEmpty()
@@ -189,4 +201,23 @@ class ParameterCollectorImplTest extends Specification {
189201
new ByteArrayInputStream(bytes)
190202
}
191203
}
204+
205+
/** Simulates a Tomcat 7 ApplicationPart that only exposes getFilename(), not getSubmittedFileName(). */
206+
static class Tomcat7Part {
207+
private final String filename
208+
private final String content
209+
210+
Tomcat7Part(String filename, String content) {
211+
this.filename = filename
212+
this.content = content
213+
}
214+
215+
String getFilename() {
216+
filename
217+
}
218+
219+
InputStream getInputStream() {
220+
new ByteArrayInputStream((content ?: '').getBytes('ISO-8859-1'))
221+
}
222+
}
192223
}

0 commit comments

Comments
 (0)