diff --git a/CHANGELOG.md b/CHANGELOG.md index 92cd4b4eec..70aed91cb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ ## Unreleased -### Internal +### Performance - Probe class availability without initializing the class during SDK init ([#5635](https://github.com/getsentry/sentry-java/pull/5635)) +- Cache reflective class lookups and avoid double-probing during SDK init ([#5636](https://github.com/getsentry/sentry-java/pull/5636)) ## 8.45.0 diff --git a/sentry/src/main/java/io/sentry/ScopesStorageFactory.java b/sentry/src/main/java/io/sentry/ScopesStorageFactory.java index 37c0acf231..f09f0130c5 100644 --- a/sentry/src/main/java/io/sentry/ScopesStorageFactory.java +++ b/sentry/src/main/java/io/sentry/ScopesStorageFactory.java @@ -23,24 +23,23 @@ public final class ScopesStorageFactory { private static @NotNull IScopesStorage createInternal( final @NotNull LoadClass loadClass, final @NotNull ILogger logger) { if (Platform.isJvm()) { - if (loadClass.isClassAvailable(OTEL_SCOPES_STORAGE, logger)) { - Class otelScopesStorageClazz = loadClass.loadClass(OTEL_SCOPES_STORAGE, logger); - if (otelScopesStorageClazz != null) { - try { - final @Nullable Object otelScopesStorage = - otelScopesStorageClazz.getDeclaredConstructor().newInstance(); - if (otelScopesStorage instanceof IScopesStorage) { - return (IScopesStorage) otelScopesStorage; - } - } catch (InstantiationException e) { - // TODO log - } catch (IllegalAccessException e) { - // TODO log - } catch (InvocationTargetException e) { - // TODO log - } catch (NoSuchMethodException e) { - // TODO log + final @Nullable Class otelScopesStorageClazz = + loadClass.loadClass(OTEL_SCOPES_STORAGE, logger); + if (otelScopesStorageClazz != null) { + try { + final @Nullable Object otelScopesStorage = + otelScopesStorageClazz.getDeclaredConstructor().newInstance(); + if (otelScopesStorage instanceof IScopesStorage) { + return (IScopesStorage) otelScopesStorage; } + } catch (InstantiationException e) { + // TODO log + } catch (IllegalAccessException e) { + // TODO log + } catch (InvocationTargetException e) { + // TODO log + } catch (NoSuchMethodException e) { + // TODO log } } } diff --git a/sentry/src/main/java/io/sentry/SpanFactoryFactory.java b/sentry/src/main/java/io/sentry/SpanFactoryFactory.java index f0e3fcbb3c..aae97851bc 100644 --- a/sentry/src/main/java/io/sentry/SpanFactoryFactory.java +++ b/sentry/src/main/java/io/sentry/SpanFactoryFactory.java @@ -15,24 +15,23 @@ public final class SpanFactoryFactory { public static @NotNull ISpanFactory create( final @NotNull LoadClass loadClass, final @NotNull ILogger logger) { if (Platform.isJvm()) { - if (loadClass.isClassAvailable(OTEL_SPAN_FACTORY, logger)) { - Class otelSpanFactoryClazz = loadClass.loadClass(OTEL_SPAN_FACTORY, logger); - if (otelSpanFactoryClazz != null) { - try { - final @Nullable Object otelSpanFactory = - otelSpanFactoryClazz.getDeclaredConstructor().newInstance(); - if (otelSpanFactory instanceof ISpanFactory) { - return (ISpanFactory) otelSpanFactory; - } - } catch (InstantiationException e) { - // TODO log - } catch (IllegalAccessException e) { - // TODO log - } catch (InvocationTargetException e) { - // TODO log - } catch (NoSuchMethodException e) { - // TODO log + final @Nullable Class otelSpanFactoryClazz = + loadClass.loadClass(OTEL_SPAN_FACTORY, logger); + if (otelSpanFactoryClazz != null) { + try { + final @Nullable Object otelSpanFactory = + otelSpanFactoryClazz.getDeclaredConstructor().newInstance(); + if (otelSpanFactory instanceof ISpanFactory) { + return (ISpanFactory) otelSpanFactory; } + } catch (InstantiationException e) { + // TODO log + } catch (IllegalAccessException e) { + // TODO log + } catch (InvocationTargetException e) { + // TODO log + } catch (NoSuchMethodException e) { + // TODO log } } } diff --git a/sentry/src/main/java/io/sentry/util/LoadClass.java b/sentry/src/main/java/io/sentry/util/LoadClass.java index c639f62b9c..0bc203356e 100644 --- a/sentry/src/main/java/io/sentry/util/LoadClass.java +++ b/sentry/src/main/java/io/sentry/util/LoadClass.java @@ -4,6 +4,8 @@ import io.sentry.ILogger; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -11,6 +13,16 @@ @Open public class LoadClass { + /** Sentinel cached for class names that are known to be unavailable. */ + private static final Object NOT_AVAILABLE = new Object(); + + /** + * Whether a class is on the classpath does not change during the lifetime of the process, so + * results are cached to avoid repeated {@link Class#forName} lookups (and the exceptions they + * throw for absent classes) when the same class is probed more than once. + */ + private static final Map CLASSES = new ConcurrentHashMap<>(); + /** * Try to load a class via reflection * @@ -19,11 +31,18 @@ public class LoadClass { * @return a Class<?> if it's available, or null */ public @Nullable Class loadClass(final @NotNull String clazz, final @Nullable ILogger logger) { + final @Nullable Object cached = CLASSES.get(clazz); + if (cached != null) { + return cached == NOT_AVAILABLE ? null : (Class) cached; + } try { // Don't initialize the class just to probe for availability; it gets initialized lazily on // first use. This avoids running unrelated static initializers during SDK init. - return Class.forName(clazz, false, LoadClass.class.getClassLoader()); + final Class loadedClass = Class.forName(clazz, false, LoadClass.class.getClassLoader()); + CLASSES.put(clazz, loadedClass); + return loadedClass; } catch (ClassNotFoundException e) { + CLASSES.put(clazz, NOT_AVAILABLE); if (logger != null) { logger.log(SentryLevel.INFO, "Class not available: " + clazz); }