From 34f220b6faac5205d8b7e39c695c671a9ae4a5f2 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Wed, 10 Sep 2025 22:32:21 +0200 Subject: [PATCH 1/3] Add caching to `JavaParser.fromJavaVersion()` Avoiding calling `Class.forName()` is important in concurrent environments. As the method always returns a parser that corresponds to the JDK version and the `JavaParser` class should be loaded by the system class loader, it should be safe to cache the result. --- .../java/org/openrewrite/java/JavaParser.java | 138 ++++++++++-------- 1 file changed, 77 insertions(+), 61 deletions(-) diff --git a/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java b/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java index edab72dafc..88fb0ee428 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java @@ -16,6 +16,7 @@ package org.openrewrite.java; import io.github.classgraph.ClassGraph; +import lombok.experimental.UtilityClass; import org.intellij.lang.annotations.Language; import org.jspecify.annotations.Nullable; import org.openrewrite.*; @@ -29,6 +30,7 @@ import org.openrewrite.style.NamedStyles; import java.io.ByteArrayInputStream; +import java.lang.reflect.Method; import java.net.URI; import java.nio.charset.Charset; import java.nio.file.Path; @@ -156,67 +158,7 @@ static List dependenciesFromResources(ExecutionContext ctx, String... arti * Builds a Java parser with a language level equal to that of the JDK running this JVM process. */ static JavaParser.Builder fromJavaVersion() { - String[] versionParts = System.getProperty("java.version").split("[.-]"); - int version = Integer.parseInt(versionParts[0]); - if (version == 1) { - version = 8; - } - - if (version > 21) { - try { - return (JavaParser.Builder) Class - .forName("org.openrewrite.java.Java25Parser") - .getDeclaredMethod("builder") - .invoke(null); - } catch (Exception e) { - //Fall through, look for a parser on an older version. - } - } - - if (version > 17) { - try { - return (JavaParser.Builder) Class - .forName("org.openrewrite.java.Java21Parser") - .getDeclaredMethod("builder") - .invoke(null); - } catch (Exception e) { - //Fall through, look for a parser on an older version. - } - } - - if (version > 11) { - try { - return (JavaParser.Builder) Class - .forName("org.openrewrite.java.Java17Parser") - .getDeclaredMethod("builder") - .invoke(null); - } catch (Exception e) { - //Fall through, look for a parser on an older version. - } - } - - if (version > 8) { - try { - return (JavaParser.Builder) Class - .forName("org.openrewrite.java.Java11Parser") - .getDeclaredMethod("builder") - .invoke(null); - } catch (Exception e) { - //Fall through, look for a parser on an older version. - } - } - - try { - return (JavaParser.Builder) Class - .forName("org.openrewrite.java.Java8Parser") - .getDeclaredMethod("builder") - .invoke(null); - } catch (Exception e) { - //Fall through to an exception without making this the "cause". - } - - throw new IllegalStateException("Unable to create a Java parser instance. " + - "`rewrite-java-8`, `rewrite-java-11`, `rewrite-java-17`, `rewrite-java-21`, or `rewrite-java-25` must be on the classpath."); + return JdkParserBuilderCache.getBuilder(); } @Override @@ -447,3 +389,77 @@ static List getRuntimeClasspath() { return runtimeClasspath; } } + +@UtilityClass +class JdkParserBuilderCache { + // Cached supplier for the parser builder - initialized on first access + private static volatile @Nullable Function<@Nullable Void, JavaParser.Builder> cachedBuilderSupplier = null; + + static JavaParser.Builder getBuilder() { + Function<@Nullable Void, JavaParser.Builder> supplier = cachedBuilderSupplier; + if (supplier != null) { + return supplier.apply(null); + } + + synchronized (JdkParserBuilderCache.class) { + // Double-check after acquiring lock + supplier = cachedBuilderSupplier; + if (supplier != null) { + return supplier.apply(null); + } + + // Determine Java version once + String[] versionParts = System.getProperty("java.version").split("[.-]"); + int version = Integer.parseInt(versionParts[0]); + if (version == 1) { + version = 8; + } + + // Try to find and cache appropriate parser + if (version > 21) { + supplier = tryCreateBuilderSupplier("org.openrewrite.java.Java25Parser"); + } + + if (version > 17 && supplier == null) { + supplier = tryCreateBuilderSupplier("org.openrewrite.java.Java21Parser"); + } + + if (version > 11 && supplier == null) { + supplier = tryCreateBuilderSupplier("org.openrewrite.java.Java17Parser"); + } + + if (version > 8 && supplier == null) { + supplier = tryCreateBuilderSupplier("org.openrewrite.java.Java11Parser"); + } + + if (supplier == null) { + supplier = tryCreateBuilderSupplier("org.openrewrite.java.Java8Parser"); + } + + if (supplier != null) { + cachedBuilderSupplier = supplier; + return supplier.apply(null); + } + + throw new IllegalStateException("Unable to create a Java parser instance. " + + "`rewrite-java-8`, `rewrite-java-11`, `rewrite-java-17`, `rewrite-java-21`, or `rewrite-java-25` must be on the classpath."); + } + } + + @Nullable + private static Function> tryCreateBuilderSupplier(String className) { + try { + Class clazz = Class.forName(className); + Method builderMethod = clazz.getDeclaredMethod("builder"); + return unused -> { + try { + return (JavaParser.Builder) builderMethod.invoke(null); + } catch (Exception e) { + throw new RuntimeException("Failed to invoke builder() on " + className, e); + } + }; + } catch (ClassNotFoundException | NoSuchMethodException e) { + return null; // This parser version isn't available + } + } +} From 9d4297f816c7a1ffdcf9b8d531976487f021789d Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Thu, 11 Sep 2025 15:02:38 +0200 Subject: [PATCH 2/3] Polish --- .../java/org/openrewrite/java/JavaParser.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java b/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java index 88fb0ee428..6aa3ff1047 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java @@ -37,6 +37,7 @@ import java.nio.file.Paths; import java.util.*; import java.util.function.Function; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -393,19 +394,19 @@ static List getRuntimeClasspath() { @UtilityClass class JdkParserBuilderCache { // Cached supplier for the parser builder - initialized on first access - private static volatile @Nullable Function<@Nullable Void, JavaParser.Builder> cachedBuilderSupplier = null; + private static volatile @Nullable Supplier> cachedBuilderSupplier = null; static JavaParser.Builder getBuilder() { - Function<@Nullable Void, JavaParser.Builder> supplier = cachedBuilderSupplier; + Supplier> supplier = cachedBuilderSupplier; if (supplier != null) { - return supplier.apply(null); + return supplier.get(); } synchronized (JdkParserBuilderCache.class) { // Double-check after acquiring lock supplier = cachedBuilderSupplier; if (supplier != null) { - return supplier.apply(null); + return supplier.get(); } // Determine Java version once @@ -438,7 +439,7 @@ class JdkParserBuilderCache { if (supplier != null) { cachedBuilderSupplier = supplier; - return supplier.apply(null); + return supplier.get(); } throw new IllegalStateException("Unable to create a Java parser instance. " + @@ -446,14 +447,14 @@ class JdkParserBuilderCache { } } - @Nullable - private static Function> tryCreateBuilderSupplier(String className) { + private static @Nullable Supplier> tryCreateBuilderSupplier(String className) { try { Class clazz = Class.forName(className); Method builderMethod = clazz.getDeclaredMethod("builder"); - return unused -> { + return () -> { try { - return (JavaParser.Builder) builderMethod.invoke(null); + //noinspection rawtypes,unchecked + return (JavaParser.Builder) builderMethod.invoke(null); } catch (Exception e) { throw new RuntimeException("Failed to invoke builder() on " + className, e); } From 753d8fdee34637de5ce835a4dc19e59253c3f8ef Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Thu, 11 Sep 2025 15:12:05 +0200 Subject: [PATCH 3/3] Also use DCL for RuntimeClasspathCache --- .../java/org/openrewrite/java/JavaParser.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java b/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java index 6aa3ff1047..37a4219f24 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java @@ -371,23 +371,27 @@ static Path resolveSourcePathFromSourceText(Path prefix, String sourceCode) { } } +@UtilityClass class RuntimeClasspathCache { - private RuntimeClasspathCache() { - } - @Nullable - private static List runtimeClasspath = null; + private static volatile List runtimeClasspath = null; static List getRuntimeClasspath() { - if (runtimeClasspath == null) { - runtimeClasspath = new ClassGraph() - .disableNestedJarScanning() - .getClasspathURIs().stream() - .filter(uri -> "file".equals(uri.getScheme())) - .map(Paths::get) - .collect(toList()); + List paths = runtimeClasspath; + if (paths == null) { + synchronized (RuntimeClasspathCache.class) { + paths = runtimeClasspath; + if (paths == null) { + runtimeClasspath = paths = new ClassGraph() + .disableNestedJarScanning() + .getClasspathURIs().stream() + .filter(uri -> "file".equals(uri.getScheme())) + .map(Paths::get) + .collect(toList()); + } + } } - return runtimeClasspath; + return paths; } }