Skip to content

Commit f6074f1

Browse files
runningcodeclaude
andcommitted
perf(core): Limit no-init class probing to isClassAvailable
The previous change made loadClass itself skip class initialization, which affected callers that load a class to actually use it (NDK integration, OTEL span factory and scopes storage). Restore loadClass to its initializing behavior and confine the non-initializing probe to isClassAvailable, which is only ever used for classpath availability checks. This keeps SDK init cheap while leaving real-use callers unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 552d466 commit f6074f1

2 files changed

Lines changed: 59 additions & 12 deletions

File tree

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

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,23 @@
1212
public class LoadClass {
1313

1414
/**
15-
* Try to load a class via reflection
15+
* Loads and initializes a class via reflection. Use this when you intend to actually use the
16+
* class (e.g. instantiate it or invoke its methods). The returned class is fully initialized, so
17+
* its static initializers run. To merely check whether a class is on the classpath, use {@link
18+
* #isClassAvailable} instead, which avoids running those initializers.
1619
*
1720
* @param clazz the full class name
1821
* @param logger an instance of ILogger
1922
* @return a Class&lt;?&gt; if it's available, or null
2023
*/
2124
public @Nullable Class<?> loadClass(final @NotNull String clazz, final @Nullable ILogger logger) {
25+
return loadClass(clazz, logger, true);
26+
}
27+
28+
private @Nullable Class<?> loadClass(
29+
final @NotNull String clazz, final @Nullable ILogger logger, final boolean initialize) {
2230
try {
23-
// Don't initialize the class just to probe for availability; it gets initialized lazily on
24-
// first use. This avoids running unrelated static initializers during SDK init.
25-
return Class.forName(clazz, false, LoadClass.class.getClassLoader());
31+
return Class.forName(clazz, initialize, LoadClass.class.getClassLoader());
2632
} catch (ClassNotFoundException e) {
2733
if (logger != null) {
2834
logger.log(SentryLevel.INFO, "Class not available: " + clazz);
@@ -39,15 +45,35 @@ public class LoadClass {
3945
return null;
4046
}
4147

48+
/**
49+
* Probes whether a class is on the classpath without initializing it. Use this for availability
50+
* checks (e.g. deciding whether to register an integration); the class is not initialized, so its
51+
* static initializers do not run until something actually uses it. This keeps SDK init cheap by
52+
* not triggering unrelated initializers. If you need to use the class, use {@link #loadClass}
53+
* instead.
54+
*
55+
* @param clazz the full class name
56+
* @param logger an instance of ILogger
57+
* @return true if the class is on the classpath
58+
*/
4259
public boolean isClassAvailable(final @NotNull String clazz, final @Nullable ILogger logger) {
43-
return loadClass(clazz, logger) != null;
60+
return loadClass(clazz, logger, false) != null;
4461
}
4562

4663
public boolean isClassAvailable(
4764
final @NotNull String clazz, final @Nullable SentryOptions options) {
4865
return isClassAvailable(clazz, options != null ? options.getLogger() : null);
4966
}
5067

68+
/**
69+
* Like {@link #isClassAvailable}, but defers the (non-initializing) availability check until the
70+
* result is first read. Use this when the check itself should not run during SDK init but only
71+
* later, on first access.
72+
*
73+
* @param clazz the full class name
74+
* @param logger an instance of ILogger
75+
* @return a lazily-evaluated availability check
76+
*/
5177
public LazyEvaluator<Boolean> isClassAvailableLazy(
5278
final @NotNull String clazz, final @Nullable ILogger logger) {
5379
return new LazyEvaluator<>(() -> isClassAvailable(clazz, logger));

sentry/src/test/java/io/sentry/util/LoadClassTest.kt

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import kotlin.test.Test
44
import kotlin.test.assertFalse
55
import kotlin.test.assertNotNull
66
import kotlin.test.assertNull
7+
import kotlin.test.assertTrue
78

89
class LoadClassTest {
910
@Test
@@ -26,24 +27,44 @@ class LoadClassTest {
2627
}
2728

2829
@Test
29-
fun `loadClass does not run the static initializer of the probed class`() {
30+
fun `isClassAvailable does not run the static initializer of the probed class`() {
3031
// Reading the flag initializes the flag holder, not the probe.
31-
assertFalse(LoadClassNoInitFlag.initialized)
32+
assertFalse(IsClassAvailableNoInitFlag.initialized)
3233

3334
// Obtaining the name via ::class.java does not initialize the probe either.
34-
LoadClass().loadClass(LoadClassNoInitProbe::class.java.name, null)
35+
LoadClass()
36+
.isClassAvailable(IsClassAvailableNoInitProbe::class.java.name, null as io.sentry.ILogger?)
3537

3638
// Availability probing must not trigger the probe's static initializer.
37-
assertFalse(LoadClassNoInitFlag.initialized)
39+
assertFalse(IsClassAvailableNoInitFlag.initialized)
40+
}
41+
42+
@Test
43+
fun `loadClass runs the static initializer of the loaded class`() {
44+
assertFalse(LoadClassInitFlag.initialized)
45+
46+
LoadClass().loadClass(LoadClassInitProbe::class.java.name, null)
47+
48+
assertTrue(LoadClassInitFlag.initialized)
49+
}
50+
}
51+
52+
private object IsClassAvailableNoInitFlag {
53+
@JvmField var initialized = false
54+
}
55+
56+
private object IsClassAvailableNoInitProbe {
57+
init {
58+
IsClassAvailableNoInitFlag.initialized = true
3859
}
3960
}
4061

41-
private object LoadClassNoInitFlag {
62+
private object LoadClassInitFlag {
4263
@JvmField var initialized = false
4364
}
4465

45-
private object LoadClassNoInitProbe {
66+
private object LoadClassInitProbe {
4667
init {
47-
LoadClassNoInitFlag.initialized = true
68+
LoadClassInitFlag.initialized = true
4869
}
4970
}

0 commit comments

Comments
 (0)