Skip to content

Commit 5b872a7

Browse files
runningcodeclaude
andcommitted
perf(core): Cache class lookups and collapse double probes
Class availability is fixed for the lifetime of the process, so cache LoadClass results to avoid repeated Class.forName lookups (and the exceptions thrown for absent classes) when the same class is probed more than once. Also collapse the isClassAvailable-then-loadClass double probe in the OpenTelemetry span/scopes factories into a single loadClass call. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f6074f1 commit 5b872a7

3 files changed

Lines changed: 53 additions & 35 deletions

File tree

‎sentry/src/main/java/io/sentry/ScopesStorageFactory.java‎

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,23 @@ public final class ScopesStorageFactory {
2323
private static @NotNull IScopesStorage createInternal(
2424
final @NotNull LoadClass loadClass, final @NotNull ILogger logger) {
2525
if (Platform.isJvm()) {
26-
if (loadClass.isClassAvailable(OTEL_SCOPES_STORAGE, logger)) {
27-
Class<?> otelScopesStorageClazz = loadClass.loadClass(OTEL_SCOPES_STORAGE, logger);
28-
if (otelScopesStorageClazz != null) {
29-
try {
30-
final @Nullable Object otelScopesStorage =
31-
otelScopesStorageClazz.getDeclaredConstructor().newInstance();
32-
if (otelScopesStorage instanceof IScopesStorage) {
33-
return (IScopesStorage) otelScopesStorage;
34-
}
35-
} catch (InstantiationException e) {
36-
// TODO log
37-
} catch (IllegalAccessException e) {
38-
// TODO log
39-
} catch (InvocationTargetException e) {
40-
// TODO log
41-
} catch (NoSuchMethodException e) {
42-
// TODO log
26+
final @Nullable Class<?> otelScopesStorageClazz =
27+
loadClass.loadClass(OTEL_SCOPES_STORAGE, logger);
28+
if (otelScopesStorageClazz != null) {
29+
try {
30+
final @Nullable Object otelScopesStorage =
31+
otelScopesStorageClazz.getDeclaredConstructor().newInstance();
32+
if (otelScopesStorage instanceof IScopesStorage) {
33+
return (IScopesStorage) otelScopesStorage;
4334
}
35+
} catch (InstantiationException e) {
36+
// TODO log
37+
} catch (IllegalAccessException e) {
38+
// TODO log
39+
} catch (InvocationTargetException e) {
40+
// TODO log
41+
} catch (NoSuchMethodException e) {
42+
// TODO log
4443
}
4544
}
4645
}

‎sentry/src/main/java/io/sentry/SpanFactoryFactory.java‎

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,23 @@ public final class SpanFactoryFactory {
1515
public static @NotNull ISpanFactory create(
1616
final @NotNull LoadClass loadClass, final @NotNull ILogger logger) {
1717
if (Platform.isJvm()) {
18-
if (loadClass.isClassAvailable(OTEL_SPAN_FACTORY, logger)) {
19-
Class<?> otelSpanFactoryClazz = loadClass.loadClass(OTEL_SPAN_FACTORY, logger);
20-
if (otelSpanFactoryClazz != null) {
21-
try {
22-
final @Nullable Object otelSpanFactory =
23-
otelSpanFactoryClazz.getDeclaredConstructor().newInstance();
24-
if (otelSpanFactory instanceof ISpanFactory) {
25-
return (ISpanFactory) otelSpanFactory;
26-
}
27-
} catch (InstantiationException e) {
28-
// TODO log
29-
} catch (IllegalAccessException e) {
30-
// TODO log
31-
} catch (InvocationTargetException e) {
32-
// TODO log
33-
} catch (NoSuchMethodException e) {
34-
// TODO log
18+
final @Nullable Class<?> otelSpanFactoryClazz =
19+
loadClass.loadClass(OTEL_SPAN_FACTORY, logger);
20+
if (otelSpanFactoryClazz != null) {
21+
try {
22+
final @Nullable Object otelSpanFactory =
23+
otelSpanFactoryClazz.getDeclaredConstructor().newInstance();
24+
if (otelSpanFactory instanceof ISpanFactory) {
25+
return (ISpanFactory) otelSpanFactory;
3526
}
27+
} catch (InstantiationException e) {
28+
// TODO log
29+
} catch (IllegalAccessException e) {
30+
// TODO log
31+
} catch (InvocationTargetException e) {
32+
// TODO log
33+
} catch (NoSuchMethodException e) {
34+
// TODO log
3635
}
3736
}
3837
}

‎sentry/src/main/java/io/sentry/util/LoadClass.java‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,25 @@
44
import io.sentry.ILogger;
55
import io.sentry.SentryLevel;
66
import io.sentry.SentryOptions;
7+
import java.util.Map;
8+
import java.util.concurrent.ConcurrentHashMap;
79
import org.jetbrains.annotations.NotNull;
810
import org.jetbrains.annotations.Nullable;
911

1012
/** An Adapter for making Class.forName testable */
1113
@Open
1214
public class LoadClass {
1315

16+
/** Sentinel cached for class names that are known to be unavailable. */
17+
private static final Object NOT_AVAILABLE = new Object();
18+
19+
/**
20+
* Whether a class is on the classpath does not change during the lifetime of the process, so
21+
* results are cached to avoid repeated {@link Class#forName} lookups (and the exceptions they
22+
* throw for absent classes) when the same class is probed more than once.
23+
*/
24+
private static final Map<String, Object> CLASSES = new ConcurrentHashMap<>();
25+
1426
/**
1527
* Loads and initializes a class via reflection. Use this when you intend to actually use the
1628
* class (e.g. instantiate it or invoke its methods). The returned class is fully initialized, so
@@ -27,9 +39,17 @@ public class LoadClass {
2739

2840
private @Nullable Class<?> loadClass(
2941
final @NotNull String clazz, final @Nullable ILogger logger, final boolean initialize) {
42+
final @Nullable Object cached = CLASSES.get(clazz);
43+
if (cached != null) {
44+
return cached == NOT_AVAILABLE ? null : (Class<?>) cached;
45+
}
3046
try {
31-
return Class.forName(clazz, initialize, LoadClass.class.getClassLoader());
47+
final Class<?> loadedClass =
48+
Class.forName(clazz, initialize, LoadClass.class.getClassLoader());
49+
CLASSES.put(clazz, loadedClass);
50+
return loadedClass;
3251
} catch (ClassNotFoundException e) {
52+
CLASSES.put(clazz, NOT_AVAILABLE);
3353
if (logger != null) {
3454
logger.log(SentryLevel.INFO, "Class not available: " + clazz);
3555
}

0 commit comments

Comments
 (0)